大数据技术之_19_Spark学习_07_Spark 性能调优小结
========== Spark 的监控方式 ==========
1、Spark Web UI Spark 内置应用运行监控工具(提供了应用运行层面的主要信息--重要)
2、Ganglia 分析集群的使用状况和资源瓶颈(提供了集群的使用状况--资源瓶颈--重要)
3、Nmon 主机 CPU、网络、磁盘、内存(提供了单机信息)
4、Jmeter 系统实时性能监控工具(提供了单机的实时信息)
5、Jprofile Java 程序性能监控工具(提供了对应用程序开发和JVM的监控--次重要)
========== Spark 的数据倾斜 ==========
1、数据倾斜是什么?答:就是在 shuffle 过程中分配到下游的 task 的数量不平均,导致了每个 task 处理的数据量和数据时间有很大差别,导致整个应用的运行时间大大加长。
2、如何定位数据倾斜?
(1)是不是有 OOM 情况出现。
(2)是不是应用运行时间差异很大,导致总体时间很长。
(3)查看导致数据倾斜的 Key 的数据分布情况,即如果有些 Key 具有大量的条数,那么可能出现数据倾斜问题。
(4)通过 Spark Web UI 和其他一些监控方式中出现的异常来综合判断。
(5)代码中是否有一些导致 shuffle 的算子出现。
3、数据倾斜的几种典型情况
(1)数据源中的数据分布不均匀,Spark 需要频繁交互。
(2)数据集中的不同 Key 由于分区方式,导致数据倾斜。
(3)JOIN 操作中,一个数据集中的数据分布不均匀,另一个数据集较小。
(4)聚合操作中,数据集中的数据分布不均匀。
(5)JOIN 操作中,两个数据集都比较大,其中只有几个 Key 的数据分布不均匀。
(6)JOIN 操作中,两个数据集都比较大,有很多 Key 的数据分布不均匀。
(7)数据集中少数几个 Key 数据量很大,不重要,其他数据均匀。
========== Spark 缓解/消除数据倾斜的方式 ==========
1、尽量避免数据源的数据倾斜
适用情况:在一些 Java 系统与 Spark 结合使用的项目中,会出现 Java 代码频繁调用 Spark 作业的场景,而且对 Spark 作业的执行性能要求很高,就比较适合使用这种方案,即可以将数据倾斜提前到上游的 Hive ETL 中。
2、调整并行度:分散同一个 Task 的不同 Key
适用情况:有大量 Key 由于分区算法或者分区数的不同,导致了 Key 的分布不均匀。
解决方案:增大或者调小分区数。
3、自定义 Partitioner
适用场景:大量不同的 Key 被分配到了相同的 Task 造成该 Task 数据量过大。
解决方案:使用自定义的 Partitioner 实现类代替默认的 HashPartitioner,尽量将所有不同的 Key 均匀分配到不同的 Task 中。
4、缓解数据倾斜 - Reduce side Join 转变为 Map side Join
适用场景:在对 RDD 使用 join 类操作,或者是 Spark SQL 中使用 join 语句时,而且 join 操作中的一个 RDD 或表的数据量比较小(比如几百 M),比较适用此方案。
解决方案:采用广播小 RDD 全量数据 +map 算子来实现与 join 同样的效果,也就是 map join,此时就不会发生 shuffle 操作,也就不会发生数据倾斜。
5、缓解数据倾斜 - 两阶段聚合(局部聚合+全局聚合)
方案适用场景:对 RDD 执行 reduceByKey 等聚合类 shuffle 算子或者在 Spark SQL 中使用 group by 语句进行分组聚合时,比较适用这种方案。
方案实现思路:这个方案的核心实现思路就是进行两阶段聚合。第一次是局部聚合,先给每个 key 都打上一个随机数,比如 10 以内的随机数,此时原先一样的 key 就变成不一样的了,比如 (hello, 1) (hello, 1) (hello, 1) (hello, 1),就会变成 (1_hello, 1) (1_hello, 1) (2_hello, 1) (2_hello, 1)。接着对打上随机数后的数据,执行 reduceByKey 等聚合操作,进行局部聚合,那么局部聚合结果,就会变成了 (1_hello, 2) (2_hello, 2)。然后将各个 key 的前缀给去掉,就会变成 (hello,2)(hello,2),再次进行全局聚合操作,就可以得到最终结果了,比如 (hello, 4)。
方案实现原理:将原本相同的 key 通过附加随机前缀的方式,变成多个不同的 key,就可以让原本被一个 task 处理的数据分散到多个 task 上去做局部聚合,进而解决单个 task 处理数据量过多的问题。接着去除掉随机前缀,再次进行全局聚合,就可以得到最终的结果。具体原理见下图。
方案优点:对于聚合类的 shuffle 操作导致的数据倾斜,效果是非常不错的。通常都可以解决掉数据倾斜,或者至少是大幅度缓解数据倾斜,将 Spark 作业的性能提升数倍以上。
方案缺点:仅仅适用于聚合类的 shuffle 操作,适用范围相对较窄。如果是 join 类的 shuffle 操作,还得用其他的解决方案。
6、为倾斜的 key 增加随机前/后缀
方案适用场景:两张表都比较大,无法使用 Map 侧 Join。其中一个 RDD 有少数几个 Key 的数据量过大,另外一个 RDD 的 Key 分布较为均匀。
方案原理:为数据量特别大的 Key 增加随机前/后缀,使得原来 Key 相同的数据变为 Key 不相同的数据,从而使倾斜的数据集分散到不同的 Task 中,彻底解决数据倾斜问题。Join 另一侧的数据中,与倾斜 Key 对应的部分数据,与随机前缀集作笛卡尔乘积,从而保证无论数据倾斜侧倾斜 Key 如何加前缀,都能与之正常 Join。
方案解决方案:将有数据倾斜的 RDD 中倾斜 Key 对应的数据集单独抽取出来加上随机前缀,另外一个 RDD 每条数据分别与随机前缀结合形成新的 RDD(相当于将其数据增到到原来的 N 倍,N 即为随机前缀的总个数),然后将二者 Join 并去掉前缀。然后将不包含倾斜 Key 的剩余数据进行 Join。最后将两次 Join 的结果集通过 union 合并,即可得到全部 Join 结果。
方案优点:相对于 Map 侧 Join,更能适应大数据集的 Join。如果资源充足,倾斜部分数据集与非倾斜部分数据集可并行进行,效率提升明显。且只针对倾斜部分的数据做数据扩展,增加的资源消耗有限。
方案缺点:如果倾斜 Key 非常多,则另一侧数据膨胀非常大,此方案不适用。而且此时对倾斜 Key 与非倾斜 Key 分开处理,需要扫描数据集两遍,增加了开销。
7、使用随机前缀和扩容 RDD 进行 join
方案适用场景:如果在进行 join 操作时,RDD 中有大量的 key 导致数据倾斜,那么进行分拆 key 也没什么意义,此时就只能使用最后一种方案来解决问题了。
方案实现思路:该方案的实现思路基本和 “解决方案6” 类似,首先查看 RDD/Hive 表中的数据分布情况,找到那个造成数据倾斜的 RDD/Hive 表,比如有多个 key 都对应了超过 1 万条数据。
然后将该 RDD 的每条数据都打上一个 n 以内的随机前缀。
同时对另外一个正常的 RDD 进行扩容,将每条数据都扩容成 n 条数据,扩容出来的每条数据都依次打上一个 0~n 的前缀。
最后将两个处理后的 RDD 进行 join 即可。
方案实现原理:将原先一样的 key 通过附加随机前缀变成不一样的 key,然后就可以将这些处理后的 “不同key” 分散到多个 task 中去处理,而不是让一个 task 处理大量的相同 key。该方案与 “解决方案6” 的不同之处就在于,上一种方案是尽量只对少数倾斜 key 对应的数据进行特殊处理,由于处理过程需要扩容 RDD,因此上一种方案扩容 RDD 后对内存的占用并不大;而这一种方案是针对有大量倾斜 key 的情况,没法将部分 key 拆分出来进行单独处理,因此只能对整个 RDD 进行数据扩容,对内存资源要求很高。
方案优点:对 join 类型的数据倾斜基本都可以处理,而且效果也相对比较显著,性能提升效果非常不错。
方案缺点:该方案更多的是缓解数据倾斜,而不是彻底避免数据倾斜。而且需要对整个 RDD 进行扩容,对内存资源要求很高。
方案实践经验:曾经开发一个数据需求的时候,发现一个 join 导致了数据倾斜。优化之前,作业的执行时间大约是 60 分钟左右;使用该方案优化之后,执行时间缩短到 10 分钟左右,性能提升了 6 倍。
8、过滤少数导致倾斜的 key
方案适用场景:如果发现导致倾斜的 key 就少数几个,而且对计算本身的影响并不大的话,那么很适合使用这种方案。
========== Spark 运行资源参数调优 ==========
num-executors Spark 作业时 executors 的数量,推荐 50-100 个
executor-memory Spark 作业时 executors 的内存,推荐 4-8 G
executor-cores Spark 作业时 executors 的CPU 核心数,推荐 2-4 个
driver-memory Spark 作业时 driver 的内存,如果使用 map side join 或者执行一些类似于 collect 的操作时,那么要相应的调大该值
spark.default.parallelism Spark 作业时 每个 stage 默认的 task 的数量,推荐 num-executors * executor-cores 的 2~3 倍,500-1000 个
spark.storage.memoryFraction Spark 作业时 每个 executors 用于 RDD 缓存的内存比例,默认值是 Executor 60% 的内存,如果程序中有大量的 RDD 数据缓存,那么要相应的调大该比例
spark.shuffle.memoryFraction Spark 作业时 每个 executors 用于 Shuffle 操作时的内存比例,默认值是 Executor 20% 的内存,如果程序中有大量的 Shuffle 类算子,那么要相应的调大该该比例
========== Spark 程序开发调优 ==========
原则一:避免创建重复的 RDD
原则二:尽可能复用同一个 RDD(即需要操作的 RDD 数据是另一个 RDD 数据的子集)
原则三:对多次使用的 RDD 进行持久化
原则四:尽量避免使用 shuffle 类算子
原则五:使用 map-side 预聚合 shuffle 操作
原则六:使用高性能的算子(即尽量多使用类似 reduceByKey 或者 aggregateByKey 这种在 map 端预聚合的算子,不使用类似 groupByKey 这种在 reduce 端聚合的算子)
原则七:广播大变量(即如果算子中的算法使用到了大变量)
原则八:使用 Kryo 优化序列化性能(即代替 Java 默认的序列化方式)
原则九:分区 Shuffle 优化(即如果一个 RDD 频繁和其他 RDD 进行 Shuffle 操作,比如:cogroup()、 groupWith()、join()、leftOuterJoin()、rightOuterJoin()、groupByKey()、reduceByKey()、 combineByKey() 以及 lookup()等),那么最好将该 RDD 通过 partitionBy() 操作进行预分区,这些操作在 Shuffle 过程中会减少 Shuffle 的数据量,可以极大提高效率。)
原则十:优化数据结构(即尽量使用字符串替代对象,使用原始类型(比如 Int、Long)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用,从而降低 GC 频率,提升性能。)
使用 reduceByKey/aggregateByKey 替代 groupByKey -- map-side 预聚合的 shuffle 操作
使用 mapPartitions 替代普通 map -- 函数执行频率
使用 foreachPartitions 替代 foreach -- 函数执行频率
使用 filter 之后进行 coalesce 操作 -- filter后对分区进行压缩
使用 repartitionAndSortWithinPartitions 替代 repartition 与 sort类 操作 -- 如果需要在 repartition 重分区之后,还要进行排序,建议直接使用 repartitionAndSortWithinPartitions 算子
========== Spark Shuffle 调优 ==========
spark.shuffle.file.buffer
默认值:32k
参数说明:该参数用于设置 shuffle write task 的 BufferedOutputStream 的 buffer 缓冲大小。将数据写到磁盘文件之前,会先写入 buffer 缓冲中,待缓冲写满之后,才会溢写到磁盘。
调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如 64k),从而减少 shuffle write 过程中溢写磁盘文件的次数,也就可以减少磁盘 IO 次数,进而提升性能。在实践中发现,合理调节该参数,性能会有 1%~5% 的提升。
spark.reducer.maxSizeInFlight
默认值:48m
参数说明:该参数用于设置 shuffle read task 的 buffer 缓冲大小,而这个 buffer 缓冲决定了每次能够拉取多少数据。
调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如 96m),从而减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。在实践中发现,合理调节该参数,性能会有 1%~5% 的提升。
spark.shuffle.io.maxRetries
默认值:3
参数说明:shuffle read task 从 shuffle write task 所在节点拉取属于自己的数据时,如果因为网络异常导致拉取失败,是会自动进行重试的。该参数就代表了可以重试的最大次数。如果在指定次数之内拉取还是没有成功,就可能会导致作业执行失败。
调优建议:对于那些包含了特别耗时的 shuffle 操作的作业,建议增加重试最大次数(比如 60 次),以避免由于 JVM 的 full gc 或者网络不稳定等因素导致的数据拉取失败。在实践中发现,对于针对超大数据量(数十亿~上百亿)的 shuffle 过程,调节该参数可以大幅度提升稳定性。
spark.shuffle.io.retryWait
默认值:5s
参数说明:具体解释同上,该参数代表了每次重试拉取数据的等待间隔,默认是 5s。
调优建议:建议加大间隔时长(比如 60s),以增加 shuffle 操作的稳定性。
spark.shuffle.memoryFraction
默认值:0.2
参数说明:该参数代表了 Executor 内存中,分配给 shuffle read task 进行聚合操作的内存比例,默认是 20%。
调优建议:在资源参数调优中讲解过这个参数。如果内存充足,而且很少使用持久化操作,建议调高这个比例,给 shuffle read 的聚合操作更多内存,以避免由于内存不足导致聚合过程中频繁读写磁盘。在实践中发现,合理调节该参数可以将性能提升 10% 左右。
spark.shuffle.manager
默认值:sort
参数说明:该参数用于设置 ShuffleManager 的类型。Spark 1.5 以后,有三个可选项:hash、sort 和 tungsten-sort。HashShuffleManager 是 Spark 1.2 以前的默认选项,但是 Spark 1.2 以及之后的版本默认都是 SortShuffleManager 了。tungsten-sort 与 sort 类似,但是使用了 tungsten 计划中的堆外内存管理机制,内存使用效率更高。
调优建议:由于 SortShuffleManager 默认会对数据进行排序,因此如果你的业务逻辑中需要该排序机制的话,则使用默认的 SortShuffleManager 就可以;而如果你的业务逻辑不需要对数据进行排序,那么建议参考后面的几个参数调优,通过 bypass 机制或优化的 HashShuffleManager 来避免排序操作,同时提供较好的磁盘读写性能。这里要注意的是,tungsten-sort 要慎用,因为之前发现了一些相应的 bug。
spark.shuffle.sort.bypassMergeThreshold
默认值:200
参数说明:当 ShuffleManager 为 SortShuffleManager 时,如果 shuffle read task 的数量小于这个阈值(默认是 200),则 shuffle write 过程中不会进行排序操作,而是直接按照未经优化的 HashShuffleManager 的方式去写数据,但是最后会将每个 task 产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。
调优建议:当你使用 SortShuffleManager 时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于 shuffle read task 的数量。那么此时就会自动启用 bypass 机制,map-side 就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此 shuffle write 性能有待提高。
spark.shuffle.consolidateFiles
默认值:false
参数说明:如果使用 HashShuffleManager,该参数有效。如果设置为 true,那么就会开启 consolidate 机制,会大幅度合并 shuffle write 的输出文件,对于 shuffle read task 数量特别多的情况下,这种方法可以极大地减少磁盘 IO 开销,提升性能。
调优建议:如果的确不需要 SortShuffleManager 的排序机制,那么除了使用 bypass 机制,还可以尝试将 spark.shffle.manager 参数手动指定为 hash,使用 HashShuffleManager,同时开启 consolidate 机制。在实践中尝试过,发现其性能比开启了 bypass 机制的 SortShuffleManager 要高出 10%~30%。
========== Spark GC 调优 ==========
【转载文章务必保留出处和署名,谢谢!】