learn-tech/专栏/Spark性能调优实战/22Catalyst物理计划:你的SQL语句是怎么被优化的(下)?.md
2024-10-16 06:37:41 +08:00

186 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
22 Catalyst物理计划你的SQL语句是怎么被优化的
你好,我是吴磊。
上一讲我们说了Catalyst优化器的逻辑优化过程包含两个环节逻辑计划解析和逻辑计划优化。逻辑优化的最终目的就是要把Unresolved Logical Plan从次优的Analyzed Logical Plan最终变身为执行高效的Optimized Logical Plan。
但是逻辑优化的每一步仅仅是从逻辑上表明Spark SQL需要“做什么”并没有从执行层面说明具体该“怎么做”。因此为了把逻辑计划交付执行Catalyst还需要把Optimized Logical Plan转换为物理计划。物理计划比逻辑计划更具体它明确交代了Spark SQL的每一步具体该怎么执行。
今天这一讲我们继续追随小Q的脚步看看它经过Catalyst的物理优化阶段之后还会发生哪些变化。
优化Spark Plan
物理阶段的优化是从逻辑优化阶段输出的Optimized Logical Plan开始的因此我们先来回顾一下小Q的原始查询和Optimized Logical Plan。
val userFile: String = _
val usersDf = spark.read.parquet(userFile)
usersDf.printSchema
/**
root
|-- userId: integer (nullable = true)
|-- name: string (nullable = true)
|-- age: integer (nullable = true)
|-- gender: string (nullable = true)
|-- email: string (nullable = true)
*/
val users = usersDf
.select("name", "age", "userId")
.filter($"age" < 30)
.filter($"gender".isin("M"))
val txFile: String = _
val txDf = spark.read.parquet(txFile)
txDf.printSchema
/**
root
|-- txId: integer (nullable = true)
|-- userId: integer (nullable = true)
|-- price: float (nullable = true)
|-- volume: integer (nullable = true)
*/
val result = txDF.select("price", "volume", "userId")
.join(users, Seq("userId"), "inner")
.groupBy(col("name"), col("age")).agg(sum(col("price") * col("volume")).alias("revenue"))
result.write.parquet("_")
两表关联的查询语句经过转换之后得到的Optimized Logical Plan如下图所示注意在逻辑计划的根节点出现了Join Inner字样Catalyst优化器明确了这一步需要做内关联但是怎么做内关联使用哪种Join策略来进行关联Catalyst并没有交代清楚因此逻辑计划本身不具备可操作性
为了让查询计划Query Plan变得可操作可执行Catalyst的物理优化阶段Physical Planning可以分为两个环节优化Spark Plan和生成Physical Plan
在优化Spark Plan的过程中Catalyst基于既定的优化策略Strategies把逻辑计划中的关系操作符一一映射成物理操作符生成Spark Plan
在生成Physical Plan过程中Catalyst再基于事先定义的Preparation Rules对Spark Plan做进一步的完善生成可执行的Physical Plan
那么问题来了在优化Spark Plan的过程中Catalyst都有哪些既定的优化策略呢从数量上来说Catalyst有14类优化策略其中有6类和流计算有关剩下的8类适用于所有的计算场景如批处理数据分析机器学习和图计算当然也包括流计算因此我们只需了解这8类优化策略
所有优化策略在转换方式上都大同小异都是使用基于模式匹配的偏函数Partial Functions把逻辑计划中的操作符平行映射为Spark Plan中的物理算子比如BasicOperators策略直接把ProjectFilterSort等逻辑操作符平行地映射为物理操作符其他策略的优化过程也类似因此在优化Spark Plan这一环节咱们只要抓住一个典型策略掌握它的转换过程即可
那我们该抓谁做典型我觉得这个典型至少要满足两个标准它要在我们的应用场景中非常普遍它的取舍对于执行性能的影响最为关键以这两个标准去遴选上面的8类策略我们分分钟就能锁定JoinSelection接下来我们就以JoinSelection为例详细讲解这一环节的优化过程
如果用一句话来概括JoinSelection的优化过程就是结合多方面的信息来决定在物理优化阶段采用哪种Join策略那么问题来了Catalyst都有哪些Join策略
Catalyst都有哪些Join策略
结合Joins的实现机制和数据的分发方式Catalyst在运行时总共支持5种Join策略分别是Broadcast Hash JoinBHJShuffle Sort Merge JoinSMJShuffle Hash JoinSHJBroadcast Nested Loop JoinBNLJ和Shuffle Cartesian Product JoinCPJ
通过上表中5种Join策略的含义我们知道它们是来自2种数据分发方式广播和Shuffle与3种Join实现机制Hash JoinsSort Merge Joins和Nested Loop Joins的排列组合那么在JoinSelection的优化过程中Catalyst会基于什么逻辑优先选择哪种Join策略呢
JoinSelection如何决定选择哪一种Join策略
逻辑其实很简单Catalyst总会尝试优先选择执行效率最高的策略具体来说在选择join策略的时候JoinSelection会先判断当前查询是否满足BHJ所要求的先决条件如果满足就立即选中BHJ如果不满足就继续判断当前查询是否满足SMJ的先决条件以此类推直到最终选无可选用CPJ来兜底
那么问题来了这5种Join策略都需要满足哪些先决条件呢换句话说JoinSelection做决策时都要依赖哪些信息呢
总的来说这些信息分为两大类第一类是条件型信息用来判决5大Join策略的先决条件第二类是指令型信息也就是开发者提供的Join Hints
我们先来说条件型信息它包含两种第一种是Join类型也就是是否等值连接形式等这种信息的来源是查询语句本身第二种是内表尺寸这些信息的来源就比较广泛了可以是Hive表之上的ANALYZE TABLE语句也可以是Spark对于ParquetORCCSV等源文件的尺寸预估甚至是来自AQE的动态统计信息
5大Join策略对于这些信息的要求我都整理到了下面的表格里你可以看一看
指令型信息也就是Join Hints它的种类非常丰富它允许我们把个人意志凌驾于Spark SQL之上比如说如果我们对小Q的查询语句做了如下的调整JoinSelection在做Join策略选择的时候就会优先尊重我们的意愿跳过SMJ去选择排序更低的SHJ具体的代码示例如下
val result = txDF.select("price", "volume", "userId")
.join(users.hint("shuffle_hash"), Seq("userId"), "inner")
.groupBy(col("name"), col("age")).agg(sum(col("price") *
col("volume")).alias("revenue"))
熟悉了JoinSelection选择Join策略的逻辑之后我们再来看小Q是怎么选择的小Q是典型的星型查询也就是事实表与维度表之间的数据关联其中维表还带过滤条件在决定采用哪种Join策略的时候JoinSelection优先尝试判断小Q是否满足BHJ的先决条件
显然小Q是等值的Inner Join因此表格中BHJ那一行的前两个条件小Q都满足但是内表users尺寸较大超出了广播阈值的默认值10MB不满足BHJ的第三个条件因此JoinSelection不得不忍痛割爱放弃BHJ策略只好退而求其次沿着表格继续向下尝试判断小Q是否满足SMJ的先决条件
SMJ的先决条件很宽松查询语句只要是等值Join就可以小Q自然是满足这个条件的因此JoinSelection最终给小Q选定的Join策略就是SMJ下图是小Q优化过后的Spark Plan从中我们可以看到查询计划的根节点正是SMJ
现在我们知道了Catalyst都有哪些Join策略JoinSelection如何对不同的Join策略做选择小Q也从Optimized Logical Plan摇身一变转换成了Spark Plan也明确了在运行时采用SMJ来做关联计算不过即使小Q在Spark Plan中已经明确了每一步该怎么做但是Spark还是做不到把这样的查询计划转化成可执行的分布式任务这又是为什么呢
生成Physical Plan
原来Shuffle Sort Merge Join的计算需要两个先决条件Shuffle和排序而Spark Plan中并没有明确指定以哪个字段为基准进行Shuffle以及按照哪个字段去做排序
因此Catalyst需要对Spark Plan做进一步的转换生成可操作可执行的Physical Plan具体怎么做呢我们结合Catalyst物理优化阶段的流程图来详细讲讲
从上图中我们可以看到从Spark Plan到Physical Plan的转换需要几组叫做Preparation Rules的规则这些规则坚守最后一班岗负责生成Physical Plan那么这些规则都是什么它们都做了哪些事情呢我们一起来看一下
Preparation Rules有6组规则这些规则作用到Spark Plan之上就会生成Physical Plan而Physical Plan最终会由Tungsten转化为用于计算RDD的分布式任务
小Q的查询语句很典型也很简单并不涉及子查询更不存在Python UDF因此在小Q的例子中我们并不会用到子查询数据复用或是Python UDF之类的规则只有EnsureRequirements和CollapseCodegenStages这两组规则会用到小Q的Physical Plan转化中
实际上它们也是结构化查询中最常见最常用的两组规则今天我们先来重点说说EnsureRequirements规则的含义和作用至于CollapseCodegenStages规则它实际上就是Tungsten的WSCG功能我们下一讲再详细说
EnsureRequirements规则
EnsureRequirements翻译过来就是确保满足前提条件这是什么意思呢对于执行计划中的每一个操作符节点都有4个属性用来分别描述数据输入和输出的分布状态
EnsureRequirements规则要求子节点的输出数据要满足父节点的输入要求这又怎么理解呢
我们以小Q的Spark Plan树形结构图为例可以看到图中左右两个分支分别表示扫描和处理users表和transactions表在树的最顶端根节点SortMergeJoin有两个Project子节点它们分别用来表示users表和transactions表上的投影数据这两个Project的outputPartitioning属性和outputOrdering属性分别是Unknow和None因此它们输出的数据没有按照任何列进行Shuffle或是排序
但是SortMergeJoin对于输入数据的要求很明确按照userId分成200个分区且排好序而这两个Project子节点的输出显然并没有满足父节点SortMergeJoin的要求这个时候EnsureRequirements规则就要介入了它通过添加必要的操作符如Shuffle和排序来保证SortMergeJoin节点对于输入数据的要求一定要得到满足如下图所示
在两个Project节点之后EnsureRequirements规则分别添加了Exchange和Sort节点其中Exchange节点代表Shuffle操作用来满足SortMergeJoin对于数据分布的要求Sort表示排序用于满足SortMergeJoin对于数据有序的要求
添加了必需的节点之后小Q的Physical Plan已经相当具体了这个时候Spark可以通过调用Physical Plan的doExecute方法把结构化查询的计算结果转换成RDD[InternalRow]这里的InternalRow就是Tungsten设计的定制化二进制数据结构这个结构我们在内存视角有过详细的讲解你可以翻回去看看通过调用RDD[InternalRow]之上的Action算子Spark就可以触发Physical Plan从头至尾依序执行
最后我们再来看看小Q又发生了哪些变化
首先我们看到EnsureRequirements规则在两个分支的顶端分别添加了Exchange和Sort操作来满足根节点SortMergeJoin的计算需要其次如果你仔细观察的话会发现Physical Plan中多了很多星号*这些星号的后面还带着括号和数字如图中的*3*1这种星号*标记表示的就是WSCG后面的数字代表Stage编号因此括号中数字相同的操作最终都会被捏合成一份手写代码也就是我们下一讲要说的Tungsten的WSCG
至此小Q从一个不考虑执行效率的叛逆少年就成长为了一名执行高效的专业人士Catalyst这位人生导师在其中的作用功不可没
小结
为了把逻辑计划转换为可以交付执行的物理计划Spark SQL物理优化阶段包含两个环节优化Spark Plan和生成Physical Plan
在优化Spark Plan这个环节Catalyst基于既定的策略把逻辑计划平行映射为Spark Plan策略很多我们重点掌握JoinSelection策略就可以它被用来在运行时选择最佳的Join策略JoinSelection按照BHJ > SMJ > SHJ > BNLJ > CPJ的顺序依次判断查询语句是否满足每一种Join策略的先决条件进行“择优录取”。
如果开发者不满足于JoinSelection默认的选择顺序也就是BHJ > SMJ > SHJ > BNLJ > CPJ还可以通过在SQL或是DSL语句中引入Join hints来明确地指定Join策略从而把自己的意愿凌驾于Catalyst之上。不过需要我们注意的是要想让指定的Join策略在运行时生效查询语句也必须要满足其先决条件才行。
在生成Physical Plan这个环节Catalyst基于既定的几组Preparation Rules把优化过后的Spark Plan转换成可以交付执行的物理计划也就是Physical Plan。在这些既定的Preparation Rules当中你需要重点掌握EnsureRequirements规则。
EnsureRequirements用来确保每一个操作符的输入条件都能够得到满足在必要的时候会把必需的操作符强行插入到Physical Plan中。比如对于Shuffle Sort Merge Join来说这个操作符对于子节点的数据分布和顺序都是有明确要求的因此在子节点之上EnsureRequirements会引入新的操作符如Exchange和Sort。
每日一练
3种Join实现方式和2种网络分发模式明明应该有6种Join策略为什么Catalyst没有支持Broadcast Sort Merge Join策略
期待在留言区看到你的思考和答案,我们下一讲见!