文章目录
1. SparkShuffle 概念
reduceByKey 会将上一个 RDD 中的每个 key 对应的所有 value 聚合成一个 value, 然后生成一个新的 RDD,元素类型是 <K,V> 对的形式,这样每个 key 对应一个聚合起来的 value。
这样我们就会想到几个问题:
- 聚合之前,每个 key 对应的 value 不一定都是在一个 partition 中,也不太可能在同一个节点上,因为 RDD 是分布式的弹性数据集,RDD 的 partition 极有可能分布在各个节点上。
针对这样的问题,我们如何聚合?
- Shuffle Write:上一个stage 的每个 map task 就必须保证将自己处理的当前分区的数据相同的 key 写入一个分区文件中,可能会写入多个不同的分区文件中。
- Shuffle Read:reduce task 就会从上一个 stage 的所有 task所在的机器上寻找属于自己的那份分区文件,这样就可以保证每个 key 所对应的 value 都会汇聚到同一个节点上去处理和聚合。
Spark 中有两种 Shuffle 管理类型,HashShuffleManager
和SortShuffleManager
。Spark1.2 之前是 HashShuffleManager
,Spark1.2 引入SortShuffleManager
。在 Spark2.0+ 版本中已经将HashShuffleManager
丢弃。
2. HashShuffleManager
2.1 普通机制
- 普通机制示意图
-
执行流程
- 每一个 map task 将不同结果写到不同的 buffer 中,每个 buffer 的大小为 32k。buffer 起到缓存的作用。
- 每个 buffer 文件最后对应一个磁盘小文件。
- reduce task 来拉取对应的磁盘小文件。
-
总结
-
map task 的计算结果会根据分区器 (默认是
hasPartitioner
) 来决定写入到哪个磁盘小文件中去。reduce task 会去 map 端拉取相应的磁盘小文件。 -
产生的磁盘小文件个数:
M(map task 个数) * R(reduce task 个数)
-
-
存在的问题
产生的磁盘小文件过多,会导致以下问题:
- 在 Shuffle Writer 过程中会产生很多写磁盘小文件的对象。
- 在 Shuffle Read 过程中会产生很多读磁盘小文件的对象。
- 在 JVM 堆内存中对象过多会造成频繁的gc,gc无法解决运行需要的内存的话,就会 OOM。
- 在数据传输过程中会有频繁的网络通信,频繁的网络通信出现通信故障的可能性大大增加,一旦网络通信出现故障会导致 shuffle file cannot find,由于这个错误导致 task失败。TaskScheduler 不负责重试,由DAGSchedule负责重试 Stage。
2.2 合并机制
- 合并机制示意图
-
执行流程
- 多个 map task 公用一个buffer,然后写入磁盘文件。
- reduce task 来拉取磁盘小文件
-
总结、
产生的磁盘小文件的个数:
C(core 的个数) * R(reduce task 个数)
3. SortShuffleManager
3.1 普通机制
- 普通机制示意图
-
执行流程
- map task 的计算结果会写入到一个内存数据结构里面,内存数据结构默认是 5M。
- 在 shuffle 的时候会有一个定时器,不定期的去估算这个内存结构的大小,当内存结构中的数据超过 5M 时,比如现在内存结构中的数据为 5.01M,那么他会申请
5.01*2 - 5 = 5.02M
内存数据结构。 - 如果申请内存成功,不会进行溢写磁盘。如果申请不成功,这时候会发送溢写磁盘。
- 在溢写磁盘之前,内存结构中的数据会进行排序分区。
- 然后开始溢写磁盘,写磁盘是以 batch 的形式去写,一个 batch 是 1 万条数据。
- map task 执行完成后,会将这些磁盘小文件合并成一个大的磁盘文件,同时生成一个索引文件。
- reduc task 去 map 端拉取数据的时候,首先解析索引文件,根据索引文件再去拉取对应的数据。
-
总结
产生磁盘小文件的个数:
2 * M (map task 的个数)
3.2 bypass 机制
-
bypass 机制示意图
-
执行流程
执行流程和普通模式基本没有什么区别,只是少了一步排序操作
-
总结
-
bypass 运行机制的触发条件如下:
shuffle reduc task 的数量小于
spark.shuffle.sort.bypassMergeThreahold
的阐述值。默认值 200。 -
产生的磁盘小文件个数:
2 * M (map task 的个数)
。
-
4. Shuffle 文件寻址
4.1 MapOutputTracker
MapOutputTracker
是 Spark 架构中的一个模块,是一个主从架构。管理磁盘小文件的地址。
MapOutputTrackerMaster
是主对象,存在于 Driver 中。MapOutputTrackerWorker
是从对象,存在于 Excutor 中。
4.2 BlockManager
BlockManager 块管理者,是 Spark 架构中的一个模块,也是一个主从架构。
-
BlockManagerMaster
,主对象,存在于 Driver 中。BlockManagerMaster
会在集群中用到广播变量和缓存数据或者删除缓存数据的时候,通知BlockManagerSlave
传输或者删除数据。 -
BlockManagerSlave
,从对象,存在于 Executor 中。BlockManagerSlave
会与BlockManagerMaster
之间通信。
无论在 Driver 端的 BlockManagerMaster
还是在 Executor 端的 BlockManagerSlave
都含有三个对象:
- DiskStore:负责磁盘的管理。
- MemoryStore:负责内存的管理。
- BlockTransferService:负责数据的传输。
4.3 Shuffle 文件寻址
如图所示,shuffle 文件寻址流程:
- 当 map task 执行完成后,会将 task 的执行情况和磁盘小文件的地址封装到
MapStatus
对象中,通过MapOutputTrackerWorker
对象向 Driver 中的MapOutputTrackerMaster
汇报。 - 在所有的 map task 执行完毕后,Driver 就掌握了所有的磁盘小文件的地址。
- 在 reduce task 执行之前,会通过 Executor 中
MapOutputTrackerWorker
向 Driver 端的MapOutputTrackerMaster
获取磁盘小文件的地址。 - 获取到磁盘小文件地址后,会通过
BlockManager
连接数据所在节点,然后通过BlockTrasferService
进行数据的传输。 BlockTransferService
默认启动 5 个 task 去节点拉取数据。默认情况下,5 个 task 拉取数据量不能超过 48M。
5. Shuffle 优化
5.1 调优参数
-
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 操作的作业,建议增加重试最大次数(比如 5次),以避免由于JVM的full gc或者网络不稳定等因素导致的数据拉取失败。在实践中发现,对于针对超大数据量(数十亿~上百亿)的 shuffle 过程,调节该参数可以大幅度提升稳定性。
shuffle file not find taskScheduler不负责重试task,由DAGScheduler负责重试stage
-
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|hash
参数说明:该参数用于设置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----针对SortShuffle
默认值:200
参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作,而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。
调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuffle read task的数量。那么此时就会自动启用bypass机制,map-side就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此shuffle write性能有待提高。
-
spark.shuffle.consolidateFiles----针对HashShuffle
默认值:false
参数说明:如果使用HashShuffleManager,该参数有效。如果设置为true,那么就会开启consolidate机制,会大幅度合并shuffle write的输出文件,对于shuffle read task数量特别多的情况下,这种方法可以极大地减少磁盘IO开销,提升性能。
调优建议:如果的确不需要SortShuffleManager的排序机制,那么除了使用bypass机制,还可以尝试将spark.shffle.manager参数手动指定为hash,使用HashShuffleManager,同时开启consolidate机制。在实践中尝试过,发现其性能比开启了bypass机制的SortShuffleManager要高出10%~30%。
5.2 调优方式
-
代码调优
sparkConf.set("spark.shuffle.file.buffer","64K")
这种方式优先级最高,但是由于硬编码,调优方式不灵活,需要修改代码才生效,不推荐。
-
配置文件调优
修改 conf/spark-defaults.conf文件
spark.shuffle.file.buffer=64k
这种方式优先级第三,用于所有的 spark 程序。
-
启动命令调优
./spark-submit --conf spark.shuffle.file.buffer=64 --conf spark.reducer.maxSizeInFlight=96
这种方式优先级第二,可以随程序运行修改调优参数,比较灵活,建议使用这种方式。