Spark Shuffle 解析-Hash Shuffle和Sort Shuffle

概述

Spark Shuffle分为Hash Shuffle和Sort Shuffle。
Hash Shuffle是Spark 1.2之前的默认Shuffle实现,并在Spark 2.0版本中被移除。因此,了解Hash Shuffle的意义更多的在于和Sort Shuffle对比,以及理解为什么Sort Shuffle能够完全取代Hash Shuffle。
Spark 1.2起默认使用Sort Shuffle,并且Sort Shuffle在map端有三种实现,分别是UnsafeShuffleWriter、BypassMergeSortShuffleWriter、SortShuffleWriter,根据运行时信息自动选择对应的实现。

Hash Shuffle

我们将 map 端划分数据、持久化数据的过程称为 shuffle write,而将 reducer 读入数据、aggregate 数据的过程称为 shuffle read。

shuffle write

shuffle write 的任务很简单:将数据 partition 好,并持久化。持久化有多个目的:

  1. 减少内存存储空间压力。
  2. 允许稍晚执行reduce阶段的某些任务,比如没有足够多的executor同时执行分配任务,则数据已经持久化到磁盘中,可以稍晚些执行。
  3. 进行容错,即错误发生时,允许计算引擎仅重新执行reduce任务而不必重新启动所有的输入任务。
  4. 进行缓存,在已经执行过shuffle操作的数据上运行新的job(每一个action触发一个job,shuffle文件只会在一个application结束之后才会删除。Spark外部shuffle服务甚至可以运行用户终止进程而不删除shuffle文件),并不会运行shuffle操作之间的任务。由于shuffle文件已经写入磁盘,则可以执行使用shuffle文件进行操作,而不用重新运行之前的阶段。
    在这里插入图片描述
    但是需要注意的是在SparkSQL中不知道什么原因,这种优化失去了效果,具体原因还未进行探究。很有可能是跟SparkSQL执行计划的优化有关。

shuffle write 的处理逻辑加入到 ShuffleMapStage(ShuffleMapTask 所在的 stage) 的最后,该 stage 的 final RDD 每输出一个 record 就将其 partition 并持久化。
在这里插入图片描述
ShuffleMapTask在将数据写入磁盘之前,会先写入内存缓冲区,缓冲区被称为bucket,其大小为spark.shuffle.file.buffer.kb ,默认是 32KB(Spark 1.1 版本以前是 100KB)。每个 ShuffleMapTask 包含 R 个缓冲区,R = reducer 个数(也就是下一个 stage 中 task 的个数)。Task具体将数据写入哪一个bucket,由partitioner.partition(record.getKey()))决定,即对数据进行分区。当缓冲区溢出时,将数据刷写到磁盘上,一个bucket形成一个ShuffleBlockFile。

由于每次shuffle使用MR个bucket(M为MapTask个数,R为ReduceTask个数),会导致产生MR个shuffle文件,文件数量太多,所以Spark提出了优化的Consolidate Shuffle
在这里插入图片描述
Consolidate Shuffle 引入了shuffleFileGroup的概念,每个shuffleFileGroup都对应一批shuffle文件。shuffle文件数量与reduceTask数量相同(即与bucket数量相同)。只有在core上第一批执行的ShuffleMapTasks会创建一个shuffleFIleGroup,将数据写入到对应shuffle文件。

在该 core 上后续执行的 ShuffleMapTasks 会复用shuffleFIleGroup和shuffle文件,即数据会继续写入到已有的shuffle文件。该机制会允许同个core上不同task复用同一个shuffle文件,对于多个task进行了一定程度的合并。这样,每次shuffle产生的文件数为C * R(C为spark集群的Core Number)。Consolidate Shuffle 功能可以通过spark.shuffle.consolidateFiles=true来开启。

ShuffleMapTask在将数据写入bucket之前,根据mapSideCombine参数决定是否对数据进行combine操作,即map端的局部聚合。如果mapSideCombine为True,且指定了聚合函数,则会对数据先进行combine操作,再写入bucket。

Shuffle read

shuffle read首先进行fetch操作,将shuffle文件fetch到本地机器上。 fetch 来的 ShuffleFile 要先在内存做缓冲,Spark 规定这个缓冲界限不能超过 spark.reducer.maxMbInFlight,这里用 softBuffer(这里使用sortBuffer并不代表会排序) 表示,默认大小为 48MB。

如果定义了聚集操作,通过聚集函数对fetch到缓冲区中的数据进行aggregator聚集操作,并且是边fetch边aggregator。Spark的聚集aggregator方式分为两种:只使用内存(必须保证有足够内存)和内存+磁盘。

  1. 如果spark.shuffle.spill = false就只用内存。内存使用的是AppendOnlyMap ,类似 Java 的HashMap。从缓冲中 deserialize 出来一个 <Key, Value> record,直接将其放进 HashMap 里面。如果该 HashMap 已经存在相应的 Key,那么直接进行 aggregate 操作,aggregate(hashMap.get(Key), Value),所以聚合操作必须是 commulative的。若在Map中没有查找到,则插入其中。
  2. 内存+磁盘使用的是ExternalAppendOnlyMap,其持有一个 AppendOnlyMap,开始过程与只使用内存模式相同,但是如果 AppendOnlyMap 快被装满时检查一下内存剩余空间是否可以够扩展,够就直接在内存中扩展。如果内存空间不足,ExternalAppendOnlyMap 将AppendOnlyMap进行排序(此排序按照key.hashcode进行排序,排序是为了之后的merge-aggregate过程,如果不排序,无法将多个文件merge)后 spill 到磁盘上生成spilledMap 文件,再重新 new 出来一个 AppendOnlyMap重复上述操作。当所有记录处理完毕之后,先对内存中的AppendOnlyMap进行排序,然后在对AppendOnlyMap和所有的spilledMap 文件进行全局 merge-aggregate。
    在这里插入图片描述

然后再根据dep.keyOrdering判断是否需要进行数据排序,如果reduce端需要进行排序,会创建ExternalSorter(传入ordering= Some(keyOrd),但是aggregator = None),将所有数据插入ExternalSorter对数据进行排序,原理与map端ExternalSorter排序相同。

Sort Shuffle

Sort Shuffle在map端有三种实现,分别是UnsafeShuffleWriter、BypassMergeSortShuffleWriter、SortShuffleWriter。
在这里插入图片描述
注:上面SortShuffleWriter中提到的Partition,不是RDD中的Partition,而是类似Spark Hash Shuffle中的bucket,如果没有单独说明,Sort Shuffle相关文章中的Partition均为bucket,和源码中的变量名保持一致。

运行时三种ShuffleWriter实现的选择

Spark根据运行时信息选择三种ShuffleWriter实现中的一种。
在这里插入图片描述

  1. 没有map端聚合操作,且RDD的Partition数小于200,使用BypassMergeSortShuffleWriter。
  2. 没有map端聚合操作,RDD的Partition数小于16777216,且Serializer支持relocation,使用UnsafeShuffleWriter。
  3. 上述条件都不满足,使用SortShuffleWriter。

Serializer支持relocation

上面提到UnsafeShuffleWriter需要Serializer支持relocation,Serializer支持relocation是指,Serializer可以对已经序列化的对象进行排序,这种排序起到的效果和先对数据排序再序列化一致。Serializer的这个属性会在UnsafeShuffleWriter进行排序时用到。支持relocation的Serializer是KryoSerializer,Spark默认使用JavaSerializer,通过参数spark.serializer设置。

BypassMergeSortShuffleWriter

BypassMergeSortShuffleWriter和Hash Shuffle中的HashShuffleWriter实现基本一致,唯一的区别在于,map端的多个输出文件会被汇总为一个文件,如下 。
在这里插入图片描述

map端结果按照bucket顺序依次写入磁盘文件中,这么处理后,Shuffle生成的文件数显著减少了,同时还会生成indexFile文件,记录各个bucket在dataFile中的位置,用于后续reducer随机读取文件。

UnsafeShuffleWriter

UnsafeShuffleWriter使用ShuffleExternalSorter和ShuffleInMemorySorter两部分,功能如下
在这里插入图片描述
即ShuffleExternalSorter记录原始数据,ShuffleInMemorySorter记录元数据(parititionid和数据位置信息)。

排序:写文件或溢写前根据数据的PartitionId信息,使用TimSort对ShuffleInMemorySorter的long数组排序,排序的结果为,PartitionId相同的聚集在一起,且PartitionId较小的排在前面,ShuffleExternalSorter中的数据不需要处理。
在这里插入图片描述
写数据:依次读取ShuffleInMemorySorter中long数组的元素,再根据page number和offset信息去ShuffleExternalSorter中读取K-V Pair,写入文件,如下
在这里插入图片描述
溢写 & 合并:UnsafeShuffleWriter首先会使用ShuffleExternalSorter在内存中接收数据,当内存数据超过阈值时,溢写数据到磁盘,每次溢写会生成上图中的一个dataFile,如果多次溢写产生多个dataFile,会在map端数据处理结束后进行merge合并为一个dataFile(一个Task生成一个file)。

UnsafeShuffleWriter原理、优势和使用条件解释:https://blog.csdn.net/u011564172/article/details/72764010

SortShuffleWriter

SortShuffleWriter在首先创建ExternalSorter(1、如果mapSideCombine为true,会传入aggregator和keyOrdering;2、否则aggregator和keyOrdering均为None),其包含两个数据结构,即PartitionedAppendOnlyMap和PartitionedPairBuffer。SortShuffleWriter根据shouldCombine (aggregator.isDefined,即有无定义aggregator操作)选择使用Map还是Buffer接收数据。PartitionedAppendOnlyMap可用于combine操作,可以边插入进行combine操作。

随后将所有数据写入ExternalSorter,当达到内存阈值之后,将PartitionedAppendOnlyMap或者PartitionedPairBuffer溢写入磁盘文件中。在溢写操作之前,对数据进行排序,排序规则根据构造ExternalSorter传入的aggregator和keyOrdering而变化,如下:

  • 如果keyOrdering和aggregator都为null,则只使用PartitionID排序;
  • 如果keyOrdering不为null,排序规则按照PartitionID+keyOrdering(对于 aggregator为null,而keyOrdering不为null,通常是被sortbykey的reduceTask使用。sortbykey在map端不会排序,因为构造ExternalSorter时,因为mapSideCombine=false,所以传入aggregator和keyOrdering参数都为null)。
  • 如果keyOrdering为null,aggregator不为null,则使用PartitionID+key.hashcode()进行排序。这种排序称为半排序,A partial ordering means that equal keys have comparator.compare(k, k) = 0, but some
    non-equal keys also have this, so we need to do a later pass to find truly equal keys。hascode相同的key,值并不一定相同,所以在合并溢写文件时,进行额外处理,区别hascode相同,但key值不相同的数据。

每次溢生成一个磁盘文件,溢写通过缓冲的形式,每写入serializerBatchSize条记录,调用flush()函数,强制刷写到磁盘。

当全部数据到写入ExternalSorter之后,将所有数据写入同一个磁盘文件。

  • 若此时没有溢写文件,则将ExternalSorter内存中的数据按照上述方法排序后写入一个磁盘文件。
  • 若此时有若干溢写文件,则先将ExternalSorter内存中的数据进行排序,然后和所有溢写文件进行merger-sort,最终生成一个磁盘文件。
  • 再写入最终的磁盘文件时,每次将属于一个分区(分发到一个ResultTask的数据)的所有记录写入后(因为是按照PartitionID+keyOrdering排序的,所以属于一个分区的数据肯定在一块),会调用writer.commitAndGet()获取分区的segment信息。将所有segment的长度记录在数组中,最后生成索引文件,标识各个分区在磁盘文件中的位置,start offset和end offset。

SortShuffleWriter源码理解:https://blog.csdn.net/qq_26222859/article/details/81562272

在这里插入图片描述

参考:
https://blog.csdn.net/u011564172/article/details/72763978
https://github.com/JerryLead/SparkInternals/blob/master/markdown/4-shuffleDetails.md
https://blog.csdn.net/u011564172/article/details/72763978
https://blog.csdn.net/u011564172/article/details/72764010
https://blog.csdn.net/u011564172/article/details/71170234
https://blog.csdn.net/weixin_39216383/article/details/81194498
https://blog.csdn.net/qq_26222859/article/details/81562272

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值