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.

134 lines
16 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 10 | 调优一筹莫展,配置项速查手册让你事半功倍!(下)
你好,我是吴磊。
上一讲我们讲了硬件资源类的配置项。这一讲我们继续说说Shuffle类和Spark SQL大类都有哪些配置项它们的含义和作用以及它们能解决的问题。同时和上一讲一样我们今天讲到的配置项也全部会围绕Executors展开。
## Shuffle类配置项
首先我们来说说Shuffle类。纵观Spark官网的[Configuration页面](http://spark.apache.org/docs/latest/configuration.html)你会发现能调节Shuffle执行性能的配置项真是寥寥无几。其实这也很好理解因为一旦Shuffle成为应用中不可或缺的一环想要优化Shuffle本身的性能我们能做的微乎其微。
不过我们也不是完全束手无策。我们知道Shuffle的计算过程分为Map和Reduce这两个阶段。其中Map阶段执行映射逻辑并按照Reducer的分区规则将中间数据写入到本地磁盘Reduce阶段从各个节点下载数据分片并根据需要实现聚合计算。
那么我们就可以通过spark.shuffle.file.buffer和spark.reducer.maxSizeInFlight这两个配置项来分别调节Map阶段和Reduce阶段读写缓冲区的大小。具体该怎么做呢我们一一来看。
![](https://static001.geekbang.org/resource/image/ab/50/ab65b81ffe85e61b186b1fb3e8620750.jpeg?wh=1544*550 "缓冲区相关配置项")
首先在Map阶段计算结果会以中间文件的形式被写入到磁盘文件系统。同时为了避免频繁的I/O操作Spark会把中间文件存储到写缓冲区Write Buffer。**这个时候我们可以通过设置spark.shuffle.file.buffer来扩大写缓冲区的大小缓冲区越大能够缓存的落盘数据越多Spark需要刷盘的次数就越少I/O效率也就能得到整体的提升。**
其次在Reduce阶段因为Spark会通过网络从不同节点的磁盘中拉取中间文件它们又会以数据块的形式暂存到计算节点的读缓冲区Read Buffer。缓冲区越大可以暂存的数据块越多在数据总量不变的情况下拉取数据所需的网络请求次数越少单次请求的网络吞吐越高网络I/O的效率也就越高。**这个时候我们就可以通过spark.reducer.maxSizeInFlight配置项控制Reduce端缓冲区大小来调节Shuffle过程中的网络负载。**
事实上对Shuffle计算过程的优化牵扯到了全部的硬件资源包括CPU、内存、磁盘和网络。因此我们上一讲汇总的关于CPU、内存和硬盘的配置项也同样可以作用在Map和Reduce阶段的内存计算过程上。
除此之外Spark还提供了一个叫做**spark.shuffle.sort.bypassMergeThreshold**的配置项去处理一种特殊的Shuffle场景。
![](https://static001.geekbang.org/resource/image/49/71/49yy9eff7b5521f0d51f9451252e0a71.jpeg?wh=1628*448 "Reduce端相关配置项")
自1.6版本之后Spark统一采用Sort shuffle manager来管理Shuffle操作在Sort shuffle manager的管理机制下无论计算结果本身是否需要排序Shuffle计算过程在Map阶段和Reduce阶段都会引入排序操作。
这样的实现机制对于repartition、groupBy这些操作就不太公平了这两个算子一个是对原始数据集重新划分分区另一个是对数据集进行分组压根儿就没有排序的需求。所以Sort shuffle manager实现机制引入的排序步骤反而变成了一种额外的计算开销。
因此,**在不需要聚合也不需要排序的计算场景中我们就可以通过设置spark.shuffle.sort.bypassMergeThreshold的参数来改变Reduce端的并行度**默认值是200。当Reduce端的分区数小于这个设置值的时候我们就能避免Shuffle在计算过程引入排序。
## Spark SQL大类配置项
接下来我们再来说说Spark SQL的相关配置项。在官网的[Configuration页面](http://spark.apache.org/docs/latest/configuration.html)中Spark SQL下面的配置项还是蛮多的其中对执行性能贡献最大的当属AQEAdaptive query execution自适应查询引擎引入的那3个特性了也就是自动分区合并、自动数据倾斜处理和Join策略调整。因此关于Spark SQL的配置项咱们围绕着这3个特性去汇总。
首先我们要知道,**AQE功能默认是禁用的想要使用这些特性我们需要先通过配置项spark.sql.adaptive.enabled来开启AQE**,具体的操作如下:
![](https://static001.geekbang.org/resource/image/b3/5b/b39cdde3cd466b5fba4cdyy01386eb5b.jpeg?wh=1832*430 "启用AQE的配置项")
因为这3个特性的原理我们在开发原则那一讲说过这里我会先带你简单回顾一下然后我们重点来讲这些环节对应的配置项有哪些。
### 哪些配置项与自动分区合并有关?
分区合并的场景用一句概括就是在Shuffle过程中因为数据分布不均衡导致Reduce阶段存在大量的小分区这些小分区的数据量非常小调度成本很高。
那么问题来了AQE是如何判断某个分区是不是足够小到底需不需要合并的呢另外既然是对多个分区进行合并自然就存在一个收敛条件的问题如果一直不停地合并下去整个分布式数据集最终就会合并为一个超级大的分区。简单来说就是“分区合并从哪里开始又到哪里结束呢
![](https://static001.geekbang.org/resource/image/da/4f/dae9dc8b90c2d5e0cf77180ac056a94f.jpg?wh=4359*1623 "分区合并示意图")
我们一起来看一下AQE分区合并的工作原理。如上图所示对于所有的数据分区无论大小AQE按照分区编号从左到右进行扫描边扫描边记录分区尺寸当相邻分区的尺寸之和大于“目标尺寸”时AQE就把这些扫描过的分区进行合并。然后继续向右扫描并采用同样的算法按照目标尺寸合并剩余分区直到所有分区都处理完毕。
总的来说就是,**AQE事先并不判断哪些分区足够小而是按照分区编号进行扫描当扫描量超过“目标尺寸”时就合并一次**。我们发现,这个过程中的关键就是“目标尺寸”的确定,它的大小决定了合并之后分布式数据集的分散程度。
那么“目标尺寸”由什么来决定的呢Spark提供了两个配置项来共同决定分区合并的“目标尺寸”它们分别是spark.sql.adaptive.advisoryPartitionSizeInBytes和spark.sql.adaptive.coalescePartitions.minPartitionNum。
![](https://static001.geekbang.org/resource/image/62/95/620fde8bd55cd9e937ebc31060936395.jpeg?wh=1920*639 "AQE自动分区合并相关配置项")
其中第一个参数advisoryPartitionSizeInBytes是开发者建议的目标尺寸第二个参数minPartitionNum的含义是合并之后的最小分区数假设它是200就说明合并之后的分区数量不能小于200。这个参数的目的就是避免并行度过低导致CPU资源利用不充分。
结合Shuffle后的数据集尺寸和最小分区数限制我们可以反推出来每个分区的平均大小咱们暂且把它记为#partitionSize。分区合并的目标尺寸取advisoryPartitionSizeInBytes与#partitionSize之间的最小值。
这么说比较抽象我们来举个例子。假设Shuffle过后数据大小为20GBminPartitionNum设置为200反推过来每个分区的尺寸就是20GB / 200 = 100MB。再假设advisoryPartitionSizeInBytes设置为200MB最终的目标分区尺寸就是取100MB200MB之间的最小值也就是100MB。因此你看并不是你指定了advisoryPartitionSizeInBytes是多少Spark就会完全尊重你的意见我们还要考虑minPartitionNum的设置。
### 哪些配置项与自动数据倾斜处理有关?
再来说说数据倾斜在数据关联Data Joins的场景中当AQE检测到倾斜的数据分区时会自动进行拆分操作把大分区拆成多个小分区从而避免单个任务的数据处理量过大。不过Spark 3.0版本发布的AQE暂时只能在Sort Merge Join中自动处理数据倾斜其他的Join实现方式如Shuffle Join还有待支持。
那么AQE如何判定数据分区是否倾斜呢它又是怎么把大分区拆分成多个小分区的
![](https://static001.geekbang.org/resource/image/3c/d6/3cd86b383909ecb2577d3839edfe2dd6.jpeg?wh=1920*701 "AQE数据倾斜处理相关配置项")
首先,**分区尺寸必须要大于spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes参数的设定值才有可能被判定为倾斜分区。然后AQE统计所有数据分区大小并排序取中位数作为放大基数尺寸大于中位数一定倍数的分区会被判定为倾斜分区中位数的放大倍数也是由参数spark.sql.adaptive.skewJoin.skewedPartitionFactor控制。**
接下来我们还是通过一个例子来理解。假设数据表A有3个分区分区大小分别是80MB、100MB和512MB。显然这些分区按大小个排序后的中位数是100MB因为skewedPartitionFactor的默认值是5倍所以大于100MB \* 5 = 500MB的分区才有可能被判定为倾斜分区。在我们的例子中只有最后一个尺寸是512MB的分区符合这个条件。
这个时候Spark还不能完全判定它就是倾斜分区还要看skewedPartitionThresholdInBytes配置项这个参数的默认值是256MB。对于那些满足中位数条件的分区必须要大于256MBSpark才会把这个分区最终判定为倾斜分区。假设skewedPartitionThresholdInBytes设定为1GB那在我们的例子中512MB那个大分区Spark也不会把它看成是倾斜分区自然也就不能享受到AQE对于数据倾斜的优化处理。
检测到倾斜分区之后接下来就是对它拆分拆分的时候还会用到advisoryPartitionSizeInBytes参数。假设我们将这个参数的值设置为256MB那么刚刚那个512MB的倾斜分区会以256MB为粒度拆分成多份因此这个大分区会被拆成 2 个小分区( 512MB / 256MB =2。拆分之后原来的数据表就由3个分区变成了4个分区每个分区的尺寸都不大于256MB。
### 哪些配置项与Join策略调整有关
最后咱们再来说说数据关联Joins。数据关联可以说是数据分析领域中最常见的操作Spark SQL中的Join策略调整它实际上指的是把会引入Shuffle的Join方式如Hash Join、Sort Merge Join“降级”Demote为Broadcast Join。
**Broadcast Join的精髓在于“以小博大”它以广播的方式将小表的全量数据分发到集群中所有的Executors大表的数据不需要以Join keys为基准去Shuffle就可以与小表数据原地进行关联操作。**Broadcast Join以小表的广播开销为杠杆博取了因消除大表Shuffle而带来的巨大性能收益。可以说Broadcast Join把“杠杆原理”应用到了极致。
在Spark发布AQE之前开发者可以利用spark.sql.autoBroadcastJoinThreshold配置项对数据关联操作进行主动降级。这个参数的默认值是10MB参与Join的两张表中只要有一张数据表的尺寸小于10MB二者的关联操作就可以降级为Broadcast Join。为了充分利用Broadcast Join“以小博大”的优势你可以考虑把这个参数值调大一些2GB左右往往是个不错的选择。
![](https://static001.geekbang.org/resource/image/d0/61/d082a82b24e2f5ecc484d849abe2e361.jpeg?wh=1733*448 "AQE推出之前Join策略相关配置项")
不过autoBroadcastJoinThreshold这个参数虽然好用但是有两个让人头疼的短板。
一是可靠性较差。尽管开发者明确设置了广播阈值而且小表数据量在阈值以内但Spark对小表尺寸的误判时有发生导致Broadcast Join降级失败。
二来预先设置广播阈值是一种静态的优化机制它没有办法在运行时动态对数据关联进行降级调整。一个典型的例子是两张大表在逻辑优化阶段都不满足广播阈值此时Spark SQL在物理计划阶段会选择Shuffle Joins。但在运行时期间其中一张表在Filter操作之后有可能出现剩余的数据量足够小小到刚好可以降级为Broadcast Join。在这种情况下静态优化机制就是无能为力的。
AQE很好地解决了这两个头疼的问题。**首先AQE的Join策略调整是一种动态优化机制对于刚才的两张大表AQE会在数据表完成过滤操作之后动态计算剩余数据量当数据量满足广播条件时AQE会重新调整逻辑执行计划在新的逻辑计划中把Shuffle Joins降级为Broadcast Join。再者运行时的数据量估算要比编译时准确得多因此AQE的动态Join策略调整相比静态优化会更可靠、更稳定。**
![](https://static001.geekbang.org/resource/image/db/5b/db78a727a8fc6fc6da0cdecaa5ba755b.jpeg?wh=1920*407 "AQE推出之后Join策略相关配置项")
不过启用动态Join策略调整还有个前提也就是要满足nonEmptyPartitionRatioForBroadcastJoin参数的限制。这个参数的默认值是0.2大表过滤之后非空的数据分区占比要小于0.2才能成功触发Broadcast Join降级。
这么说有点绕我们来举个例子。假设大表过滤之前有100个分区Filter操作之后有85个分区内的数据因为不满足过滤条件在过滤之后都变成了没有任何数据的空分区另外的15个分区还保留着满足过滤条件的数据。这样一来这张大表过滤之后的非空分区占比是 15 / 100 = 15%因为15%小于0.2所以这个例子中的大表会成功触发Broadcast Join降级。
相反如果大表过滤之后非空分区占比大于0.2那么剩余数据量再小AQE也不会把Shuffle Joins降级为Broadcast Join。因此如果你想要充分利用Broadcast Join的优势可以考虑把这个参数适当调高。
## 小结
今天这一讲我们深入探讨了Shuffle类和Spark SQL大类两类配置项以及每个配置项可以解决的问题。
对于Shuffle类我们要知道在Shuffle过程中对于不需要排序和聚合的操作我们可以通过控制spark.shuffle.sort.bypassMergeThreshold参数来避免Shuffle执行过程中引入的排序环节从而避免没必要的计算开销。
对于Spark SQL大类我们首先要知道AQE默认是禁用状态要充分利用AQE提供的3个特性就是自动分区合并、数据倾斜处理和Join策略调整我们需要把spark.sql.adaptive.enabled置为true。
除此之外AQE的3个特性各自都有相对应的配置项需要我们单独调整。
* AQE中的自动分区合并过程与我们预想的不太一样。QE事先并不判断哪些分区足够小而是按照分区编号进行扫描当扫描量超过“目标尺寸”时就合并一次。目标尺寸由advisoryPartitionSizeInBytes和coalescePartitions.minPartitionNum两个参数共同决定。
* AQE能够自动处理Sort Merge Join场景中的数据倾斜问题。首先根据所有分区大小的中位数以及放大倍数skewedPartitionFactor来检测倾斜分区然后以advisoryPartitionSizeInBytes为粒度对倾斜分区进行拆分。
* AQE动态Join策略调整可以在运行时将Shuffle Joins降级为Broadcast Join同时运行时的数据量估算要比编译时准确得多因此相比静态优化会更可靠。不过需要我们注意的是Shuffle过后非空分区占比要小于nonEmptyPartitionRatioForBroadcastJoin才能触发Broadcast Join的降级优化。
好啦经过这两讲的学习我们一起汇总出了Spark中与性能调优息息相关的所有配置项为了方便你快速查阅我把它们合并在了一张文稿的表格中希望你能在工作中好好利用起来。
![](https://static001.geekbang.org/resource/image/31/6a/31356505a2c36bac10de0e06d7e4526a.jpg?wh=1920*2510)
## 每日一练
1. AQE的分区合并算法略显简单粗暴如果让你来重新实现分区合并特性的话你都有哪些思路呢
2. AQE中数据倾斜的处理机制你认为有哪些潜在的隐患
期待在留言区看到你的思考和答案,也欢迎你把这份调优手册分享给你的朋友们,我们下一讲见!