Spark丰富了任务类型,有些任务之间数据流转不需要通过Shuffle,但是有些任务之间还是需要通过Shuffle来传递数据,比如wide dependency的group by key。
为了方便理解,在Shuffle操作中,我们称负责分发数据的Executor叫做Mapper
,而接收数据的Executor叫做Reducer
参考资料: Spark架构-Shuffle(译)
Hash Shuffle
(spark 1.2以前的默认shuffle)
首先要知道分片,Task个数,Executor个数他们直接有什么关系,具体可参考下文:
扩展资料:关于Spark中Task,Partition,RDD、节点数、Executor数、core数目的关系
简单来说,就是MapTask的数量=数据的分片数,每个Task会分配给某个WorkNode上的某个Executor执行,每个Executor包含多个Core (可以理解为工作线程),每个Core最多只能执行一个Task,每个Task执行的结果就是生成了目标RDD的一个partiton
(设集群共有E
个Executor,每个executor都有C
个cores(spark.executor.cores
or –executor-cores
for YARN) ,每个Task需要T
个CPU(spark.task.cpus
),那集群中能执行的Task为E * C / T
)
1) 每一个Mapper创建出和Reducer数目相同的bucket,bucket实际上是一个buffer,其大小为spark.shuffle.file.buffer.kb(默认32KB)。
2) Mapper产生的结果会根据设置的partition算法填充到每个bucket中去,然后再写入到磁盘文件。
3) Reducer从远端或是本地的block manager中找到相应的文件读取数据。
Map创建的bucket其实对应磁盘上的一个文件,Map的结果写到每个bucket中其实就是写到那个磁盘文件中,这个文件也被称为blockFile
这样会产生一个问题,Core中的每一个Task都会生成一个blockFile,导致产生大量的小文件
数量分析:
假设有`M`个Mapper,有`N`个Reducer,
那集群中就会为Shuffle创建`M * R`个文件
为了解决上面的那个问题,有一个优化的选项spark.shuffle.consolidateFiles
(默认是false)。如果设置成true,则不会给每个Reducer创建一个文件,而是会创建一个文件池。当一个Map Task要开始输出数据时,它会从这个池中请求R个文件的group。当MapTask输出完成后,Map会归还该group给文件池。
简单来说就是同一个core内的Task会复用申请到的文件,往里面输出。
数量分析:
假设集群中有E个executors
每个executor都有C个cores
每个Task需要T个CPU
每个Executor会创建C / T个group
那集群中能并行执行的Task为E * C / T
每个group包含R个文件
总共要创建的文件数量是E * (C / T) * R
优点
- 非常快速,不需要排序,也不需要维护哈希表
- 没有数据排序的内存开销
- 没有额外的IO开销,数据只被读写一次
缺点
- 当分区很多时,由于大量输出文件,性能开始下降
- 当大量文件被写到文件系统时,会产生大量的Random IO。而Random IO是最慢的,比Sequential IO要慢100倍
如果Reducer不在乎Record的顺序,那么Reducer只会拿到它依赖的Mapper的的一个iterator。但是,如果在乎顺序,那么会拿到全部数据,并通过ExternalSorter在Reducer端做一次排序。
Sort Shuffle
Sort Shuffle的思想
Sort Shuffle 不为每个reduce任务单独创建一个输出文件,而是每个MapTask只有一个输出文件,这个文件内部是根据reduce id排序,并且有一个额外的索引文件,当后续reduce需要取文件时,只需要经过一次fseek
和一次fread
即可拿到文件
显而易见,当reduce较少时,输出到不同的文件再合并,效率要高于维护一个有序队列,因此,可以通过设置spark.shuffle.sort.bypassMergeThreshold
,当reduce个数低于该阈值时,使用BypassMergeSortShuffleWriter
,MapTask将会把数据Hash到不同的文件,最后再合并成一个大文件。
要注意的是,Sort Shuffle只在Map端对数据排序,但在reduce端并不会对这个排序结果进行归并,如果需要数据有序,则会重新排序
溢写
那如果Reducer并没有足够的内存,放不下全部Mapper发过来的数据,这时候就需要将中间数据刷到磁盘上了。spark.shuffle.spill
这个参数决定是否将中间结果刷到磁盘上,默认是开启的。如果你关闭了这个选项,如果Reducer没有足够的内存了,那就会OOM
内存中数据的存放
Map端通过AppendOnlyMap
存放数据
这是Scala自定义的HashMap,只允许添加数据,不允许删除(存在内存隐患,后来改为ExternalAppendOnlyMap
)
在这AppendOnlyMap中,每个key对应一个val的数组,当发生spill溢写或者没有后续的Map数据了的时候,才会对数组进行排序(TimSort)
多次溢写会产生多个文件,这些文件只有在被reduce fetch的时候才会进行归并,而归并的方法与MR不同,而是使用了最小堆(java的优先队列)进行归并.
优点
- Map端创建了更少的文件
- 随机读写较少,顺序读写更多
缺点
- 排序比哈希要慢,所以需要不断调试找到合适的
bypassMergeThreshold
- 如果使用SSD,HashShuffle会更好