目录
Spark3的AQE(adaptive query execution)
Spark的五种join
- Broadcasthashjoin:适合一张小表和大表进行Join(常见)
- Shufflehashjoin:适合一张小表和一张大表进行join
- sort merge join:适合两张较大的表之间进行join(常见)
- cartesian join:笛卡尔积
- broadcast neseted loop join:嵌套循环
Broadcast hash Join
因为Join操作是对两个表中key值相同的记录进行连接,在SparkSQL中,对两个表做Join最直接的方式是先根据key分区,再在每个分区中把key值相同的记录拿出来做连接操作。但这样就不可避免地涉及到shuffle,而shuffle在Spark中是比较耗时的操作,我们应该尽可能的设计Spark应用使其避免大量的shuffle。
当维度表和事实表进行Join操作时,为了避免shuffle,我们可以将大小有限的维度表的全部数据分发到每个节点上,供事实表使用。executor存储维度表的全部数据,一定程度上牺牲了空间,换取shuffle操作大量的耗时,这在SparkSQL中称作Broadcast Join,也称之为Map端JOIN,如下图所示:
Table B是较小的表,黑色的曲线表示将其广播到每个executor节点上,Table A的每个partition会通过block manager取到Table A的数据。根据每条记录的Join Key取到Table B中相对应的记录,根据Join Type进行操作。
Broadcast Join的条件
- 被广播的表需要小于spark.sql.autoBroadcastJoinThreshold所配置的值,默认是10M (或者加了broadcast join的hint)
- 基表不能被广播,比如left outer join时,只能广播右表
缺点: 这个方案只能用于广播较小的表,否则数据的冗余传输就远大于shuffle的开销。
broadcast hash join可以分为两步
- broadcast阶段:将小表广播分发到大表所在的所有主机。广播算法可以有很多,最简单的是先发给driver,driver再统一分发给所有executor。
- hash join阶段:在每个executor上执行单机版hash join,小表映射,大表试探;实现分布式join操作。
Sort Merge Join
当两个表都非常大时,SparkSQL采用了一种全新的方案来对表进行Join,即Sort Merge Join。这种实现方式不用将一侧数据全部加载后再进行hash join,但需要在join前将数据排序
- shuffle阶段:将两张大表根据join key进行重新分区,两张表数据会分布到整个集群,以便分布式并行处理。
- sort阶段:对单个分区节点的两表数据,分别进行排序。
- merge阶段:对排好序的两张分区表数据执行join操作。join操作很简单,分别遍历两个有序序列,碰到相同join key就merge输出,否则取更小一边。
Cartesian Join
Cartesian Join机制专门用来实现cross join,结果的分区数等于输入数据集的分区数之积,结果中每一个分区的数据对应一个输入数据集的一个分区和另外一个输入数据集的一个分区。
如果 Spark 中两张参与 Join 的表没指定join key(ON 条件)那么会产生 Cartesian product join,这个 Join 得到的结果其实就是两张行数的乘积。
比如:
1 | SELECT COUNT(1) FROM fin_dw_cp.dwt_appl_no_credit_cert_list_pdi a inner join fin_dw_cp.dwt_appl_no_credit_cert_list_pdi b |
Broadcast Nested Loop Join
Broadcast Nested Join将一个输入数据集广播到每个executor上,然后在各个executor上,另一个数据集的分区会和第一个数据集使用嵌套循环的方式进行Join输出结果。
Broadcast Nested Join需要广播数据集和嵌套循环,计算效率极低,对内存的需求也极大,因为不论数据集大小,都会有一个数据集被广播到所有executor上。
该方式是在没有合适的JOIN机制可供选择时,最终会选择该种join策略。
五种join优先级
Broadcast Hash Join > Sort Merge Join > Shuffle Hash Join > cartesian Join > Broadcast Nested Loop Join。
Spark2遇到的问题
问题一:并行度问题
在日常的 Spark SQL 开发中,我们通过设置 spark.sql.shuffle.partitions 参数来调整 partition 数量,默认值是200。即 Shuffle partition 数量需要手动调整才可以获得相对理想的性能。
虽然我们可以设置 shuffle partition 数量,但是无法给出一个对所有任务来说都是最优的值,因为每个 task 处理的的数据量以及 shuffle 策略也可能不同。
Shuffle partition 太大或太小都会带来问题:
1. partition 数量太大
可能会需要处理大量小的 task,导致增加 task 调度开销以及资源调度开销。另外,如果该 Stage 最后要输出存储,造成很多小的 IO 操作,还会造成在 HDFS 上存储大量的小文件。
2. partition 数量太小
可能会导致每个 task 处理大量的数据,处理效率低下,无法有效利用集群资源的并行处理能力,甚至导致 OOM 的问题。
目前 shuffle partition 数量无法根据每个任务动态调整,只能针对不同的任务进行多次的优化调整,才能得到较为合理的值,但是往往作业的数据量是逐日累增的,所以之前优化的值可能不再适合后续的作业。
因此理想情况下,为了获取最佳的性能,Spark 能够实现在作业执行过程中根据数据量大小动态设置合适的 shuffle partition 数量。
总结一下并行度问题带来的挑战:
单一的 partition 配置不可能适合所有的 partition 以获得最佳性能
问题二:join策略选择
我们都知道,shuffle是一个很耗性能的操作。通过避免不必要的shuffle也能带上一定的性能提升。最常见的做法就是在大小表做Join时,将小表提前加载进内存,之后直接使用内存的数据进行join,这样就少了shuffle带来的性能损耗了。这种做法就是MapJoin,在Spark中,也叫做BroadcastHashJoin。原理是将小表数据以broadcast变量加载到内存,然后广播到各个Executor上,直接在map中做join。在Spark中,可以通过spark.sql.autoBroadcastJoinThreshold来设置启动BroadcastHashJoin的阀值,默认是10MB。
SparkSQL在执行过程中,在经过逻辑优化时,会估算是否要开启BroadcastHashJoin。但是这种优化对于复杂的SQL效果并不明显,因为复杂SQL会产生大量的Stage,spark优化程序很难准确的估算各个Stage的数据量来判断是否要开启BroadcastHashJoin。如下图所示:
图中左边的Stage的数据量只有46.9KB,完全可以优化成BroadcastHashJoin。然而Spark使用的还是常规的SortMergeJoin(也就是Shuffle)。
这个问题主要还是在逻辑优化时无法准确的估算数据量导致的,那么我们是否可以在执行过程中根据数据量动态的去调整执行计划来解决这个问题呢?
问题三:数据倾斜的问题
数据倾斜是某一个partition的数据量远大于其他的partition,数据量大的那个partition处理速度就会拖慢整个任务的处理速度。
我们使用的 MapReduce、Spark 和 Flink 都会存在数据倾斜的问题,而且在实际需求开发中(比如使用 join 和 group by 操作),数据倾斜问题也是出现频率比较高的,大部分作业卡在 99% 进度的罪魁祸首。
数据倾斜引起的原因
- 源表本身就有倾斜的数据
- 中间操作(比如 outer join)可能生成倾斜数据
数据倾斜的危害
- 作业运行过程被单个 task 拖垮
- 可能引起 OOM
如何解决数据倾斜
- 增加 shuffle partition 数量
通过调整 shuffle partition 数量来避免某个 partition 数据量特别大,将该 partition 数据分散到多个 partition 中。
- 加盐处理倾斜的 key
增加 shuffle partition 数量的方法,对于同一个海量数据倾斜的 key 来说,不起作用。不过,我们可以对该数据倾斜的 key 通过加盐方式来打散数据,然后再借助 shuffle partition 的功能。
- 使用 Broadcast Hash Join
在某些场景下,可以把 Sort Merge Join 转化成 Broadcast Hash Join,从而避免 shuffle 产生的数据倾斜。比如,如果两个 join 的表中有一个表是小表,可以优化成Broadcast Hash Join 来消除 shuffle 引起的数据倾斜问题。
但是上面这些解决方案都是针对单一任务进行调优,没有一个解决方案可以有效的解决所有的数据倾斜问题。
Spark3的AQE(adaptive query execution)
简单说一下,SQL 语句首先通过 Parser 模块被解析为语法树,称为 Unresolved Logical Plan,接着 Unresolved Logical Plan 通过 Analyzer 模块借助于 Catalog 中的表信息解析为 Logical Plan,然后 Optimizer 再通过各种优化策略进行深入优化,得到 Optimized Logical Plan,Planner 模块再将优化后的逻辑计划根据预先设定的映射逻辑转换为 Physical Plan,最后物理执行计划做 RDD 计算,提交 Spark 集群运算,最终向用户返回数据。
Adaptive Execution 项目的想法是:
当一个 stage 的 map 任务在 runtime 完成时,我们利用 map 输出大小信息,对并行度、join 策略和倾斜处理进行相应的调整。
Adaptive Execution 框架
-
当一个 Adaptive Stage 执行时,它会急切地执行它所有的子 Adaptive Stage
-
当所有的子 Adaptive Stage 执行完成后,它将拥有所有的 map 输出大小,用于优化决策。
并行度优化
通过使用 map 输出大小的信息,我们可以在运行时对并行度进行调整。
如上图所示,假设我们设置初始 shuffle partition 数量为 8,在 map stage 结束之后,可以看到每一个 Partition(1-8)的大小分别是20M、30M、10M、20M、35M、45M、10M 和 70M。假设设置每一个 reducer 处理的目标数据量(target input size)是 64M,那么在运行时,我们实际使用 4 个 reducer,即第一个 reducer 处理 Partition 1-3,共 60M,第二个 reducer 处理 Partition 4-5,共 55M,第三个 reducer 处理 Partition 6-7,共 55M,第四个 reducer 处理 Partition 8,即 70M。整个作业需要 4 个 task 运行,而不是 8 个 task。
一般情况下,一个 partition 是由一个 task 来处理的。经过优化,我们可以安排一个 task 处理多个 partition,这样,我们就可以保证各个分区相对均衡,不会存在大量数据量很小的 partition。
开启 Adaptive Execution 特性的方式:
1 | spark.sql.adaptive.enabled=true |
参数配置:
1 | spark.sql.adaptive.shuffle.targetPostShuffleInputSize |
动态调整 reduce 个数的 partition 大小依据。如设置 64MB,则 reduce 阶段每个 task 最少处理 64MB 的数据。默认值为 64MB。
1 | spark.sql.adaptive.shuffle.targetPostShuffleRowCount |
动态调整 reduce 个数的 partition 条数依据。如设置 20000000,则 reduce 阶段每个 task 最少处理 20000000 条的数据。默认值为 20000000。
1 | spark.sql.adaptive.minNumPostShufflePartitions |
reduce 个数区间最小值。
1 | spark.sql.adaptive.maxNumPostShufflePartitions |
reduce 个数区间最大值。
Join 策略优化
同样的,通过使用 map 输出大小的信息,我们可以在运行时对 join 策略进行调整。
在 Shuffle Write 之后,观察两个 Stage 输出的数据量。如果有一个 Stage 数据量明显比较小,可以转换成 Broadcast Hash Join,这样就可以动态的去调整执行计划。
蓝色方框为大表,黄色方框为小表,在shuffle write之后,经过了并行度优化之后,小表数据为10M,符合广播策略,将 Sort Merge Join 转化成 Broadcast Hash Join,此时 join 读取数据是直接从本地读取,没有数据通过网络传输,避开了网络IO的开销,性能会高很多。
开启方式:
1 2 | spark.sql.adaptive.enabled=true spark.sql.adaptive.join.enabled=true |
参数配置:
1 | spark.sql.adaptiveBroadcastJoinThreshold |
设置了 SortMergeJoin 转 BroadcastJoin 的阈值。如果不设置该参数,该阈值与 spark.sql.autoBroadcastJoinThreshold 的值相等。
数据倾斜优化处理
对于大量小数据的 partiiton,可以通过合并来解决问题,即一个 task 处理多个 partition 的数据。
对于数据量特别大的 partition,使用多个 task 来处理该 partition。
开启自动调整数据倾斜功能后,在作业执行过程中,Spark 会自动找出出现倾斜的 partiiton,然后用多个 task 来处理该 partition,之后再将这些 task 的处理结果进行合并。
开启方式:
1 | spark.sql.adaptive.skewedJoin.enabled=true |
倾斜处理开关。
1 | spark.sql.adaptive.skewedPartitionMaxSplits |
在开启 Adaptive Execution 时,控制处理一个倾斜 partition 的 task 个数上限,默认值为 5。
1 | spark.sql.adaptive.skewedPartitionRowCountThreshold |
倾斜的 partition 条数不能小于该值。partition 的条数如果少于这个值,数据量再大也不会被当成是倾斜的partition。默认值为 10000000。
1 | spark.sql.adaptive.skewedPartitionSizeThreshold |
倾斜的 partition 大小不能小于该值。默认值为 64MB。
1 | spark.sql.adaptive.skewedPartitionFactor |
当一个 partition 的 size 大小大于该值(所有 parititon 大小的中位数)且大于spark.sql.adaptive.skewedPartitionSizeThreshold,或者 parition 的条数大于该值(所有 parititon 条数的中位数)且大于 spark.sql.adaptive.skewedPartitionRowCountThreshold,才会被当做倾斜的 partition 进行相应的处理。默认值为 10。