Spark3 AQE (Adaptive Query Execution) 一文搞懂 新特性

介绍Spark3中Adaptive Query Execution(AQE)的新特性,包括动态合并分区、Join策略调整及数据倾斜优化等内容。

Spark 3 AQE (Adaptive Query Execution)

在这里插入图片描述

在搭建平台的过程中,我们使用 CDH 6.3.2 进行搭建,但 CDH 中阉割掉了 spark-sql 功能,所以我们外挂了 Spark 3,补充 spark-sql 功能,版本为 3.3。在使用的过程中,查看 sql 执行图,发现了一个以前没有发现的 AQE 功能。所以今天我们就来聊一聊关于 AQE 这个 Spark 新特性(Hadoop3 也采用了这一个思路)。

AQE Adaptive Query Execution 原理 代码 讲解


AQE 面对的场景

在我们大型分布式计算框架中,遇到问题最多的就是对是否 shuffle、切块切片、谓词没有下推、数据量倾斜、小文件等等问题。而产生这些问题的根本在于解析 sql 时就已经决定了整个任务的执行规划,但没有考虑到真实的数据场景。因为开发人员也不可能每次都去实地考察每一次运行的 sql 环境,并且做出对应的参数调整。

比如以下一些任务场景:


数据分区不均衡

某类数据白天数据量很大,晚上数据量很小,白天每个文件可能都很大,但晚上的文件很小。如果我们配置了调度运行这个任务,那么可能我们的 spark.sql.shuffle.partition 是固定的,这样就决定了我们 reduce 任务的数量。那么造成白天 reduce 数量少,造成 OOM;而晚上的 reduce 数量又多,造成小文件的产生。


执行计划总是不最优的

一般我们的 CBO 是通过 Hive Metastore 的记录来进行优化策略调整,但这种策略也是静态策略,一旦开始执行就无法进行更改。

比如:一张大表 A join 一张大表 B,但其实表 B 经过过滤后,是可以 broadcast 进行关联的,但还是只能停留使用 sort merge join,造成大量的 shuffle,降低了查询性能。


热点 Key 问题

总有一些 top 流量是远远超过其他的,所以如果我们恰好使用这些热点 Key 来进行一些排序、分区,那么 shuffle 就会使得某一个节点造成数据倾斜。

一般对于这种,我们可能会考虑手动过滤,或者 加盐 处理,但并不是所有的 Key 我们都可以手动处理的。


AQE 是什么

上述的问题,其实都是关于真实运行数据时遇到的问题,而处理这些问题,大多数并不能在提交任务之前进行优化,而是基于运行过程中,产生的数据进行优化。所以, Spark 社区在 DAGScheduler 中,新增了一个 API 在支持提交单个 map 阶段,以及在运行时修改 shuffle 分区数等等,而这些就是 AQE。

在 spark 运行时,每当一个 shuffle map 阶段进行完毕,AQE 就会统计这个阶段的信息,并且基于规则进行动态调整并修正还未执行的任务逻辑计算与物理计划(在条件运行的情况下),使得 spark 程序在接下来的运行过程中得到优化。

所以,和 CBO 不同, AQE 是通过 shuffle map 中间阶段的输出文件信息进行调整。

在运行 spark 程序时,每个 Map Task 都会产出很多以 data 为后缀的数据文件,和以 index 结尾的索引文件。每个 data 的文件大小、空文件数量与占比、每个 reduce task 对应分区大小。A QE 就获取这些来进行相关优化。

与 RBO 也不同,RBO 是根据 sql 表象来进行一些经验主义的优化,如谓词下推,列剪枝等等。

后面会讲到 AQE 也会有谓词下推的优化,但一个是根据 SQL 表象(通过 sql 即可获取到的信息),一个是根据实际数据情况需要(需要部分运算后才能得到的信息)。


AQE 配置

在配置中,开启参数 spark.sql.adaptive.enabled=true,在 spark3.3 实际使用中,已经是默认开启(但 spark3.0 是 false)。


原始 Spark sql 解析以及任务提交流程

下图是描述 spark sql 的内部结构,一般来说物理计划是解析 sql 得到。

包含目录解析,基于规则的常量折叠、谓词和计算下推,转换为多个 spark 算子(一般会有多个,然后选择成本最低的)等基础优化。

在这里插入图片描述

比如这个 SQL 的解析:

select a1, avg(b1)
from A 
join B on A.pk = B.pk
where b1 <= 10000
group by a1
;

获取将 A 表和 B 表的 pk 进行关联,并且 B 表有一个筛选,A 表字段聚合。那么他的 SQL 逻辑计划如:

观察该图应该从叶节点到根节点(倒过来看),只有当下层执行完毕才会执行上层任务。

那么我们还需要将逻辑计划转变为物理计划,比如 join 操作就可能是多种中的一种:

在这里插入图片描述

当物理计划一旦确定,那么在运行时就不能够更改,这里选择 SortMergeJoin 就是可能因为 A 表和 B 表都是很大的表(但可能其实过滤条件将大表已经化小了,如前面提到的场景 2)。物理计划确定后,就会形成 DAG 图,使用 RDD 进行计算,并且有对应的依赖关系(宽窄依赖)。最后会提交给 DAGScheduler 进行分解,然后传递 TaskSet 在 TaskScheduler 负责的物理资源上执行。


AQE 基于任务统计信息进行任务修订

在没有 AQE 时,spark 会在确定了物理执行计划之后,根据每个算子定义生成 RDD 以及对应的 DAG,然后 spark DAGScheduler 通过 shuffle 来划分 RDD Graph,并以此创建每个 stage,以及挨个执行 stage。

在开启 AQE 时,会将逻辑计划拆分为 QueryStage 独立的子图,更早的拆分 Stage。通过单独提交 mapStage(与 Stage 不同),收集它们的 MapOutputStatistics 对象。

在 AQE 的 plan 中,定义了两种类型的 QueryStage:

  1. Shuffle query stages

    ​ 将输出物化为 Shuffle 文件。

  2. Broadcast query stages

    ​ 将输出物化到 Driver 内存中的数组。

所以如果要支持这个新特性,必须依靠上面的 DAGSchedule

<think>我们正在讨论SparkAdaptive Query ExecutionAQE)机制,特别是动态调整DAG执行计划的运作原理。根据引用内容,AQE的核心是在运行时根据统计信息调整执行计划,以优化查询性能。 引用[2]提到:“每当一个shuffle map阶段进行完毕,AQE就会统计这个阶段的信息,并且基于规则进行动态调整并修正还未执行的任务逻辑计算与物理计划”。引用[4]详细说明了Join策略调整和自动分区合并。 运作原理可以分解为以下步骤: 1. **Shuffle Map阶段完成后的统计收集**: 当Shuffle Map阶段完成时,AQE会收集该阶段的统计信息,包括每个分区的数据大小、行数等(引用[2],[4])。 2. **动态调整执行计划**: 基于收集到的统计信息,AQE会应用一系列优化规则来调整后续的执行计划。主要优化包括: - **Join策略调整**:如果发现参与Join的一张表的大小小于广播阈值,则可以将Shuffle Join改为Broadcast Join(引用[4])。 - **自动分区合并**:当检测到某些分区数据量过小(可能由于数据倾斜或过滤导致)时,AQE会将这些小分区合并,减少后续任务数,避免资源浪费(引用[4])。 - **倾斜Join优化**:自动检测倾斜的分区,并将其拆分成更小的子分区,从而平衡负载(引用[3])。 3. **更新执行计划**: 调整后的执行计划会替换掉原来的计划,并且通过事件`SparkListenerSQLAdaptiveExecutionUpdate`更新UI(引用[3])。 4. **继续执行**: 后续的任务将使用优化后的执行计划运行。 整个过程是自适应的,因为每次Shuffle阶段完成后都可能触发调整。 下面用伪代码和流程图说明: **流程图表示AQE的运作流程**: ```mermaid graph TD A[开始执行查询] --> B{是否存在Shuffle Map阶段完成?} B -->|是| C[收集该阶段的统计信息] C --> D[应用优化规则:<br/>1. Join策略调整<br/>2. 合并小分区<br/>3. 处理倾斜Join] D --> E[动态更新后续执行计划] E --> F[更新UI显示新计划] F --> G[继续执行后续阶段] G --> B B -->|否| H[执行结束] ``` **优化规则详细说明**: 1. **Join策略调整**: 假设有一个Join操作,原计划是SortMergeJoin(需要Shuffle)。在Shuffle Map阶段完成后,AQE检测到其中一张表的大小为$S$,而广播阈值为$T$。如果$S < T$,则AQE会将后续的SortMergeJoin改为BroadcastJoin。这样可以避免Shuffle,提高性能。 2. **自动分区合并**: 假设一个Shuffle操作后有200个分区,但统计显示大部分分区的数据量都很小(比如小于设定阈值),而少数分区很大。AQE会将多个小分区合并成一个分区,从而减少后续任务的数量。具体来说,它会将相邻的小分区合并,直到每个合并后的分区大小达到目标(如默认的128MB)。这可以避免启动过多的小任务。 3. **倾斜Join优化**: 在Join操作中,如果某个分区的数据量远远大于其他分区(例如,分区A有100GB,而其他分区平均1GB),则AQE会将该大分区拆分成多个子分区。例如,将分区A根据某个键拆分成A1, A2, ..., An,每个子分区大小与其他分区相当。然后,分别与另一张表的对应分区进行Join。这样就能避免单个任务执行时间过长。 **举例说明**: 原始查询计划: ``` Project | Join (SortMergeJoin) | | | TableScan(t1) | Aggregate | TableScan(t2) ``` 在Aggregate阶段(Shuffle Map阶段)完成后,AQE发现t1表很小,于是将SortMergeJoin改为BroadcastJoin: ``` Project | Join (BroadcastJoin) | | | TableScan(t1) [现在被广播] | Aggregate | TableScan(t2) ``` **注意事项**: - AQE需要满足一定条件才会触发,例如在Spark 3.0及以上版本中默认开启,但需要设置`spark.sql.adaptive.enabled=true`(默认已经是true)。 - AQE的优化是动态的,因此执行计划在运行时可能会改变多次。 §§相关问题§§ 1. AQE在哪些场景下无法发挥作用? 2. 如何监控AQE的动态调整效果? 3. 除了Join策略调整和分区合并,AQE还有哪些优化规则? 4. AQE如何处理聚合操作的倾斜? 5. 为什么AQE需要Shuffle Map阶段完成才能进行调整? </think>### Adaptive Query ExecutionAQE)运作原理详解 SparkAQE机制通过**运行时动态优化执行计划**解决传统静态优化的局限,其核心运作流程如下: #### 1. **Shuffle阶段统计信息采集** 当Shuffle Map阶段完成时,AQE自动收集以下统计信息: - **分区数据量**:各分区大小(如分区A:128MB,分区B:2GB) - **数据分布特征**:空文件比例、Key分布直方图 - **Join中间结果**:参与Join的表大小估算值 *触发条件*:Shuffle写操作完成时自动激活[^2][^4] #### 2. **动态优化规则应用 AQE基于统计信息实时应用优化规则: ##### (a) Join策略动态切换 - **原理**:检测小表满足$S < broadcast\_threshold$时 $$\text{SortMergeJoin} \xrightarrow{\text{动态转换}} \text{BroadcastJoin}$$ - **示例**: 原始10GB表Join 90MB表(静态计划:SortMergeJoin) → AQE检测小表<100MB → 切换为BroadcastJoin[^4] ##### (b) 分区自动合并(Coalescing Partitions) - **倾斜处理**:合并过小分区(<128MB默认值) $$P_{\text{new}} = \bigcup_{i=1}^{k} P_i \quad \text{当} \sum_{i=1}^{k} |P_i| < \theta$$ - **效果**:减少2000个小分区 → 合并为200个合理分区[^4] ##### (c) 倾斜Join优化 - **检测倾斜**:识别数据量$>\mu + 3\sigma$的分区($\mu$为平均大小) - **分裂处理**:将倾斜分区拆分为$N$个子分区 $$P_{\text{skew}} \Rightarrow \{P_{\text{sub1}}, P_{\text{sub2}}, ..., P_{\text{subN}}\}$$ 确保$\max(|P_{\text{sub}}|) \approx \mu$[^3] #### 3. **执行计划更新 - **物理计划重写**:基于新统计信息重建DAG - **拓扑更新**:通过`SparkListenerSQLAdaptiveExecutionUpdate`事件刷新UI显示[^3] - **增量优化**:每个Shuffle阶段后均可触发新优化 #### 4. 运作流程图解 ```mermaid graph LR A[原始执行计划] --> B{Shuffle Map阶段完成} B -->|采集统计信息| C[分区大小/数据分布/JOIN表尺寸] C --> D{优化规则检查} D -->|小表检测| E[切换BroadcastJoin] D -->|小分区检测| F[合并分区] D -->|倾斜分区检测| G[分裂倾斜分区] E & F & G --> H[更新物理计划和DAG] H --> I[刷新UI显示新计划] --> J[继续执行后续阶段] ``` #### 关键优势 vs 静态优化 | 维度 | 静态优化 | AQE动态优化 | |------|----------|-------------| | 决策依据 | 表元数据估算 | 运行时真实数据统计 | | 优化时机 | 查询编译时 | Shuffle阶段完成后 | | 倾斜处理 | 需手动 hint | 自动检测+修复 | | Join策略 | 一次性选择 | 运行时动态切换 | | 分区控制 | 固定分区数 | 自适应合并/分裂 |
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值