[Spark]Shuffle

Spark丰富了任务类型,有些任务之间数据流转不需要通过Shuffle,但是有些任务之间还是需要通过Shuffle来传递数据,比如wide dependency的group by key。

为了方便理解,在Shuffle操作中,我们称负责分发数据的Executor叫做Mapper,而接收数据的Executor叫做Reducer

参考资料: Spark架构-Shuffle(译)

Hash Shuffle

(spark 1.2以前的默认shuffle)

[Spark]Shuffle_第1张图片
首先要知道分片,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会复用申请到的文件,往里面输出。
[Spark]Shuffle_第2张图片

数量分析:

假设集群中有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

Spark 1.2 以后的默认shuffle
[Spark]Shuffle_第3张图片

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存放数据

关于AppendOnlyMap

这是Scala自定义的HashMap,只允许添加数据,不允许删除(存在内存隐患,后来改为ExternalAppendOnlyMap)

在这AppendOnlyMap中,每个key对应一个val的数组,当发生spill溢写或者没有后续的Map数据了的时候,才会对数组进行排序(TimSort)

多次溢写会产生多个文件,这些文件只有在被reduce fetch的时候才会进行归并,而归并的方法与MR不同,而是使用了最小堆(java的优先队列)进行归并.

优点
  • Map端创建了更少的文件
  • 随机读写较少,顺序读写更多
缺点
  • 排序比哈希要慢,所以需要不断调试找到合适的bypassMergeThreshold
  • 如果使用SSD,HashShuffle会更好

你可能感兴趣的:(spark)