本文隶属于专栏《1000个问题搞定大数据技术体系》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!
本专栏目录结构和参考文献请见1000个问题搞定大数据技术体系
Spark SQL 工作流程源码解析(一)总览(基于 Spark 3.3.0)
Spark SQL 工作流程源码解析(二)parsing 阶段(基于 Spark 3.3.0)
Spark SQL 工作流程源码解析(三)analysis 阶段(基于 Spark 3.3.0)
Spark SQL 工作流程源码解析(四)optimization 阶段(基于 Spark 3.3.0)
Spark SQL 工作流程源码解析(五)planning 阶段(基于 Spark 3.3.0)
{"name": "Alice","age": 18,"sex": "Female","addr": ["address_1","address_2", " address_3"]}
{"name": "Thomas","age": 20, "sex": "Male","addr": ["address_1"]}
{"name": "Tom","age": 50, "sex": "Male","addr": ["address_1","address_2","address_3"]}
{"name": "Catalina","age": 30, "sex": "Female","addr": ["address_1","address_2"]}
package com.shockang.study.spark.sql.demo
import org.apache.log4j.{Level, Logger}
import org.apache.spark.sql.SparkSession
/**
* @author Shockang
*/
object SparkSQLExample {
val DATA_PATH = "/Users/shockang/code/spark-examples/data/simple/sql/user.json"
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.OFF)
val spark = SparkSession.builder.master("local[*]").appName("SparkSQLExample").getOrCreate()
val df = spark.read.json(DATA_PATH)
df.createTempView("t_user")
spark.sql("SELECT * FROM t_user").show
spark.stop()
}
}
先来看看控制台输出:
+--------------------+---+--------+------+
| addr|age| name| sex|
+--------------------+---+--------+------+
|[address_1, addre...| 18| Alice|Female|
| [address_1]| 20| Thomas| Male|
|[address_1, addre...| 50| Tom| Male|
|[address_1, addre...| 30|Catalina|Female|
+--------------------+---+--------+------+
接下来一行行源码来分析,上面的结果是如何得到的
建议按照我的这篇博客下载编译 Apache Spark 源码,边看源码边阅读这篇博客——编译 Apache Spark 源码报错?那是因为你漏掉了关键操作
上面创建数据表时虽然没有显示调用 SQL 语句(类似 CREATE TABLE 这样的),但是本质上也是 SQL 的一种(DDL),内部的执行过程涉及到的流程和 spark.sql 查询的流程是很类似的。所以,只对 spark.sql 的实现来具体分析。
spark.sql 来自 SparkSession.scala
文件,我们来看看具体的源码:
/**
* 使用 Spark 执行 SQL 查询,并将结果作为 DataFrame 返回。
* 此 API 会“急切地”运行 DDL/DML 命令,但遇到 SELECT 查询则不会。
*
* @since 2.0.0
*/
def sql(sqlText: String): DataFrame = withActive {
// 定义一个查询计划追踪器
val tracker = new QueryPlanningTracker
// 统计 parsing 阶段的开始和结束时间
val plan = tracker.measurePhase(QueryPlanningTracker.PARSING) {
// parsing 阶段
sessionState.sqlParser.parsePlan(sqlText)
}
// 将 parsing 阶段生成的逻辑计划经过处理生成 DataFrame 返回
Dataset.ofRows(self, plan, tracker)
}
/**
* 执行一段代码块,将当前会话设置为活跃会话,并且在完成的时候恢复之前的会话。
*/
private[sql] def withActive[T](block: => T): T = {
// 直接使用线程本地的活跃会话,以确保我们得到的会话实际上是事实上设置的而不是默认的会话。
// 这是为了防止一旦我们都搞完之后把默认会话升级到了活跃会话。
val old = SparkSession.activeThreadSession.get()
SparkSession.setActiveSession(this)
try block finally {
SparkSession.setActiveSession(old)
}
}
这里会涉及一个类:QueryPlanningTracker
,它是用于追踪查询计划中的运行时和相关统计信息的简单实用程序。
我们会追踪两个不同的概念:
Catalyst
规则。除了时间,我们还跟踪调用的数量和有效调用。值得注意的是在 object QueryPlanningTracker
里面定义了 4 大阶段
// 此处定义了通用阶段的列表。
val PARSING = "parsing"
val ANALYSIS = "analysis"
val OPTIMIZATION = "optimization"
val PLANNING = "planning"
所以,很明显,Apache Spark 的官方已经通过源码告诉了我们,Spark SQL 的工作流程就这 4 个阶段,这也是最标准的划分!
tracker.measurePhase(QueryPlanningTracker.PARSING)
这行源码的作用就是测量 parsing
阶段的开始和结束时间,后面在每一个阶段都会同样处理。
如果在同一阶段多次调用 tracker.measurePhase 函数,则记录的开始时间将是第一次调用的开始时间,记录的结束时间将是最后一次调用的结束时间。
这是在给定 SparkSession
实例中保存所有会话特定状态的类。
Spark 最初添加这个类的原因是方便将一个 SparkSession 的状态 copy 到另一个 SparkSession 中,所以,很明显,SparkSession
的所有特有状态都会交给 SessionState
来保存。
那么,具体保存了哪些状态呢?
我们来看看这个类的类定义:
private[sql] class SessionState(
sharedState: SharedState,
val conf: SQLConf,
val experimentalMethods: ExperimentalMethods,
val functionRegistry: FunctionRegistry,
val tableFunctionRegistry: TableFunctionRegistry,
val udfRegistration: UDFRegistration,
catalogBuilder: () => SessionCatalog,
val sqlParser: ParserInterface,
analyzerBuilder: () => Analyzer,
optimizerBuilder: () => Optimizer,
val planner: SparkPlanner,
val streamingQueryManagerBuilder: () => StreamingQueryManager,
val listenerManager: ExecutionListenerManager,
resourceLoaderBuilder: () => SessionResourceLoader,
createQueryExecution: (LogicalPlan, CommandExecutionMode.Value) => QueryExecution,
createClone: (SparkSession, SessionState) => SessionState,
val columnarRules: Seq[ColumnarRule],
val queryStagePrepRules: Seq[Rule[SparkPlan]])
每个构造参数对应的含义如下:
其中,比较核心的参数有:
按照 SQL 标准,Catalog
是一个宽泛概念,通常可以理解为一个容器或数据库对象命名空间中的一个层次,主要用来解决命名冲突等问题。
在 Spark SQL 系统中,Catalog
主要于各种函资源信息信息(数据库、数据表、数据视图、数据分区与函数等)的统一管理。
具体来讲,Spark SQL 中Catalog
体系就是以 SessionCatalog
为主,通过 SparkSession
来提供给外界调用。
一般,一个 SparkSession
对应一个 SessionCatalog
。
本质上, SessionCatalog
起到了一个代理的作用,对底层的元数据信息(例如,Hive Metastore)、临时表信息、视图信息和函数做了封装。
编译器通用接口,用来将 SQL 语句转换成 AST(抽象语法树),也就是 Unresolved Logical Plan
,这个接口中包含对 SQL 语句、Expression 表达式和 TableIdentifier 数据表标识符的解析方法。
ParserInterface 有两个主要实现类:
SparkSqlParser
用于外部调用,我们平常写的 SQL 都是它在解析。CatalystSqlParser
用于内部 Catalyst 引擎使用的解析器。== Parsed Logical Plan ==
'Project [*]
+- 'UnresolvedRelation [t_user], [], false
此处可能部分同学会对
Parsed Logical Plan
感到奇怪,实际上Unresolved Logical Plan
这个概念来自 Spark SQL 的原始论文Spark SQL: Relational Data Processing in Spark 。大家只需要知道这两个概念是一回事情,只不过Unresolved Logical Plan
是约定俗成的叫法而已。
提供逻辑查询计划的分析器,这个分析器会使用 SessionCatalog
中的信息将 UnsolvedAttributes
和 UnsolvedRelationships
转换为有类型的对象。
其中,UnsolvedAttributes
保存尚未解析的属性的名称,UnsolvedRelationships
保存尚未在SessionCatalog
中查找的关系的名称。
简单来讲,就是将 Unresolved Logical Plan
转化成 Analyzed Logical Plan
。
这个过程主要会结合 DataFrame 的 Schema 信息(来自 SessionCatalog
),检查下面 3 点:
== Analyzed Logical Plan ==
addr: array, age: bigint, name: string, sex: string
Project [addr#7, age#8L, name#9, sex#10]
+- SubqueryAlias t_user
+- View (`t_user`, [addr#7,age#8L,name#9,sex#10])
+- Relation [addr#7,age#8L,name#9,sex#10] json
所有优化器都应该继承的抽象类,包含标准规则批次(扩展优化器可以覆盖它)。
其实例类是 SparkOptimizer
Optimizer 会基于启发式的规则,将 Analyzed Logical Plan
转化成 Optimized Logical Plan
。
其中,启发式的规则主要涉及以下 3 个方面的优化:
== Optimized Logical Plan ==
InMemoryRelation [addr#7, age#8L, name#9, sex#10], StorageLevel(disk, memory, deserialized, 1 replicas)
+- FileScan json [addr#7,age#8L,name#9,sex#10] Batched: false, DataFilters: [], Format: JSON, Location: InMemoryFileIndex(1 paths)[file:/Users/shockang/code/spark-examples/data/simple/sql/user.json], PartitionFilters: [], PushedFilters: [], ReadSchema: struct,age:bigint,name:string,sex:string>
基于既定的规则将逻辑计划转换成具体的物理计划(不涉及执行,只是提前规划),即将Optimized Logical Plan
转化成 Physical Plan
。
简单来说,逻辑计划就是“应该做什么”,物理计划就是“具体怎么做”。
物理计划包含 3 个子阶段:
Prepared SparkPlan
。最终生成的是一个 RDD
对象,并将其提交给 Spark Core 来执行。
后面涉及 Spark Core 的内容会专门写博客来详情阐述。
== Physical Plan ==
InMemoryTableScan [addr#7, age#8L, name#9, sex#10]
+- InMemoryRelation [addr#7, age#8L, name#9, sex#10], StorageLevel(disk, memory, deserialized, 1 replicas)
+- FileScan json [addr#7,age#8L,name#9,sex#10] Batched: false, DataFilters: [], Format: JSON, Location: InMemoryFileIndex(1 paths)[file:/Users/shockang/code/spark-examples/data/simple/sql/user.json], PartitionFilters: [], PushedFilters: [], ReadSchema: struct,age:bigint,name:string,sex:string>
细心的同学已经发现,上面的 4 个类/接口对应的就是前面提到的 4 大阶段
类/接口 | 阶段 | 执行计划输出 |
---|---|---|
ParserInterface | parsing | Unresolved Logical Plan |
Analyzer | analysis | Analyzed Logical Plan |
Optimizer | optimization | Optimized Logical Plan |
SparkPlanner | planning | Physical Plan |
将这 4 个阶段串联起来,就得到了 Spark SQL 最基本的工作流程:
注:此处给出的是基本的工作流程,其中不包含 AQE(自适应查询执行), CBO(基于成本的优化),WSCG(全阶段代码生成)等,完整的流程会在最后一讲讲完上述几个概念之后再给出。