Spark Shuffle
Shuffle
简介
Shuffle
(数据混洗)是将一组无规则的数据转换为一组有规则的数据。Spark
是一个分布式计算引擎,大多数的计算和数据转换过程是在多台计算机上执行的,当我们对RDD
进行规约操作时,例如reduceByKey
,或者当两个RDD
之间是宽依赖的关系时,都会产生Shuffle
。
Shuffle
实现方案
Shuffle
过程中会导致RDD
进行重分区,在数据量情况较大情况下也会将数据序列到磁盘上,所以整个Shuffle
过程会有大量的网络通信和磁盘IO
,Shuffle
的效率高低成为了Spark
计算效率高低的关键。
Hash Shuffle
-
在
Spark1.6
之前使用的Hash Shuffle
-
Hash Shuffle
原理图
- 过程解析
每一个MapTask
会将数据按照key
值进行hash
然后按照ReduceTask
需要的分区数进行分为指定个数的bucket
,这里的bucket
是内存缓存区,默认大小为100M
,MapTask
将数据平均的分布到每个bucket
,当每个bucket
快满了时(0.8),将bucket
内存中的数据写入到blockFile
中,最终所有的数据都写入blockFile
中,这里存在的性能问题是将产生大量的临时文件,临时文件个数为M*R
,会产生大量的磁盘IO
请求操作,效率低下。
- 过程解析
考虑到多个临时文件导致磁盘IO
请求过多,产生效率问题。Hash Shuffle
进行了优化。之前每个Executor
中并行着多个Task
任务,临时文件个数为M*R
,现在考虑将每个Executor
上执行的多个Task
的结果写入到一个文件中,这样临时文件的个数为E*R
,这个数量级比之前比小了并发Task数量
倍数。但是如果Executor
节点过多,效率还是不高。
Sort Based Shuffle
Sort Based Shuffle
原理图
- 过程解析
为了解决每个MapTask
任务对应多个ReduceTask
导致生成多个临时小文件的问题,Sort Based Shuffle
引入了索引文件的概念。将每一个MapTask
生成的对应每个分区的小bucket
先合并成一个大的data
文件,在data
文件中采用segment(分段)
的方式去记录中间数据,每个data文件
对应一个index索引
文件,用于标识每个分区起始位置和结束位置。磁盘io
请求操作从变成了M
数量级。
- 策略简介
1、SortShuffleWriter
SortShuffleWriter
在将数据从临时bucket
转化为大的data
文件时会进行key
值hash
后排序(堆排),并对相同的key
的value
值合并,这样生成的大data
文件是一个有序的大文件。像reduceByKey
这样的操作就使用这样的策略。
2、ByPassMergeSortShuffleWriter
ByPassMergeSortShuffleWriter
在将数据从临时bucket
文件转化为大的data
文件时,不进行按照key
值排序,也不会将相同key
的value
值进行合并。生成的data
文件内部按照reduceId
进行标识分段。因为这样写不在乎顺序所以都是并发写,但是当ReduceTask
分区数过多时,会导致占用过多内存。这种混洗方式适用于ReduceTask
数量不多(小于默认值200
)并且不需要排序和map
端聚合的任务效率更高。
3、UnasfeSortShuffleWriter
UnsafeShuffleWriter
里面维护着一个 ShuffleExternalSorter
, 用来做外部排序, 外部排序就是要先部分排序数据并把数据输出到磁盘,然后最后再进行merge
全局排序。
和SortShuffleWriter
的区别:
区别 | UnsafeShuffleWriter | SortShuffleWriter |
---|---|---|
排序方式 | 最终只是 partition 级别的排序 | 先 partition 排序,相同分区key 有序 |
aggregation | 没有反序列化,没有aggregation | 支持 aggregation |
- 策略使用条件
1.在数据量小,Reduce分区数<=spark.shuffle.sort.by pass Merge Threshold( 默认200 ) 并且任务中没有Map端聚合操作时,采用类似Hase Based Shuffle的实现方式,不会对数据进行sort和merge,而是直接将每个Map Task的数据按Key值得到Reduce分区ID,然后每个分区生成一个DiskObjectWriter将该条数据Append到这个MapTask对应分区的临时文件里面,不同的是最后会对MapTask生成的临时文件进行合并,每个MapTask生成一个数据文件和一个索引文件。
2.当对象的序列化方式支持Recolation并且Reduce分区数小于2^24时,采用UnsafeSortshuffle,这种方式省去了将接收到的数据进行排序时要经历的反序列化然后再序列化的过程,直接将二进制数据写入MemoryBlock,同时将该数据的Reduce分区Id以及数据地址写入inMemorySorter的指针数组,排序时只对指针数组排序,spill时根据地址指针去获取数据。
3.最后一种SortShufleWriter,当Shuffle不满足上述两种条件时采用。内部采用ExternalSorter来溢写数据。其中,当没有map端聚合时,ExternalSorter内部采用PartitionedPairBuffer来接纳数据,这个时候即使job输出指定了排序逻辑,在shuffle write时并不会预排序,PartitionedPairBuffer的结构特点使得数据插入后是分区Id有序的,达到溢写磁盘条件时直接将Buffer内数据写出;当map端有聚合时,会采用PartitionedAppendOnlyMap来接纳数据,可以对key值进行跟新来完成聚合操作,并且这个时候job输出指定了排序逻辑的话,spill的时候数据会按照(Id,Key)的二元组进行排序。
逻辑摘抄自:
https://mp.weixin.qq.com/s?__biz=MzU3NTY3MTQzMg==&mid=100000768&idx=1&sn=0a5905b9f5567ea94f568120f1245c3c&chksm=7d1ed9e74a6950f1ad956d462a50c093ada8d588c5b499ca5a4501aacf3dcee1c88ae5d296a0&mpshare=1&scene=23&srcid=0214mymTPMCIplOiR6gfWO6U#rd
Shuffle
中的数据倾斜
- 产生原因
在进行shuffle
的时候,必须将各个节点上相同的key
拉取到某个节点上的一个task
来进行处理,此时如果某个key
对应的数据量特别大的话,就会发生数据倾斜。
- 解决方案
1、先局部再整体
先给每个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)
,再次进行全局聚合操作。将产生倾斜的数据分解,数据均匀分布到多个节点上,
2、替代join
将较小RDD
中的数据直接通过collect
拉取到Driver
端的内存中来,然后对其创建一个Broadcast
变量,接着对另外一个RDD
执行map
操作。