You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

149 lines
17 KiB
Markdown

2 years ago
# 24 | Spark 3.0AQE的3个特性怎么才能用好
你好,我是吴磊。
目前距离Spark 3.0版本的发布已经将近一年的时间了这次版本升级添加了自适应查询执行AQE、动态分区剪裁DPP和扩展的 Join Hints 等新特性。利用好这些新特性可以让我们的性能调优如虎添翼。因此我会用三讲的时间和你聊聊它们。今天我们先来说说AQE。
我发现同学们在使用AQE的时候总会抱怨说“AQE的开关打开了相关的配置项也设了可应用性能还是没有提升。”这往往是因为我们对于AQE的理解不够透彻调优总是照葫芦画瓢所以这一讲我们就先从AQE的设计初衷说起然后说说它的工作原理最后再去探讨怎样才能用好AQE。
## Spark为什么需要AQE
在2.0版本之前Spark SQL仅仅支持启发式、静态的优化过程就像我们在第21、22、23三讲介绍的一样。
启发式的优化又叫RBORule Based Optimization基于规则的优化它往往基于一些规则和策略实现如谓词下推、列剪枝这些规则和策略来源于数据库领域已有的应用经验。也就是说**启发式的优化实际上算是一种经验主义**。
经验主义的弊端就是不分青红皂白、胡子眉毛一把抓对待相似的问题和场景都使用同一类套路。Spark社区正是因为意识到了RBO的局限性因此在2.2版本中推出了CBOCost Based Optimization基于成本的优化
**CBO的特点是“实事求是”**基于数据表的统计信息如表大小、数据列分布来选择优化策略。CBO支持的统计信息很丰富比如数据表的行数、每列的基数Cardinality、空值数、最大值、最小值和直方图等等。因为有统计数据做支持所以CBO选择的优化策略往往优于RBO选择的优化规则。
但是,**CBO也面临三个方面的窘境“窄、慢、静”**。窄指的是适用面太窄CBO仅支持注册到Hive Metastore的数据表但在大量的应用场景中数据源往往是存储在分布式文件系统的各类文件如Parquet、ORC、CSV等等。
慢指的是统计信息的搜集效率比较低。对于注册到Hive Metastore的数据表开发者需要调用ANALYZE TABLE COMPUTE STATISTICS语句收集统计信息而各类信息的收集会消耗大量时间。
静指的是静态优化这一点与RBO一样。CBO结合各类统计信息制定执行计划一旦执行计划交付运行CBO的使命就算完成了。换句话说如果在运行时数据分布发生动态变化CBO先前制定的执行计划并不会跟着调整、适配。
## AQE到底是什么
考虑到RBO和CBO的种种限制Spark在3.0版本推出了AQEAdaptive Query Execution自适应查询执行。如果用一句话来概括**AQE是Spark SQL的一种动态优化机制在运行时每当Shuffle Map阶段执行完毕AQE都会结合这个阶段的统计信息基于既定的规则动态地调整、修正尚未执行的逻辑计划和物理计划来完成对原始查询语句的运行时优化**。
从定义中,我们不难发现,**AQE优化机制触发的时机是Shuffle Map阶段执行完毕。也就是说AQE优化的频次与执行计划中Shuffle的次数一致**。反过来说如果你的查询语句不会引入Shuffle操作那么Spark SQL是不会触发AQE的。对于这样的查询无论你怎么调整AQE相关的配置项AQE也都爱莫能助。
对于AQE的定义我相信你还有很多问题比如AQE依赖的统计信息具体是什么既定的规则和策略具体指什么接下来我们一一来解答。
**首先AQE赖以优化的统计信息与CBO不同这些统计信息并不是关于某张表或是哪个列而是Shuffle Map阶段输出的中间文件。**学习过Shuffle的工作原理之后我们知道每个Map Task都会输出以data为后缀的数据文件还有以index为结尾的索引文件这些文件统称为中间文件。每个data文件的大小、空文件数量与占比、每个Reduce Task对应的分区大小所有这些基于中间文件的统计值构成了AQE进行优化的信息来源。
**其次结合Spark SQL端到端优化流程图我们可以看到AQE从运行时获取统计信息在条件允许的情况下优化决策会分别作用到逻辑计划和物理计划。**
![](https://static001.geekbang.org/resource/image/f3/72/f3ffb5fc43ae3c9bca44c1f4f8b7e872.jpg?wh=6539*1200 "AQE在Spark SQL中的位置与作用")
**AQE既定的规则和策略主要有4个分为1个逻辑优化规则和3个物理优化策略。**我把这些规则与策略和相应的AQE特性以及每个特性仰仗的统计信息都汇总到了如下的表格中你可以看一看。
![](https://static001.geekbang.org/resource/image/1c/d5/1cfef782e6dfecce3c9252c6181388d5.jpeg?wh=1806*770)
## 如何用好AQE
那么AQE是如何根据Map阶段的统计信息以及这4个规则与策略来动态地调整和修正尚未执行的逻辑计划和物理计划的呢这就要提到AQE的三大特性也就是Join策略调整、自动分区合并以及自动倾斜处理我们需要借助它们去分析AQE动态优化的过程。它们的基本概念我们在[第9讲](https://time.geekbang.org/column/article/357342)说过,这里我再带你简单回顾一下。
* Join策略调整如果某张表在过滤之后尺寸小于广播变量阈值这张表参与的数据关联就会从Shuffle Sort Merge Join降级Demote为执行效率更高的Broadcast Hash Join。
* 自动分区合并在Shuffle过后Reduce Task数据分布参差不齐AQE将自动合并过小的数据分区。
* 自动倾斜处理结合配置项AQE自动拆分Reduce阶段过大的数据分区降低单个Reduce Task的工作负载。
接下来我们就一起来分析这3个特性的动态优化过程。
### Join策略调整
我们先来说说Join策略调整这个特性涉及了一个逻辑规则和一个物理策略它们分别是DemoteBroadcastHashJoin和OptimizeLocalShuffleReader。
**DemoteBroadcastHashJoin规则的作用是把Shuffle Joins降级为Broadcast Joins。需要注意的是这个规则仅适用于Shuffle Sort Merge Join这种关联机制其他机制如Shuffle Hash Join、Shuffle Nested Loop Join都不支持。**对于参与Join的两张表来说在它们分别完成Shuffle Map阶段的计算之后DemoteBroadcastHashJoin会判断中间文件是否满足如下条件
* 中间文件尺寸总和小于广播阈值spark.sql.autoBroadcastJoinThreshold
* 空文件占比小于配置项spark.sql.adaptive.nonEmptyPartitionRatioForBroadcastJoin
只要有任意一张表的统计信息满足这两个条件Shuffle Sort Merge Join就会降级为Broadcast Hash Join。说到这儿你可能会问“既然DemoteBroadcastHashJoin逻辑规则可以把Sort Merge Join转换为Broadcast Join那同样用来调整Join策略的OptimizeLocalShuffleReader规则又是干什么用的呢看上去有些多余啊
不知道你注意到没有,我一直强调,**AQE依赖的统计信息来自于Shuffle Map阶段生成的中间文件**。这意味什么呢这就意味着AQE在开始优化之前Shuffle操作已经执行过半了
我们来举个例子现在有两张表事实表Order和维度表User它们的查询语句和初始的执行计划如下。
```
//订单表与用户表关联
select sum(order.price * order.volume), user.id
from order inner join user
on order.userId = user.id
where user.type = Head Users
group by user.id
```
由于两张表大都到超过了广播阈值因此Spark SQL在最初的执行计划中选择了Sort Merge Join。AQE需要同时结合两个分支中的ShuffleExchange输出才能判断是否可以降级为Broadcast Join以及用哪张表降级。这就意味着不论大表还是小表都要完成Shuffle Map阶段的计算并且把中间文件落盘AQE才能做出决策。
![](https://static001.geekbang.org/resource/image/c3/b3/c3d611282c56687342d3ea459242bdb3.jpg?wh=1659*1779 "Sort Merge Join执行计划左侧是事实表的执行分支右侧是维度表的执行分支")
你可能会说“根本不需要大表做Shuffle呀AQE只需要去判断小表Shuffle的中间文件就好啦”。可问题是AQE可分不清哪张是大表、哪张是小表。在Shuffle Map阶段结束之前数据表的尺寸大小对于AQE来说是“透明的”。因此AQE必须等待两张表都完成Shuffle Map的计算然后统计中间文件才能判断降级条件是否成立以及用哪张表做广播变量。
在常规的Shuffle计算流程中Reduce阶段的计算需要跨节点访问中间文件拉取数据分片。如果遵循常规步骤即便AQE在运行时把Shuffle Sort Merge Join降级为Broadcast Join大表的中间文件还是需要通过网络进行分发。这个时候AQE的动态Join策略调整也就失去了实用价值。原因很简单负载最重的大表Shuffle计算已经完成再去决定切换到Broadcast Join已经没有任何意义。
在这样的背景下OptimizeLocalShuffleReader物理策略就非常重要了。既然大表已经完成Shuffle Map阶段的计算这些计算可不能白白浪费掉。**采取OptimizeLocalShuffleReader策略可以省去Shuffle常规步骤中的网络分发Reduce Task可以就地读取本地节点Local的中间文件完成与广播小表的关联操作。**
不过,需要我们特别注意的是,**OptimizeLocalShuffleReader物理策略的生效与否由一个配置项决定**。这个配置项是spark.sql.adaptive.localShuffleReader.enabled尽管它的默认值是True但是你千万不要把它的值改为False。否则就像我们刚才说的AQE的Join策略调整就变成了形同虚设。
说到这里你可能会说“这么看AQE的Join策略调整有些鸡肋啊毕竟Shuffle计算都已经过半Shuffle Map阶段的内存消耗和磁盘I/O是半点没省”确实Shuffle Map阶段的计算开销是半点没省。但是OptimizeLocalShuffleReader策略避免了Reduce阶段数据在网络中的全量分发仅凭这一点大多数的应用都能获益匪浅。因此对于AQE的Join策略调整**我们可以用一个成语来形容:“亡羊补牢、犹未为晚”**。
### 自动分区合并
接下来,我们再来说说自动分区合并。分区合并的原理比较简单,**在Reduce阶段当Reduce Task从全网把数据分片拉回AQE按照分区编号的顺序依次把小于目标尺寸的分区合并在一起**。目标分区尺寸由以下两个参数共同决定。这部分我们在第10讲详细讲过如果不记得你可以翻回去看一看。
* spark.sql.adaptive.advisoryPartitionSizeInBytes由开发者指定分区合并后的推荐尺寸。
* spark.sql.adaptive.coalescePartitions.minPartitionNum分区合并后分区数不能低于该值。
![](https://static001.geekbang.org/resource/image/da/4f/dae9dc8b90c2d5e0cf77180ac056a94f.jpg?wh=4359*1623)
除此之外我们还要注意在Shuffle Map阶段完成之后AQE优化机制被触发CoalesceShufflePartitions策略“无条件”地被添加到新的物理计划中。读取配置项、计算目标分区大小、依序合并相邻分区这些计算逻辑在Tungsten WSCG的作用下融合进“手写代码”于Reduce阶段执行。
### 自动倾斜处理
与自动分区合并相反自动倾斜处理的操作是“拆”。在Reduce阶段当Reduce Task所需处理的分区尺寸大于一定阈值时利用OptimizeSkewedJoin策略AQE会把大分区拆成多个小分区。倾斜分区和拆分粒度由以下这些配置项决定。关于它们的含义与作用我们在[第10讲](https://time.geekbang.org/column/article/357342)说过,你可以再翻回去看一看。
* spark.sql.adaptive.skewJoin.skewedPartitionFactor判定倾斜的膨胀系数
* spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes判定倾斜的最低阈值
* spark.sql.adaptive.advisoryPartitionSizeInBytes以字节为单位定义拆分粒度
自动倾斜处理的拆分操作也是在Reduce阶段执行的。在同一个Executor内部本该由一个Task去处理的大分区被AQE拆成多个小分区并交由多个Task去计算。这样一来Task之间的计算负载就可以得到平衡。但是这并不能解决不同Executors之间的负载均衡问题。
我们来举个例子假设有个Shuffle操作它的Map阶段有3个分区Reduce阶段有4个分区。4个分区中的两个都是倾斜的大分区而且这两个倾斜的大分区刚好都分发到了Executor 0。通过下图我们能够直观地看到尽管两个大分区被拆分但横向来看整个作业的主要负载还是落在了Executor 0的身上。Executor 0的计算能力依然是整个作业的瓶颈这一点并没有因为分区拆分而得到实质性的缓解。
![](https://static001.geekbang.org/resource/image/f4/72/f4fe3149112466174bdefcc0ee573d72.jpg?wh=2889*1614 "Executors之间的负载平衡还有待优化")
另外在数据关联的场景中对于参与Join的两张表我们暂且把它们记做数据表1和数据表2如果表1存在数据倾斜表2不倾斜那在关联的过程中AQE除了对表1做拆分之外还需要对表2对应的数据分区做复制来保证关联关系不被破坏。
![](https://static001.geekbang.org/resource/image/33/a9/33a112480b1c1bf8b21d26412a7857a9.jpg?wh=4359*1224 "数据关联场景中的自动倾斜处理")
在这样的运行机制下如果两张表都存在数据倾斜怎么办这个时候事情就开始变得逐渐复杂起来了。对于上图中的表1和表2我们假设表1还是拆出来两个分区表2因为倾斜也拆出来两个分区。这个时候为了不破坏逻辑上的关联关系表1、表2拆分出来的分区还要各自复制出一份如下图所示。
![](https://static001.geekbang.org/resource/image/91/0a/91yy4df2cbafeba2142775cd80d3110a.jpg?wh=8800x3266 "两边倾斜")
如果现在问题变得更复杂了左表拆出M个分区右表拆出N各分区那么每张表最终都需要保持M x N份分区数据才能保证关联逻辑的一致性。当M和N逐渐变大时AQE处理数据倾斜所需的计算开销将会面临失控的风险。
**总的来说当应用场景中的数据倾斜比较简单比如虽然有倾斜但数据分布相对均匀或是关联计算中只有一边倾斜我们完全可以依赖AQE的自动倾斜处理机制。但是当我们的场景中数据倾斜变得复杂比如数据中不同Key的分布悬殊或是参与关联的两表都存在大量的倾斜我们就需要衡量AQE的自动化机制与手工处理倾斜之间的利害得失。**关于手工处理倾斜我们留到第28讲再去展开。
## 小结
AQE是Spark SQL的一种动态优化机制它的诞生解决了RBO、CBO这些启发式、静态优化机制的局限性。想要用好AQE我们就要掌握它的特点以及它支持的三种优化特性的工作原理和使用方法。
如果用一句话来概括AQE的定义就是每当Shuffle Map阶段执行完毕它都会结合这个阶段的统计信息根据既定的规则和策略动态地调整、修正尚未执行的逻辑计划和物理计划从而完成对原始查询语句的运行时优化。也因此只有当你的查询语句会引入Shuffle操作的时候Spark SQL才会触发AQE。
AQE支持的三种优化特性分别是Join策略调整、自动分区合并和自动倾斜处理。
关于Join策略调整我们首先要知道DemoteBroadcastHashJoin规则仅仅适用于Shuffle Sort Merge Join这种关联机制对于其他Shuffle Joins类型AQE暂不支持把它们转化为Broadcast Joins。其次为了确保AQE的Join策略调整正常运行我们要确保spark.sql.adaptive.localShuffleReader.enabled配置项始终为开启状态。
关于自动分区合并我们要知道在Shuffle Map阶段完成之后结合分区推荐尺寸与分区数量限制AQE会自动帮我们完成分区合并的计算过程。
关于AQE的自动倾斜处理我们要知道它只能以Task为粒度缓解数据倾斜并不能解决不同Executors之间的负载均衡问题。针对场景较为简单的倾斜问题比如关联计算中只涉及单边倾斜我们完全可以依赖AQE的自动倾斜处理机制。但是当数据倾斜问题变得复杂的时候我们需要衡量AQE的自动化机制与手工处理倾斜之间的利害得失。
## 每日一练
1. 我们知道AQE依赖的统计信息来源于Shuffle Map阶段输出的中间文件。你觉得在运行时AQE还有其他渠道可以获得同样的统计信息吗
2. AQE的自动倾斜处理机制只能以Task为粒度来平衡工作负载如果让你重新实现这个机制你有什么更好的办法能让AQE以Executors为粒度做到负载均衡吗
期待在留言区看到你的思考和答案,我们下一讲见!