【Spark】Shuffle详解

一、概要

1、Shuffle调优概述

Spark作业性能主要消耗在Shuffle环境,因为其中包含大量磁盘IO、序列化、网络数据传输等操作,如果想提升作业性能,有必要对Shuffle过程进行调优。但也要注意,影响Spark作业性能因素主要还是代码开发、资源参数以及数据倾斜,Shuffle调优只占一小部分,不要舍本逐末。

2、Shuffle发生阶段

1

3、触发 Shuffle 操作的算子

分类 操作
Repartition相关 repartition、coalesce、repartitionAndSortWithinPartitions
*ByKey操作(除了countByKey) groupByKey,reduceByKey,combineByKey、aggregateByKey、sortBeyKey
Join相关 cogroup,join

二、发展演进

Spark版本 Shuffle演进
<=0.8 Hash Based Shuffle
0.8.1 为 Hash Based Shuffle引入File Consolidation机制
0.9 引入 ExternalAppendOnlyMap
1.1 引入 Sort Based Shuffle,但默认仍为 Hash Based Shuffle
1.2 默认的 Shuffle 方式改为 Sort Based Shuffle
1.4 引入 Tungsten-Sort Based Shuffle
1.6 Tungsten-Sort Based Shuffle 并入 Sort Based Shuffle
2.0 Hash Based Shuffle 退出历史舞台

0、MapReduce中Shuffle

1

注:后续Spark Shuffle都以MapReduce Shuffle为参考,下面的Map Task就是指Shuffle Write阶段,Reduce Task指Shuffle Read阶段

1、Hash Shuffle V1

相比传统MR,Spark假定大多数Shuffle数据无需排序,如word count,因此不在Shuffle Read时做Merge Sort,需要合并操作会用聚合(aggregator),即用一个HashMap(实际是AppendOnlyMap)来将数据进行合并。

Map Task按Hash方式重组partition数据,每个Map Task为每个Reduce Task生成一个文件,通常会产生大量文件,伴随大量随机磁盘IO操作和内存开销。
3

问题:
1、Map Task:生成大量文件,消耗内存,IO操作低效。
2、Reduce Task:合并操作把数据放到一个HashMap,容易引发OOM。

(M个Map Task, R个Reduce Task,会有M*R个中间文件,4个Map Task,4个Reducer,会产生16个小文件)

2、Hash Shuffle V2

针对问题1-生成大量文件,Spark引入File Consolidation机制。
一个Executor上所有Map Task生成分区文件只有一份,即将所有Map Task相同分区文件合并,这样每个Executor上最多生成N个分区文件。
4

这样减少了文件数,但如果下游Stage分区数N很大,一个Executor有K个Core,就会开K*N个Writer Handler,仍然容易导致OOM。
4个Map Task分两批运行在2个core上,产生8个小文件。

3、Sort Shuffle V1

为了更好地解决问题1-生成大量文件,Spark参考MR中Shuffle处理方式,引入基于排序的Shuffle写操作机制。
每个Task不会为后续每个Task创建单独文件,而是将所有结果写入同一个文件。文件中的积累按照Partition Id排序,每个Partition内部再按照Key排序,Map Task运行期间会顺序写每个Partition数据,同时生成一个索引文件记录每个Partition的大小和偏移量。
Reduce阶段,Reduce Task拉取数据做Combine时不再采用HashMap,而是采用ExternalAppendOnlyMap,该数据结果做Combine时,如果内存不足,会刷写磁盘,很大程度上保证了鲁棒性,避免了大多数OOM。
总体来看,Sort Shuffle解决了Hash Shuffle所有弊端,但因为Shuffle过程需要对记录排序,性能上有所损失。

4、Tungsten-Sort Based Shuffle / Unsafe Shuffle

Spark 1.5.0开始了钨丝(Tungsten)计划,以优化内存和CPU使用。由于使用了基于JDK Sun Unsafe API的堆外内存,故Tungsten-Sort Based Shuffle也被称为Unsafe Shuffle。
它将数据记录用二进制方式存储,直接在序列化二进制数据上Sort,而非Java对象,这样可以减少内存使用和GC开销,避免Shuffle过程中频繁序列化和反序列化。排序过程中,提供了cache-efficient sorter,使用8 bytes指针,把排序转化成指针数组排序,极大优化了排序性能。
但Tungsten-Sort Based Shuffle有几个限制,Shuffle阶段不能有aggregate操作,分区数不能超过可编码的最大PartitionId(2^24-1),所有像reduceByKey类有aggregate操作的算子不能使用,会退化采用Sort Shuffle。

5、Sort Shuffle V2

从Spark-1.6.0开始,Sort Shuffle和Tungsten-Sort Based Shuffle全部统一到SortShuffle中。如果检测满足Tungsten-Sort Based Shuffle条件,会自动采用该算法,否则采用Sort Shuffle。
从Spark-2.0.0开始,Spark把Hash Shuffle移除,可以说目前Spark-2.0.0只有Sort Shuffle。

三、Writer三种运行模式

Spark 2.3中唯一支持方式是SortShuffleManager,SortShuffleManager定义了writer和reader对应Shuffle的map和reduce阶段。Writer有三种运行模式 — BypassMergeSortShuffleWriter、SortShuffleWriter、UnsafeShuffleWriter
5

1、BypassMergeSortShuffleWriter

触发条件:
1)Shuffle reduce task(即partition)数量小于
spark.Shuffle.sort.bypassMergeThreshold参数的值。
2)没有map side aggregations
注:map side aggregations指map端聚合操作,通常聚合类算子都有map端aggregation。但mapSideCombine设定为false,groupByKey和combineByKey就不会有map side aggregations。

整体处理流程:
6

每个task会为每一个下游reduce task创建一个临时文件(图中下游reduce task应该有3个,这张图引用的是美团技术博客,不改了),将key通过hash存入临时文件,因为写入磁盘文件是通过Java的BufferedOutputStream实现。首先会将数据缓存在内存中,当内存缓冲溢满后再写入磁盘文件,这样可以减少磁盘IO,提升性能。最后会将所有临时文件合并成一个磁盘文件,并创建一个索引文件标识下游各个reduce task的数据在文件中的start offset和end offset。
过程中磁盘写机制其实与未经优化的HashShuffleManager一样,也会创建很多临时文件(所以有reduce task数量限制),只是最后会做一个磁盘文件合并,对于Shuffle reader会更友好些。

2、SortShuffleWriter

该writer思想照抄了MapReduce的Shuffle。
7

该模式下,数据首先写入一个内存数据结构,此时根据不同Shuffle算子,可能选用不同数据结构,如果是reduceByKey这种聚合类Shuffle算子,那会选用Map,一边通过Map聚合,一边写入内存;如果是join类普通Shuffle算子,那会选用Array数据结构,直接写入内存。
接着每写一条数据进入内存数据结构,就会判断下,是否临界阈值,达到的话,就会尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。
在溢写磁盘文件前,会先根据key对内存结构中已有数据进行排序。排序后,会分批将数据写入磁盘文件,默认batch数量是10000条,也就是说,排好的数据,会以每批一万条数据的形式分批写入磁盘文件。
一个task将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作,也会产生多个临时文件。最后会将之前所有临时磁盘文件进行合并,这就是merge过程,此时会将之前所有临时磁盘文件数读取出来,然后依次写入最终磁盘文件中。此外,由于一个task就只对应一个磁盘文件,也就意味着该task为下游stage的task准备的数据都在这一个文件中,因此还会单独写一份索引文件,其中表示下游各个task数据在文件中start offset和end offset。

BypassMergeSortShuffleWriter VS SortShuffleWriter:
1)磁盘写机制不同 2)BypassMergeSortShuffleWriter不会进行排序。

3、UnsafeShuffleWriter:

触发条件:
1)Serializer支持relocation,这里指Serializer可以对已序列化的对象进行排序,排序起到的效果和先对数据排序再序列化一致。
2)没有map side aggregations
3)Shuffle reduce task(即partition)数量不能大于支持的上限(2^24)

UnsafeShuffleWriter将record序列化后插入sorter,然后对已经序列化的record进行排序,并在排序完成后写入磁盘文件作为spill fill,再将多个spill file合并成一个输出文件。在合并时会基于spill fill的数量和IO compression codec选择最合适的合并策略。

四、Shuffle Read

1、什么时候获取数据。
从Parent Stage的所有ShuffleMapTasks结束后再fetch。

2、边获取边处理,还是一次性获取完再处理
Spark不要求Shuffle后数据全局有序,所以没必要等全部数据Shuffle完成再处理,所以是边fetch边处理。

3、获取来的数据存放到哪里
刚获取FileSegment存放在softBuffer缓冲区,经过处理后,数据放在内存+磁盘上,内存使用AppendOnlyMap,类似Java的HashMap,内存+磁盘使用的是ExternalAppendOnlyMap,如果内存空间不足,ExternalAppendOnlyMap可以将records进行sort后spill到磁盘上,等需要时再归并。

4、怎么获得数据存放位置
通过Driver端的MapOutputTrackerMaster询问ShuffleMapTask输出数据位置。

五、参考

1、Spark Shuffle 详解
2、【Spark】Spark 存储原理–Shuffle 过程
3、彻底搞懂 Spark 的 Shuffle 过程(Shuffle write)
4、Inf-- 深入理解 Spark Shuffle
5、Shuffle 过程
6、Spark性能优化指南——高级篇

猜你喜欢

转载自blog.csdn.net/HeavenDan/article/details/115030431