本文已参与「新人创作礼」活动,一起开启掘金创作之路。
前言
spark 3.X发布到现在已经有1年多了,很多小伙伴已经尝过鲜了,很多小伙伴准备去尝鲜,最近看了下spark 3.X相关的新特性,发现了3.0当前为止最大的优化还是集中在spark sql上,而这个恰恰也是大家使用场景中经常用到的部分,所以这个优化对于大多数用户来说还是很香的,新版spark sql不仅解决了spark 2.X很多痛点,还在此基础上进行了很多优化,下面我们就先来看看spark 3.X版本中关于spark sql的优化AQE(Adaptive Query Execution),作者认为这个优化可能是3.X对于大多数普通用户来说最有价值的优化点,值得好好了解下。下面我就和大家一起分享下AQE。
正文
说到AQE,QE好理解,查询执行,那么这个Adaptive是怎么体现的呢?这与AQE之前查询执行的优化有关系,下面我们来看看在Spark 2.X时是怎么进行查询计划优化的。
RBO
RBO即Rule Based Optimization,基于规则的优化。2.2之前版本的spark只支持这一种优化方式。这种优化策略基于一些规则和策略实现,如谓词下推、列裁剪,分区裁剪等。这些规则和策略来源于数据库领域已有的应用经验。这种优化策略的好处是即拿即用的经验主义。但经验主义的弊端就是教条主义,无法具体问题具体分析,对待相似的问题和场景都使用同一类套路,忽视相似场景中的差异。
从上面可以看到,RBO的缺点比较大有两项:
- 呆板:由于是基于经验进行优化,无法做到具体问题具体分析,优化效果不稳定且很难进行调整
- 静态:无法做到动态调整,查询在物理执行计划生成的那一刻就已经锁定了,无法在运行中进行调整
CBO
CBO即Cost Based Optimization,基于成本的优化。2.2以后版本的spark为了解决RBO的种种缺点,引入了CBO。CBO是基于各种统计值进行查询优化,包含但不限于数据表的行数、每列的基数(Cardinality)、空值数、最大值、最小值和直方图等等。所以CBO起码解决了RBO无法具体问题具体分析的问题,可以根据相关的参数进行精细的调优并用以支持形成相对更优的执行计划。
虽然CBO比RBO更靠谱更先进,但是CBO仍然有其局限,主要分为下面三个方面:
- 适用场景少:CBO 仅支持注册到 Hive Metastore 的数据表,但在大量的应用场景中,数据源往往是存储在分布式文件系统的各类文件,如 Parquet、ORC、CSV 等等
- 数据收集慢:对于注册到 Hive Metastore 的数据表,开发者需要调用 ANALYZE TABLE COMPUTE STATISTICS 语句收集统计信息,而各类信息的收集会消耗大量时间
- 无法动态配置:此问题和RBO一致,查询在物理执行计划生成的那一刻就已经锁定了,无法在运行中进行调整,哪怕实际数据和预想的有差别或者有抖动
下面通过一张图片来看下2.X版本中RBO与CBO的优化流程:
综上,CBO虽然解决了RBO的具体问题具体分析的问题,但是无法解决动态优化执行计划的问题。于是在这种情况下AQE就出现了,目的是在CBO的基础上解决CBO的三个问题,最理想的是能达到Adaptive的目的。
AQE
AQE的优化流程可以用下面一张图片来说明:
AQE的定义
下面先简单的说下AQE的概念:
AQE 是 Spark SQL 的一种动态优化机制,在运行时,每当 Shuffle Map 阶段执行完毕,AQE 都会结合这个阶段的统计信息,基于既定的规则动态地调整、修正尚未执行的逻辑计划和物理计划,来完成对原始查询语句的运行时优化。
从上面的定义我们可以得到下面的结论:
- AQE优化的执行点是shuffle的触发,如果你的spark sql中没有shuffle,则AQE对查询无任何优化效果。
- shuffle每执行一次,都会根据统计信息进行一次优化,所以AQE在整个任务中会优化N-1次。
- AQE的统计信息是来自于map阶段的shuffle写的结果,不需要借助于外部的数据存储或者元数据。
- AQE在生成优化策略后,会立即应用到尚未执行的逻辑计划和物理计划的优化中,来完成对原始查询语句的运行时优化。
AQE的优化规则
AQE的优化规则主要分为下面三种:
- 动态折叠shuffle过程中的partition
- 动态选择join策略
- 动态优化存在数据倾斜的join
下面我们就分开来详细说明下:
动态折叠shuffle过程中的partition
在我们处理的数据量级非常大时,shuffle通常来说是最影响性能的。shuffle阶段在Map阶段会生成多个partition的数据,在 Reduce 阶段,当 Reduce Task 从全网把数据partition拉回。在此过程中,partition的数量相当的关键。原因如下:
- 如果partition过少,每个partition数据量就会过多,可能无法充分发挥spark的并行优势,如果partition的数据如果不均衡,很容易出现短板效应,查询的最终时间等于最慢的那个partition执行完毕的时间,从而拖慢了整体的查询速度。
- 如果partition过多,每个partition数据量就会很少,这样每个task的生命周期管理的成本就会凸显的很高,并且影响Spark task scheduler,从而拖慢查询。
而 AQE 按照分区编号的顺序,依次把小于目标尺寸的分区合并在一起,使得每个partition的数据量基本上差别不大。其中目标分区尺寸由以下两个参数共同决定:
- spark.sql.adaptive.advisoryPartitionSizeInBytes,由开发者指定分区合并后的推荐尺寸。
- spark.sql.adaptive.coalescePartitions.minPartitionNum,分区合并后,分区数不能低于该值。
下面用两张图来说明下动态折叠的过程: 没有AQE的shuffle过程:
有AQE的shuffle过程:
动态优化数据倾斜
与自动分区合并相反,自动倾斜处理的操作是“拆”。在 Reduce 阶段,当 Reduce Task 所需处理的分区尺寸大于一定阈值时,利用 OptimizeSkewedJoin 策略,AQE 会把大分区拆成多个小分区。倾斜分区和拆分粒度由以下这些配置项决定:
- spark.sql.adaptive.skewJoin.skewedPartitionFactor,判定倾斜的膨胀系数
- spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes,判定倾斜的最低阈值
- spark.sql.adaptive.advisoryPartitionSizeInBytes,以字节为单位,定义拆分粒度
下面用两张图来说明下动态优化数据倾斜的过程: 没有AQE的shuffle过程:
有AQE的shuffle过程:
如图所示,如果不做这个优化,SMJ将会产生4个tasks并且其中A0-B0这个partition的执行时间远大于其他,最后的查询的执行时间和这个最慢的partition执行时间相同。
经过AQE的优化,这个join将会有5个tasks,但每个task执行耗时差不多相同,反而使整个查询带来了更好的性能。
动态选择join策略
在Spark所支持的众多join中,broadcast hash join性能是最好的。因此,如果需要广播的表的预估大小小于了广播限制阈值,那么我们就应该将其设为BHJ。
但是,对于表的大小估计不当会导致决策错误,比如join表有很多的filter(容易把表估大)或者join表有很多其他算子(容易把表估小),而不仅仅是全量扫描一张表。
所以生产环境中很多时候无法很准确的判断join的表的准确大小,所以很难进行静态配置join策略,此时动态配置就变得很关键和必须了。在这种情况下,AQE完成了此种需求。
AQE在动态选择join策略时做了两件事情:
- DemoteBroadcastHashJoin:把 Shuffle Joins 降级为 Broadcast Joins。需要注意的是,这个规则仅适用于 Shuffle Sort Merge Join 这种关联机制,其他机制如 Shuffle Hash Join、Shuffle Nested Loop Join 都不支持
- OptimizeLocalShuffleReader:利用Shuffle Map 阶段的输出结果,省去 Shuffle 常规步骤中的网络分发,Reduce Task 可以就地读取本地节点(Local)的中间文件,完成与广播小表的关联操作
从上面的过程可以看出,其实在join的过程中Shuffle Map 阶段的内存消耗和磁盘 I/O 是半点没省!因为需要触发AQE的优化策略,所以shuffle map是不能省的。但是,shuffle map的结果并没有浪费,OptimizeLocalShuffleReader 策略避免了 Reduce 阶段数据在网络中的全量分发,直接从硬盘上读取shuffle map的结果,仅凭这一点,大多数的应用都能获益匪浅。
最后再放一张图来说明下这个过程:
总结
AQE 是 Spark SQL 的一种动态优化机制,它的诞生解决了 RBO、CBO这些有局限性的查询计划优化所存在的弱点和问题。是spark 3.X中提高性能的一把利器。
更让人振奋的是,如果正确的配置完毕后,这些优化spark会帮我们自动进行,不需要我们操心,真是一大美事,值得我们为之鼓掌!