gitbook/零基础入门Spark/docs/429113.md

187 lines
16 KiB
Markdown
Raw Permalink Normal View History

2022-09-03 22:05:03 +08:00
# 19 | 配置项详解:哪些参数会影响应用程序执行性能?
你好,我是吴磊。
在上一讲我们学习了Broadcast Join这种执行高效的Join策略。要想触发Spark SQL选择这类Join策略可以利用SQL Functions中的broadcast函数来强制广播基表。在这种情况下Spark SQL会完全“尊重”开发者的意愿只要基表小于8GB它就会竭尽全力地去尝试进行广播并采用Broadcast Join策略。
除了这种比较“强势”的做法我们还可以用另一种比较温和方式来把选择权“下放”给Spark SQL让它自己来决定什么时候选择Broadcast Join什么时候回退到Shuffle Join。这种温和的方式就是配置项设置。在第12讲我们掌握了Spark常规配置项今天这一讲咱们来说一说与Spark SQL有关的那些配置项。
不过打开Spark官网的 [Configuration页面](http://spark.apache.org/docs/latest/configuration.html)你会发现这里有上百个配置项与Spark SQL相关的有好几十个看得人眼花缭乱、头晕目眩。实际上绝大多数配置项仅需采用默认值即可并不需要我们过多关注。因此我们把目光和注意力聚集到Join策略选择和AQE上。
Join策略的重要性不必多说AQEAdaptive Query Execution是Spark 3.0推出的新特性它帮助Spark SQL在运行时动态地调整执行计划更加灵活地优化作业的执行性能。
## Broadcast Join
接下来我们先来说说如何使用配置项来“温和”地让Spark SQL选择Broadcast Join。对于参与Join的两张表来说我们把其中尺寸较小的表称作基表。
如果基表的存储尺寸小于广播阈值那么无需开发者显示调用broadcast函数Spark SQL同样会选择Broadcast Join的策略在基表之上创建广播变量来完成两张表的数据关联。
那么问题来了广播阈值是什么它是怎么定义的呢广播阈值实际上就是一个标记存储尺寸的数值它可以是10MB、也可是1GB等等。**广播阈值由如下配置项设定只要基表小于该配置项的设定值Spark SQL就会自动选择Broadcast Join策略**。
![图片](https://static001.geekbang.org/resource/image/94/c2/94842ac81446fdb91942eccf9664fbc2.jpg?wh=1920x411 "广播阈值配置项 ")
如上表所示广播阈值的默认值为10MB。一般来说在工业级应用中我们往往把它设置到2GB左右即可有效触发Broadcast Join。广播阈值有了要比较它与基表存储尺寸谁大谁小Spark SQL还要还得事先计算好基表的存储尺寸才行。那问题来了Spark SQL依据什么来计算这个数值呢
这个问题要分两种情况讨论如果基表数据来自文件系统那么Spark SQL用来与广播阈值对比的基准就是基表在磁盘中的存储大小。如果基表数据来自DAG计算的中间环节那么Spark SQL将参考DataFrame执行计划中的统计值跟广播阈值做对比如下所示。
```scala
val df: DataFrame = _
// 先对分布式数据集加Cache
df.cache.count
 
// 获取执行计划
val plan = df.queryExecution.logical
// 获取执行计划对于数据集大小的精确预估
val estimated: BigInt = spark
.sessionState
.executePlan(plan)
.optimizedPlan
.stats
.sizeInBytes
```
讲到这里你也许会有点不耐烦“何必这么麻烦又要设置配置项又要提前预估基表大小真是麻烦还不如用上一讲提到的broadcast函数来得干脆
从开发者的角度看来确实broadcast函数用起来更方便一些。不过广播阈值加基表预估的方式除了为开发者提供一条额外的调优途径外还为Spark SQL的动态优化奠定了基础。
所谓动态优化自然是相对静态优化来说的。在3.0版本之前对于执行计划的优化Spark SQL仰仗的主要是编译时运行时之前的统计信息如数据表在磁盘中的存储大小等等。
因此在3.0版本之前Spark SQL所有的优化机制如Join策略的选择都是静态的它没有办法在运行时动态地调整执行计划从而顺应数据集在运行时此消彼长的变化。
举例来说在Spark SQL的逻辑优化阶段两张大表的尺寸都超过了广播阈值因此Spark SQL在物理优化阶段就不得不选择Shuffle Join这种次优的策略。
但实际上在运行时期间其中一张表在Filter过后剩余的数据量远小于广播阈值完全可以放进广播变量。可惜此时“木已成舟”静态优化机制没有办法再将Shuffle Join调整为Broadcast Join。
## AQE
为了弥补静态优化的缺陷、同时让Spark SQL变得更加智能Spark社区在3.0版本中推出了AQE机制。
**AQE的全称是Adaptive Query Execution翻译过来是“自适应查询执行”。它包含了3个动态优化特性分别是Join策略调整、自动分区合并和自动倾斜处理**。
或许是Spark社区对于新的优化机制偏向于保守AQE机制默认是未开启的要想充分利用上述的3个特性我们得先把spark.sql.adaptive.enabled修改为true才行。
![图片](https://static001.geekbang.org/resource/image/62/53/6213e9819da6a63be3f9714932da0c53.jpg?wh=1920x437 "是否启用AQE")
好啦成功开启了AQE机制之后接下来我们就结合相关的配置项来聊一聊这些特性都解决了哪些问题以及它们是如何工作的。
### Join策略调整
我们先来说说Join策略调整如果用一句话来概括**Join策略调整指的就是Spark SQL在运行时动态地将原本的Shuffle Join策略调整为执行更加高效的Broadcast Join**。
具体来说每当DAG中的Map阶段执行完毕Spark SQL就会结合Shuffle中间文件的统计信息重新计算Reduce阶段数据表的存储大小。如果发现基表尺寸小于广播阈值那么Spark SQL就把下一阶段的Shuffle Join调整为Broadcast Join。
不难发现这里的关键是Shuffle以及Shuffle的中间文件。**事实上不光是Join策略调整这个特性整个AQE机制的运行都依赖于DAG中的Shuffle环节**。
所谓巧妇难为无米之炊要做到动态优化Spark SQL必须要仰仗运行时的执行状态而Shuffle中间文件则是这些状态的唯一来源。
举例来说通过Shuffle中间文件Spark SQL可以获得诸如文件尺寸、Map Task数据分片大小、Reduce Task分片大小、空文件占比之类的统计信息。正是利用这些统计信息Spark SQL才能在作业执行的过程中动态地调整执行计划。
我们结合例子进一步来理解以Join策略调整为例给定如下查询语句假设salaries表和employees表的存储大小都超过了广播阈值在这种情况下对于两张表的关联计算Spark SQL只能选择Shuffle Join策略。
不过实际上employees按照年龄过滤之后剩余的数据量是小于广播阈值的。这个时候得益于AQE机制的Join策略调整Spark SQL能够把最初制定的Shuffle Join策略调整为Broadcast Join策略从而在运行时加速执行性能。
```sql
select * from salaries inner join employees
  on salaries.id = employees.id
  where employees.age >= 30 and employees.age < 45
```
你看在这种情况下广播阈值的设置、以及基表过滤之后数据量的预估就变得非常重要。原因在于这两个要素决定了Spark SQL能否成功地在运行时充分利用AQE的Join策略调整特性进而在整体上优化执行性能。因此我们必须要掌握广播阈值的设置方法以及数据集尺寸预估的方法。
介绍完Join策略调整接下来我们再来说说AQE机制的另外两个特性自动分区合并与自动倾斜处理它们都是对于Shuffle本身的优化策略。
我们先来说说自动分区合并与自动倾斜处理都在尝试解决什么问题。我们知道Shuffle的计算过程分为两个阶段Map阶段和Reduce阶段。Map阶段的数据分布往往由分布式文件系统中的源数据决定因此数据集在这个阶段的分布是相对均匀的。
Reduce阶段的数据分布则不同它是由Distribution Key和Reduce阶段并行度决定的。并行度也就是分区数目这个概念咱们在之前的几讲反复强调想必你并不陌生。
而Distribution Key则定义了Shuffle分发数据的依据对于reduceByKey算子来说Distribution Key就是Paired RDD的Key而对于repartition算子来说Distribution Key就是传递给repartition算子的形参如repartition($“Column Name”)。
在业务上Distribution Key往往是user\_id、item\_id这一类容易产生倾斜的字段相应地数据集在Reduce阶段的分布往往也是不均衡的。
数据的不均衡往往体现在两个方面一方面是一部分数据分区的体量过小而另一方面则是少数分区的体量极其庞大。AQE机制的自动分区合并与自动倾斜处理正是用来应对数据不均衡的这两个方面。
### 自动分区合并
了解了自动分区合并的用武之地接下来我们来说说Spark SQL具体如何做到把Reduce阶段过小的分区合并到一起。要弄清楚分区合并的工作原理我们首先得搞明白“分区合并从哪里开始又到哪里结束呢
具体来说Spark SQL怎么判断一个数据分区是不是足够小、它到底需不需要被合并再者既然是对多个分区做合并那么自然就存在一个收敛条件。原因很简单如果一直不停地合并下去那么整个数据集就被捏合成了一个超级大的分区并行度也会下降至1显然这不是我们想要的结果。
![](https://static001.geekbang.org/resource/image/2d/cb/2d9d860cde0375a061f9cf4628c514cb.jpg?wh=6986x2876 "分区合并示意图")
事实上Spark SQL采用了一种相对朴素的方法来实现分区合并。具体来说**Spark SQL事先并不去判断哪些分区是不是足够小而是按照分区的编号依次进行扫描当扫描过的数据体量超过了“目标尺寸”时就进行一次合并**。而这个目标尺寸,由以下两个配置项来决定。
![图片](https://static001.geekbang.org/resource/image/44/3f/443a5062311169174b96d7c9bd73843f.jpg?wh=1920x528 "分区合并相关配置项")
其中开发者可以通过第一个配置项spark.sql.adaptive.advisoryPartitionSizeInBytes来直接指定目标尺寸。第二个参数用于限制Reduce阶段在合并之后的并行度避免因为合并导致并行度过低造成CPU资源利用不充分。
结合数据集大小与最低并行度,我们可以反推出来每个分区的平均大小,假设我们把这个平均大小记作是#partitionSize。那么实际的目标尺寸取advisoryPartitionSizeInBytes设定值与#partitionSize之间较小的那个数值。
确定了目标尺寸之后Spark SQL就会依序扫描数据分区当相邻分区的尺寸之和大于目标尺寸的时候Spark SQL就把扫描过的分区做一次合并。然后继续使用这种方式依次合并剩余的分区直到所有分区都处理完毕。
### 自动倾斜处理
没有对比就没有鉴别,分析完自动分区合并如何搞定数据分区过小、过于分散的问题之后,接下来,我们再来说一说,自动倾斜处理如何应对那些倾斜严重的大分区。
经过上面的分析,我们不难发现,自动分区合并实际上包含两个关键环节,一个是确定合并的目标尺寸,一个是依次扫描合并。与之相对应,自动倾斜处理也分为两步,**第一步是检测并判定体量较大的倾斜分区,第二步是把这些大分区拆分为小分区**。要做到这两步Spark SQL需要依赖如下3个配置项。
![图片](https://static001.geekbang.org/resource/image/48/6b/480177b03202283aa493684672207c6b.jpg?wh=1920x607 "自动倾斜处理的配置项")
其中前两个配置项用于判定倾斜分区第3个配置项advisoryPartitionSizeInBytes我们刚刚学过这个参数除了用于合并小分区外同时还用于拆分倾斜分区可以说是“一菜两吃”。
下面我们重点来讲一讲Spark SQL如何利用前两个参数来判定大分区的过程。
首先Spark SQL对所有数据分区按照存储大小做排序取中位数作为基数。然后将中位数乘以skewedPartitionFactor指定的比例系数得到判定阈值。凡是存储尺寸大于判定阈值的数据分区都有可能被判定为倾斜分区。
为什么说“有可能”而不是“一定”呢原因是倾斜分区的判定还要受到skewedPartitionThresholdInBytes参数的限制它是判定倾斜分区的最低阈值。也就是说只有那些尺寸大于skewedPartitionThresholdInBytes设定值的“候选分区”才会最终判定为倾斜分区。
为了更好地理解这个判定的过程我们来举个例子。假设数据表salaries有3个分区大小分别是90MB、100MB和512MB。显然这3个分区的中位数是100MB那么拿它乘以比例系数skewedPartitionFactor默认值为5得到判定阈值为100MB \* 5 = 500MB。因此在咱们的例子中只有最后一个尺寸为512MB的数据分区会被列为“候选分区”。
接下来Spark SQL还要拿512MB与skewedPartitionThresholdInBytes作对比这个参数的默认值是256MB。
显然512MB比256MB要大得多这个时候Spark SQL才会最终把最后一个分区判定为倾斜分区。相反假设我们把skewedPartitionThresholdInBytes这个参数调大设置为1GB那么最后一个分区就不满足最低阈值因此也就不会被判定为倾斜分区。
倾斜分区判定完毕之后下一步就是根据advisoryPartitionSizeInBytes参数指定的目标尺寸对大分区进行拆分。假设我们把这个参数的值设置为256MB那么刚刚512MB的大分区就会被拆成两个小分区512MB / 2 = 256MB。拆分之后salaries表就由3个分区变成了4个分区每个数据分区的尺寸都不超过256MB。
## 重点回顾
好啦到此为止与Spark SQL相关的重要配置项我们就讲到这里。今天的内容很多我们一起来总结一下。
首先我们介绍了广播阈值这一概念它的作用在于当基表尺寸小于广播阈值时Spark SQL将自动选择Broadcast Join策略来完成关联计算。
然后,我们分别介绍了**AQEAdaptive Query Execution机制的3个特性分别是Join策略调整、自动分区合并、以及自动倾斜处理**。与Spark SQL的静态优化机制不同AQE结合Shuffle中间文件提供的统计信息在运行时动态地调整执行计划从而达到优化作业执行性能的目的。
所谓Join策略调整它指的是结合过滤之后的基表尺寸与广播阈值Spark SQL在运行时动态地将原本的Shuffle Join策略调整为Broadcast Join策略的过程。基表尺寸的预估可以使用如下方法来获得。
```scala
val df: DataFrame = _
// 先对分布式数据集加Cache
df.cache.count
// 获取执行计划
val plan = df.queryExecution.logical
// 获取执行计划对于数据集大小的精确预估
val estimated: BigInt = spark
.sessionState
.executePlan(plan)
.optimizedPlan
.stats
.sizeInBytes
```
自动分区合并与自动倾斜处理实际上都是用来解决Shuffle过后数据分布不均匀的问题。自动分区合并的作用在于合并过小的数据分区从而避免Task粒度过细、任务调度开销过高的问题。与之相对自动倾斜处理它的用途在于拆分过大的数据分区从而避免个别Task负载过高而拖累整个作业的执行性能。
不论是广播阈值还是AQE的诸多特性我们都可以通过调节相关的配置项来影响Spark SQL的优化行为。为了方便你回顾、查找这些配置项我整理了如下表格供你随时参考。
![图片](https://static001.geekbang.org/resource/image/b8/aa/b85084c1f228a649cd1f3d2cfyy762aa.jpg?wh=1920x932)
## 每课一练
结合AQE必须要依赖Shuffle中间文件这一特点你能说一说AQE有什么不尽如人意之处吗提示从Shuffle的两个计算阶段出发去思考这个问题
欢迎你在留言区跟我交流讨论,也推荐你把这一讲分享给更多的同事、朋友。