目录
一、SparkSession 与 DataFrame、Dataset
一、SparkSession 与 DataFrame、Dataset
1. 要使用sparksql功能,需要需创建一个SparkSession对象,使用 SparkSession.builder()方法来创建:
val session = SparkSession
.builder()
.appName("Spark Hive Example")
.master("local[2]")
.getOrCreate()
2. Spark SQL中的 DataFrame 类似于一张关系型数据表。在关系型数据库中对单表或进行的查询操作,在DataFrame中都可以通过调用其API接口来实现。Dataset 与 DateFrame 的区别为 Dataset 中的泛型类 T 是某个确定类型,需要用户显示指定 schema 来确定,DataFrame 是 Dataset 的特定泛型类:type DataFrame = Dataset[Row]。创建 DataFrame 有3种方式:
- 基于已存在内存的数据对象,如List, RDD等:
//基于List对象创建 val peopleList = List(People(1, "zhangsan"), People(2, "Lisi")) val dfFromList = session.createDataFrame(peopleList) //基于Rdd创建 val textRdd = session.sparkContext.makeRDD(peopleList) val dfFromRdd = session.createDataFrame(textRdd)
- 使用 DataFrameReader 类的 api,该类支持读取 text、csv、json 等各种文件,也支持通过jdbc 连接读取表数据:
val dataReader = session.read dataReader.csv("/tmp/test.txt") dataReader.jdbc("jdbc:mysql://localhost:3306", "user", new Properties()) dataReader.json("/data1/people.json")
- 使用 session.sql() 方法读取 Hive 表数据:
val session = SparkSession .builder() .appName("Spark Hive Example") .config("spark.sql.warehouse.dir", "/apps/hive/warehouse") .config("spark.driver.memory", "1024m") .master("local[2]") .enableHiveSupport() .getOrCreate() session.sql("select * from people where age > 18")
二、Spark Sql 解析
1. 整体概览
一般来讲,对于Spark SQL 系统,从 SQL 到 Spark 中 RDD 的执行需要经过两个大的阶段,分别是逻辑计划(LogicalPlan)和物理计划(PhysicalPlan),如下图所示:
Spark sql 模块提供 sql 语法分析与编译功能,让我们写 sql 代码查询表数据,而无需调用繁杂的 api。语法分析模块在spark 源码的 sql/catalyst 目录下,使用 antlr4 框架生成语法树。sql 语法分析要经过 3 个阶段:
- 词法分析:将 sql 语句按规则切分成一个个 token。
- 句法分析:使用自上而下的递归下降分析方法生成语法树,树的各个节点在 antlr4 中使用 ParseTree 接口的子类 ParserRuleContext 来表示,在 spark 中继承了该类来表示各个节点。
- 语法树遍历:antlr4 使用 访问者模式 去访问这些树节点生成不同的对象。
实现上述过程需要 3 个关键角色:SqlParser,解析 sql 生成语法树。visitor,拜访者,遍历树节点;ParseDriver,驱动类,驱动代码运行。整体可以用伪代码表示如下:
public class MyVisitor extends SqlBaseBaseVisitor<String> {
public String visitSingleStatement(SqlBaseParser.SingleStatementContext ctx) {
System.out.println("visitSingleStatement");
return visitChildren(ctx);
}
public String visitSingleExpression(SqlBaseParser.SingleExpressionContext ctx) {
System.out.println("visitSingleExpression");
return visitChildren(ctx);
}
// ... 其余代码省略
}
public class ParserDriver {
public static void main(String[] args) {
String query = "select * from people where age > 18".toUpperCase();
SqlBaseLexer lexer = new SqlBaseLexer(new ANTLRInputStream(query));
SqlBaseParser parser = new SqlBaseParser(new CommonTokenStream(lexer));
MyVisitor visitor = new MyVisitor();
String res = visitor.visitSingleStatement(parser.singleStatement());
System.out.println("res="+res);
}
}
2. sql 语法解析关键对象
2.1 SparkSqlParser
Spark 使用 SparkSqlParser 类来解析 sql 语句,在 SparkSession.sql() 方法中被使用:
def sql(sqlText: String): DataFrame = {
Dataset.ofRows(self, sessionState.sqlParser.parsePlan(sqlText))
}
SparkSqlParser 类图结构如下所示:
上文提到了在 Catalyst 中,SQL 语法经过解析,生成的抽象语法树节点以 Context 结尾来命名,以 select id, name, age, sex from people where age > 18 order by id desc 这条为例,生成的语法树结构如下(我在图中只画出了关键树节点,实际节点复杂的多):
SparkSqlParser 将 sql 语句转换成语法树后,会使用自身实例属性 val astBuilder = new SparkSqlAstBuilder(conf) 去遍历这些树节点,从上面类图中我们知道该类是 SqlBaseBaseVistor 的子类,是一个 visitor,大部分逻辑在 AstBuilder 类中实现。
2.2 AstBuilder
从上文中我们知道 AstBuilder 是一个 visitor,用来遍历语法树,先来看一下该类的定义以及一些方法:
class AstBuilder(conf: SQLConf) extends SqlBaseBaseVisitor[AnyRef] with Logging {
override def visitSingleStatement(ctx: SingleStatementContext): LogicalPlan = withOrigin(ctx) {
visit(ctx.statement).asInstanceOf[LogicalPlan]
}
override def visitSingleExpression(ctx: SingleExpressionContext): Expression = withOrigin(ctx) {
visitNamedExpression(ctx.namedExpression)
}
override def visitSingleTableIdentifier(
ctx: SingleTableIdentifierContext): TableIdentifier = withOrigin(ctx) {
visitTableIdentifier(ctx.tableIdentifier)
}
override def visitSingleFunctionIdentifier(
ctx: SingleFunctionIdentifierContext): FunctionIdentifier = withOrigin(ctx) {
visitFunctionIdentifier(ctx.functionIdentifier)
}
}
该类复写了父类的一些方法,这些方法在去访问各个树节点(Context对象)时,实际上是去调用了 Context 类的 accept() 方法。各种 Context 类的 accept 方法大致实现逻辑如下:
- 如果该树节点(Context)不是叶子节点,则让 visitor 去递归访问自己的孩子节点。
- 如果该树节点(Context)是叶子节点(TerminalNodeImpl),则调用 visitor 的 visitTerminal() 方法,该方法默认实现为调用 visitor 自身的 defaultResult() 方法。
AstBuilder 类的 visitXXX() 方法返回对象类型是 LogicalPlan 或者 Expression。该类是Spark SQL 逻辑计划在代码层面的定义类。
三、Spark LogicalPlan(逻辑计划)
1. 整体概述
经过上面的分析,我们知道了 Spark Sql 通过 visitor 遍历语法树节点(各种Context对象),返回多个 LogicalPlan 对象,而这些对象也是保持着树状关系。
从 sql 语句经过 SparkSqlParser 解析生成 UnresolvedLogicalPlan,到最终生成 Optimized LogicalPlan,这个流程主要经过3个阶段,如下图所示:
其中 OptimizedLogicalPlan 传递到下一个阶段用于物理执行计划的生成。
2. LogicalPlan 类结构体系
在介绍 LogicalPlan 之前,有必要介绍一下其父类 QueryPlan。QueryPlan 的主要操作分为6个模块:
- 输入输出:QueryPlan 的输入输出定义了5个方法,用来表示该节点本身的输出以及接收其子节点的输入。
- 基本属性:表示 QueryPlan 节点中的一些基本信息,其中 schema 对应 output 输出属性的 schema 信息。
- 字符串:这部分方法主要用于输出打印 QueryPlan 树型结构信息,其中 schema 信息也会以树状形式展示。
- 规范化
- 表达式操作
- 约束
LogicalPlan 的类继承关系如下图所示:
LogicalPlan 仍然是抽象类,根据子节点数目,绝大部分的 LogicalPlan 可以分为三类,即叶子节点 LeafNode 类型(不存在子节点)、一元节点 UnaryNode 类型(仅包含一个子节点)和二元节点 BinaryNode 类型(包含两个子节点)。类结构关系如下图所示:
LeafNode 类型的 LogicalPlan 节点对应数据表和命令(Command)相关的逻辑,因此这些 LeafNode 子类中有很大一部分属于 datasources 包和 comand 包。与命令相关的有12种情形,包括 database 相关命令、Table 相关命令、View 相关命令、DDL 相关命令、Function 相关命令和 Resource 相关命令等。
Unary 类型的 LogicalPlan 表示该节点为非叶子节点,但是只有1个子节点,用于将得到的子节点的数据做一次转换操作,如:Sort、Filter、Distinct、Sample等
BinaryNode 类型的 LogicalPlan 表示该节点有两个子节点,最常见的就是 Join。
从逻辑上看,对根节点的访问操作会递归访问其子节点(ctx.statement,默认为 StatementDefaultContext 节点)。这样逐步向下递归调用,直到访问某个子节点时能够构造LogicalPlan,然后传递给父节点,因此返回的结果可以转换为 LogicalPlan 类型。
总的来看,生成UnresolvedLogicalPlan 的过程如下图所示(图片转载自知乎):
3. Analyzed LogicalPlan 生成
经过上一个阶段 AstBuilder 的处理,已经得到了 Unresolved LogicalPlan。再回头来看 SparkSession.sql 方法代码:
class SparkSession {
def sql(sqlText: String): DataFrame = {
// 调用下面 Dataset 类的方法
Dataset.ofRows(self, sessionState.sqlParser.parsePlan(sqlText))
}
}
private[sql] object Dataset {
def ofRows(sparkSession: SparkSession, logicalPlan: LogicalPlan): DataFrame = {
// 调用下面 SessionState 类的方法生成 QueryExecution 对象
val qe = sparkSession.sessionState.executePlan(logicalPlan)
qe.assertAnalyzed()
new Dataset[Row](sparkSession, qe, RowEncoder(qe.analyzed.schema))
}
}
private[sql] class SessionState {
def executePlan(plan: LogicalPlan): QueryExecution = createQueryExecution(plan)
}
从上面的代码中我们可以看出,sql 方法先得到 LogicalPlan,然后调用 Dataset.ofRows() 方法生成 Dataset 对象,而 ofRows 方法又先调用 SessionState 类的 executePlan 方法生成 QueryExecution 对象,然后调用其 assertAnalyzed 方法去解析 Unresolved LogicalPlan
QueryExecution 类是一个比较重要的类,其后续的由 LogicalPlan 生成 PhysicalPlan 的方法都是由该类提供的。
再来看 QueryExecution.assertAnalyzed 方法:
class QueryExecution(val sparkSession: SparkSession, val logical: LogicalPlan) {
def assertAnalyzed(): Unit = analyzed
lazy val analyzed: LogicalPlan = {
SparkSession.setActiveSession(sparkSession)
sparkSession.sessionState.analyzer.executeAndCheck(logical)
}
}
class Analyzer(catalog: SessionCatalog, conf: SQLConf, maxIterations: Int)
extends RuleExecutor[LogicalPlan] with CheckAnalysis {
def executeAndCheck(plan: LogicalPlan): LogicalPlan = {
val analyzed = execute(plan)
try {
checkAnalysis(analyzed)
EliminateBarriers(analyzed)
} catch {
case e: AnalysisException =>
val ae = new AnalysisException(e.message, e.line, e.startPosition, Option(analyzed))
ae.setStackTrace(e.getStackTrace)
throw ae
}
}
}
从代码中我们可以看出执行 LogicalPlan 的方法入口是 Analyzer.execute(plan)。
3.1 Catalog 体系
在 Spark SQL 系统中,Catalog 主要用于各种函数资源信息和元数据信息(数据库、数据表、数据视图等)的统一管理。Catatlog 体系实现以 SessionCatalog 为主体,通过 SparkSession 提供给外部调用,整体类关系如下图所示:
3.2 Rule 体系
在 Unresolved LogicalPlan 逻辑算子树的操作(如绑定、解析、优化等)中,主要方法都是基于规则(Rule)的,通过 Scala 语言模式匹配机制进行树结构的转换或节点改写。Rule 是一个抽象类,子类需要复写 apply(plan: TreeType) 方法来制定特殊的处理逻辑。
有了各种规则后,还需要驱动程序来调用这些规则,在 Catalyast 中这个功能由 RuleExecutor 提供。RuleExecutor 内部提供了一个 Seq[Batch],里面定义的是该 RuleExecutor 的处理步骤。每个Batch 代表一个规则,配置一个策略(Strategy),
该策略说明了迭代次数(一次还是多次)。
RuleExecutor 类的关键代码如下:
abstract class RuleExecutor[TreeType <: TreeNode[_]] extends Logging {
abstract class Strategy { def maxIterations: Int }
case object Once extends Strategy { val maxIterations = 1 }
case class FixedPoint(maxIterations: Int) extends Strategy
protected case class Batch(name: String, strategy: Strategy, rules: Rule[TreeType]*)
protected def batches: Seq[Batch]
protected def isPlanIntegral(plan: TreeType): Boolean = true
def execute(plan: TreeType): TreeType = {
// ... 代码太长省略
}
}
Analyzer 继承 RuleExecutor 类,为父类的 batches 变量进行了赋值,添加了多个 batch 及 rule。
class Analyzer(
catalog: SessionCatalog,
conf: SQLConf,
maxIterations: Int)
extends RuleExecutor[LogicalPlan] with CheckAnalysis {
lazy val batches: Seq[Batch] = Seq(
Batch("Hints", fixedPoint,
new ResolveHints.ResolveBroadcastHints(conf),
ResolveHints.RemoveAllHints),
Batch("Simple Sanity Check", Once,
LookupFunctions),
Batch("Substitution", fixedPoint,
CTESubstitution,
WindowsSubstitution,
EliminateUnions,
new SubstituteUnresolvedOrdinals(conf)),
Batch("Resolution", fixedPoint,
ResolveTableValuedFunctions ::
ResolveRelations ::
ResolveReferences ::
ResolveCreateNamedStruct ::
ResolveDeserializer ::
ResolveNewInstance ::
ResolveUpCast ::
ResolveGroupingAnalytics ::
ResolvePivot ::
ResolveOrdinalInOrderByAndGroupBy ::
ResolveAggAliasInGroupBy ::
ResolveMissingReferences ::
ExtractGenerator ::
ResolveGenerate ::
ResolveFunctions ::
ResolveAliases ::
ResolveSubquery ::
ResolveSubqueryColumnAliases ::
ResolveWindowOrder ::
ResolveWindowFrame ::
ResolveNaturalAndUsingJoin ::
ExtractWindowExpressions ::
GlobalAggregates ::
ResolveAggregateFunctions ::
TimeWindowing ::
ResolveInlineTables(conf) ::
ResolveTimeZone(conf) ::
ResolvedUuidExpressions ::
TypeCoercion.typeCoercionRules(conf) ++
extendedResolutionRules : _*),
Batch("Post-Hoc Resolution", Once, postHocResolutionRules: _*),
Batch("View", Once,
AliasViewChild(conf)),
Batch("Nondeterministic", Once,
PullOutNondeterministic),
Batch("UDF", Once,
HandleNullInputsForUDF),
Batch("FixNullability", Once,
FixNullability),
Batch("Subquery", Once,
UpdateOuterReferences),
Batch("Cleanup", fixedPoint,
CleanupAliases)
)
}
Analyzer 类默认定义了 11 个 Batch,其中最重要的是 Resolution(解析)类别,它下面的 ResolveRelations 规则会去匹配 UnresolvedRelation 节点(由 from 语句得到),从数据库中查找该表的元信息,如果该表不存在,则会抛出异常。
3.3 AnalyzedLogicalPlan 生成过程
前面提到执行逻辑计划解析的入口方法是 Analyzer.execute(),该方法实际上是去调用父类 RuleExecutor 的 execute 方法,方法代码如下:
def execute(plan: TreeType): TreeType = {
var curPlan = plan
val queryExecutionMetrics = RuleExecutor.queryExecutionMeter
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
if (!result.fastEquals(plan)) {
queryExecutionMetrics.incNumEffectiveExecution(rule.ruleName)
queryExecutionMetrics.incTimeEffectiveExecutionBy(rule.ruleName, runTime)
logTrace(
s"""
|=== Applying Rule ${rule.ruleName} ===
|${sideBySide(plan.treeString, result.treeString).mkString("\n")}
""".stripMargin)
}
queryExecutionMetrics.incExecutionTimeBy(rule.ruleName, runTime)
queryExecutionMetrics.incNumExecution(rule.ruleName)
// Run the structural integrity checker against the plan after each rule.
if (!isPlanIntegral(result)) {
val message = s"After applying rule ${rule.ruleName} in batch ${batch.name}, " +
"the structural integrity of the plan is broken."
throw new TreeNodeException(result, message, null)
}
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) {
val message = s"Max iterations (${iteration - 1}) reached for batch ${batch.name}"
if (Utils.isTesting) {
throw new TreeNodeException(curPlan, message, null)
} else {
logWarning(message)
}
}
continue = false
}
if (curPlan.fastEquals(lastPlan)) {
continue = false
}
lastPlan = curPlan
}
}
curPlan
}
RuleExecutor 的 execute() 方法会按照 batches 顺序和 batch 内的 Rules 顺序,对传入的节点进行迭代处理,处理逻辑由具体 Rule 子类的 apply 方法实现。以上面的 ResolveRelations 节点为例,它实现其父类方法代码如下:
object ResolveRelations extends Rule[LogicalPlan] {
def apply(plan: LogicalPlan): LogicalPlan = plan.transformUp {
case i @ InsertIntoTable(u: UnresolvedRelation, parts, child, _, _) if child.resolved =>
EliminateSubqueryAliases(lookupTableFromCatalog(u)) match {
case v: View =>
u.failAnalysis(s"Inserting into a view is not allowed. View: ${v.desc.identifier}.")
case other => i.copy(table = other)
}
case u: UnresolvedRelation => resolveRelation(u)
}
}
上面代码分析了总体 analyzed 流程,可以概况起来为:SparkSqlParser 类进行语法解析生成语法树,得到的结果是将未解析的 LogicalPlan,然后转交给 QueryExection 类去处理。QueryExecution 类是一个比较重要的类,它定义了 analyzed,optimizedPlan, sparkPlan 等一系列方法,分别对应逻辑树解析、优化、生成物理算子树等一系列过程,其中 analyzed 方法便是解析未经解析的 LogicalPlan 得到解析后的 LogicalPlan。
QueryExection 的 analyzed 方法实际上是去调用 Analyzer 类的方法做实际解析。Analyzer 继承自 RuleExecutor 类,重写了父类的 batches 方法,添加了一系列的 rule 到集合中。然后调用其父类的 execute 方法,该方法遍历 rule 列表,根据模式匹配 LogicalPlan 节点,生成解析后的 LogicalPlan 节点。
并且要注意的是,我们调用 sparkSession.sql("xxx") 方法,到这理流程就结束了,可以再回顾一下 sql 方法:
def sql(sqlText: String): DataFrame = {
Dataset.ofRows(self, sessionState.sqlParser.parsePlan(sqlText))
}
private[sql] object Dataset {
def ofRows(sparkSession: SparkSession, logicalPlan: LogicalPlan): DataFrame = {
val qe = sparkSession.sessionState.executePlan(logicalPlan)
qe.assertAnalyzed()
new Dataset[Row](sparkSession, qe, RowEncoder(qe.analyzed.schema))
}
}
从方法中我们可以看出返回的 Dataset 对象,其构造方法了传入 SparkSession、QueryExecution、RowEncoder 这3个类的实例。这也是为什么我们使用了 sql 方法不能得到查询结果的原因,这里面包含了 spark 惰性计算的思想,后面我们必须再调用例如 show 这样的方法,才会触发后续流程的计算过程。