在上一篇博文中,我们深入的了解了SparkSql中的sql语句经过DDLParser、SparkSQLParser和SqlParser处理后得到了一个树结构的Unresolved Logical Plan,这也是我们每一次使用sparkSql时必然会执行的,但是对于一些不是立刻需要返回结果的造作,执行到这边也就结束了,只有遇到哪些诸如show,collect等需要立刻的返回结果的操作,我们才会继续后面的执行,这篇博文主要就是将这些。
在真正地需要计算的结果的时候,我们在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等一样,我们也先来看看这个类的继承关系:
那么这边调用的实际上是其父类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
如果给TreeNode应用Rule的TreeNode与之前相同,则退出当前的Rule,进行下一个Rule的处理,直到所有的Rule全部遍历结束。此时就意味着Unresolved LogicalPlan解析完毕,生成了一个Resolved LogicalPlan
与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的执行的整个过程便解析完毕,我们可以总结成下面的流程: