Spark源码分析之SparkSql的Analyzer,Optimizer

在上一篇博文中,我们深入的了解了SparkSql中的sql语句经过DDLParser、SparkSQLParser和SqlParser处理后得到了一个树结构的Unresolved Logical Plan,这也是我们每一次使用sparkSql时必然会执行的,但是对于一些不是立刻需要返回结果的造作,执行到这边也就结束了,只有遇到哪些诸如show,collect等需要立刻的返回结果的操作,我们才会继续后面的执行,这篇博文主要就是将这些。

第一步:使用Analyzer将Unresolved Logical Plan与数据字典(Catalog)进行绑定,生成Resolved LogicalPlan

在真正地需要计算的结果的时候,我们在SQLContext中执行下面的方法:

//实际上在后面真正对DataFrame操作的时候需要真正的去执行sql语句的时候
// 就会触发sqlContext的executesql方法的执行
//该方法实际上会返回一个QueryExecution,这个QueryExecution就会触发后续的整个流程
protected[sql] def executeSql(sql: String): this.QueryExecution = executePlan(parseSql(sql))
protected[sql] def executePlan(plan: LogicalPlan) = new this.QueryExecution(plan)

既然这段代码的主要操作就是创建了一个QueryExecution对象,那么此时我们就去看看这段代码中干了些什么

protected[sql] class QueryExecution(val logical: LogicalPlan) {
    def assertAnalyzed(): Unit = analyzer.checkAnalysis(analyzed)

    //使用一个UnResolved LogicPlan去构造一个QueryExecution的实例对象
    //那么sql语句的设计执行就会立即一步一步的触发
    //调用analyzer来生成一个resolved LogicPlan
    lazy val analyzed: LogicalPlan = analyzer.execute(logical)

    //如果当前的这个执行计划缓存中有,那么就从缓存中读取
    lazy val withCachedData: LogicalPlan = {
      assertAnalyzed()
      cacheManager.useCachedData(analyzed)
    }

    //针对resolvedPlan调用optimizer的execute进行优化,得到优化后的optimized LogicalPlan
    //获得优化后的逻辑执行计划
    lazy val optimizedPlan: LogicalPlan = optimizer.execute(withCachedData)

    // TODO: Don't just pick the first one...
    //使用sparkPlanner根据刚刚创建的一个optimized LogicalPlan创建一个sparkplan
    lazy val sparkPlan: SparkPlan = {
      SparkPlan.currentContext.set(self)
      planner.plan(optimizedPlan).next()
    }

    /*在sparksql中,逻辑执行计划就是LogicalPlan,物理执行计划就是SparkPlan*/
    // executedPlan should not be used to initialize any SparkPlan. It should be
    // only used for execution.
    //生成一个可以执行的sparkplan,此时就是physicplan,此时就是物理执行计划
    //此时的话就已经绑定好了数据源,知各个表如何join
    //如果进行join,默认spark内部是会对小表进行广播的
    lazy val executedPlan: SparkPlan = prepareForExecution.execute(sparkPlan)

    /** Internal version of the RDD. Avoids copies and has no schema */
      //调用sparkPlan (封装了Physical plan)的execute方法,execute方法实际上就会执行物理执行计划
    lazy val toRdd: RDD[InternalRow] = executedPlan.execute()

    protected def stringOrError[A](f: => A): String =
      try f.toString catch { case e: Throwable => e.toString }

    def simpleString: String =
      s"""== Physical Plan ==
         |${stringOrError(executedPlan)}
      """.stripMargin.trim

    override def toString: String = {
      def output =
        analyzed.output.map(o => s"${o.name}: ${o.dataType.simpleString}").mkString(", ")

      s"""== Parsed Logical Plan ==
         |${stringOrError(logical)}
         |== Analyzed Logical Plan ==
         |${stringOrError(output)}
         |${stringOrError(analyzed)}
         |== Optimized Logical Plan ==
         |${stringOrError(optimizedPlan)}
         |== Physical Plan ==
         |${stringOrError(executedPlan)}
         |Code Generation: ${stringOrError(executedPlan.codegenEnabled)}
      """.stripMargin.trim
    }
  }

在此我们可以看到就是调用了analyzer的execute方法,与前面的DDLParser、SparkSQLParser、SQLParser等一样,我们也先来看看这个类的继承关系:
Spark源码分析之SparkSql的Analyzer,Optimizer_第1张图片
那么这边调用的实际上是其父类RuleExecutor中的execute方法:

def execute(plan: TreeType): TreeType = {
    var curPlan = plan
    batches.foreach { batch =>
      val batchStartPlan = curPlan
      var iteration = 1
      var lastPlan = curPlan
      var continue = true

      // Run until fix point (or the max number of iterations as specified in the strategy.
      while (continue) {
        curPlan = batch.rules.foldLeft(curPlan) {
          case (plan, rule) =>
            val startTime = System.nanoTime()
            val result = rule(plan)
            val runTime = System.nanoTime() - startTime
            RuleExecutor.timeMap.addAndGet(rule.ruleName, runTime)

            if (!result.fastEquals(plan)) {
              logTrace(
                s"""
                  |=== Applying Rule ${rule.ruleName} ===
                  |${sideBySide(plan.treeString, result.treeString).mkString("\n")}
                """.stripMargin)
            }

            result
        }
        iteration += 1
        if (iteration > batch.strategy.maxIterations) {
          // Only log if this is a rule that is supposed to run more than once.
          if (iteration != 2) {
            logInfo(s"Max iterations (${iteration - 1}) reached for batch ${batch.name}")
          }
          continue = false
        }

        if (curPlan.fastEquals(lastPlan)) {
          logTrace(
            s"Fixed point reached for batch ${batch.name} after ${iteration - 1} iterations.")
          continue = false
        }
        lastPlan = curPlan
      }

      if (!batchStartPlan.fastEquals(curPlan)) {
        logDebug(
          s"""
          |=== Result of Batch ${batch.name} ===
          |${sideBySide(plan.treeString, curPlan.treeString).mkString("\n")}
        """.stripMargin)
      } else {
        logTrace(s"Batch ${batch.name} has no effect.")
      }
    }

    curPlan
  }

理解这个方法之前我们先需要理解下面的概念:
在上一阶段生成的Unresolved LogicalPlan后,Analyzer和optimizer肯定是通过一系列的操作才将这个未解析的逻辑计划转换为resolved LogicalPlan的,那么这系列的操作就是一系列的Rule
那么这边的RuleExecutor的主要作用就是执行Rule
这个方法的主要作用就是通过遍历取出Analyzer中定义的batches里存储的每一个Batch,每一个Batch中会封装同属某一个类别的Rule及其相应的执行策略,我们看一段Analyzer中的Batch的代码

lazy val batches: Seq[Batch] = Seq(
    Batch("Substitution", fixedPoint,
      CTESubstitution ::
      WindowsSubstitution ::
      Nil : _*),
    Batch("Resolution", fixedPoint,
      ResolveRelations ::
      ResolveReferences ::
      ResolveGroupingAnalytics ::
      ResolveSortReferences ::
      ResolveGenerate ::
      ResolveFunctions ::
      ResolveAliases ::
      ExtractWindowExpressions ::
      GlobalAggregates ::
      ResolveAggregateFunctions ::
      HiveTypeCoercion.typeCoercionRules ++
      extendedResolutionRules : _*),
    Batch("Nondeterministic", Once,
      PullOutNondeterministic),
    Batch("Cleanup", fixedPoint,
      CleanupAliases)
  )

在这个里面的诸如CTESubstitution等都是一个Rule,它们继承自Rule类,我们还可以看到这边有一个fixedPoint,这个字段的意思就是当使用一个Rule的时候如果循环的次数达到了FixedPoint次,或者前后两次树结构没有变化那么就停止操作的策略。

下面我们再回到那个execute方法,它的意思其实就是去遍历每一个Batch,再遍历里面的每一个Rule,按照指定的次数应用到logicalPlan上,注意LogicalPlan的本质上就是一个TreeNode,其间接继承自TreeNode
Spark源码分析之SparkSql的Analyzer,Optimizer_第2张图片
如果给TreeNode应用Rule的TreeNode与之前相同,则退出当前的Rule,进行下一个Rule的处理,直到所有的Rule全部遍历结束。此时就意味着Unresolved LogicalPlan解析完毕,生成了一个Resolved LogicalPlan

第二步:使用Optimizer对刚刚生成的Resolved Logical Plan进行一系列的优化

与Analyzer的用法一样,只不过这边的Optimizer使用的是默认的实现DefaultOptimizer,在其中定义了很多的优化策略:

//这里封装了每一个Spark SQL中,可以对逻辑执行计划执行的优化策略
  val batches =
    // SubQueries are only needed for analysis and can be removed before execution.
    Batch("Remove SubQueries", FixedPoint(100),
      EliminateSubQueries) ::
    Batch("Aggregate", FixedPoint(100),
      ReplaceDistinctWithAggregate,
      RemoveLiteralFromGroupExpressions) ::
    Batch("Operator Optimizations", FixedPoint(100),
      // Operator push down
      SetOperationPushDown,
      SamplePushDown,
      PushPredicateThroughJoin,
      PushPredicateThroughProject,
      PushPredicateThroughGenerate,
      ColumnPruning,  //列剪裁,就是对我们要查询的列进行剪裁
      // Operator combine
      ProjectCollapsing,
      CombineFilters,
      CombineLimits,  //合并limit子句,取一个并集就可以了,这样的话在后面limit执行的时候就执行一次就可以了
      // Constant folding
      NullPropagation,  //针对null的优化,尽量避免值出现null的情况,否则null很容易产生数据倾斜
      OptimizeIn,
      ConstantFolding, //针对常量的优化,在这里会直接计算可以获得的常量,所以我们对自己的sql中可能出现的常量尽量直接给出
      LikeSimplification,//like的简化优化,
      BooleanSimplification,//Boolean的简化优化
      RemovePositive,
      SimplifyFilters,
      SimplifyCasts,
      SimplifyCaseConversionExpressions) ::
    Batch("Decimal Optimizations", FixedPoint(100),
      DecimalAggregates) ::
    Batch("LocalRelation", FixedPoint(100),
      ConvertToLocalRelation) :: Nil
}

在上面的代码中已经注释了一些优化的策略,了解这些优化对我们程序性能的提升还是有用处的,如果我们一开始写的sql就是按照这个优化策略来写的,那么这边就不需要再费大量的时间进行优化了

第三步:生成物理执行计划

经过SqlParser、Analyzer、Optimizer的处理,生成的逻辑计划还无法被当做一般的Job来处理,此时为了能够将逻辑执行计划按照其他Job一样对待,需要将logicalPlan转换为物理执行计划
主要的代码也就是prepareForExecution.execute(sparkPlan),那么此时prepareForExecution其实就是一个RuleExecutor的对象,只不过其会设置这个对象的batch值,具体的逻辑遇上面的Analyzer和Optimizer差不多,此处不再赘述

第四步:执行物理执行计划

调用executedPlan.execute()方法
至此整个SparkSQL的执行的整个过程便解析完毕,我们可以总结成下面的流程:
Spark源码分析之SparkSql的Analyzer,Optimizer_第3张图片

你可能感兴趣的:(spark)