这是我参与「第四届青训营 」笔记创作活动的第4天
1.概述
在04年谷歌发布的《MapReduce:Simplified Data Processing on Large Clusters》论文中,提到了shuffle的概念。
1.1 一个经典的MapReduce过程
- Map:在单机上,对一小份数据并行进行处理
- Shuffle:在map的基础上,将数据进行分区、排序、合并后形成有一定规律的数据,为后续的reduce阶段做准备
- Reduce:汇总整理的阶段,对移动后的数据进行处理,依然是在单机上处理一小份数据
1.2 shuffle的对性能的影响
- M*R次网络连接、大量的数据移动、数据丢失风险、可能存在大量的排序操作、大量的数据序列化、反序列化操作、数据压缩
1.3 总结
数据shuffle表示了不同分区数据交换的过程,不同的shuffle策略性能差异较大。目前在各个引擎中shuffle都是优化的重点,在spark框架中,shuffle是支撑spark进行大规模复杂数据处理的基石,shuffle是性能优化的重点。
2. shuffle算子
常见的触发shuffle的算子
算子使用例子
val text = sc.textFile("mytextfile.txt")
val counts = text
.flatMap(line => line.split(" "))
.map(word => (word,1))
.reduceByKey(_+_)
counts.collect
Shuffle Dependency
- 宽依赖:父RDD的每个分片至多被子RDD中的一个分片所依赖
- 窄依赖:父RDD的每个分片可能被子RDD中的多个分片所依赖
创建会产生shuffle的RDD时,RDD会创建Shuffle Dependency来描述Shuffle相关的信息。构造函数中有以下步骤:
-
Partitioner——用来将record映射到具体的partition的方法
- numberPartitions
- getPartition
-
Aggregator——在map侧合并部分record的函数
- createCombiner:只有一个value的时候初始化的方法
- mergeValue:合并一个value到Aggregator中
- mergeCombiners:合并两个Aggregator
3.shuffle过程
3.1 spark中的shuffle变迁过程
-
HashShuffle 0.8之前,2.0退出
- 优点:不需要排序
- 缺点:因为每个partition会映射到一个文件,同时打开,创建的文件过多
- 优化:写数据优化,使得每个partition都映射到一个文件片段
-
SortShuffle 1.1引入,1.2默认
- 优点:打开的文件少、支持map-side combine
- 缺点:需要排序
- 做法:每个task生成一个包含所有partition数据的文件
-
TungstenSortShuffle 1.4引入
- 优点:更快的排序效率,更高的内存利用效率
- 缺点:不支持map-side combine
3.2 shuffle注册
-
由action算子触发DAG Scheduler进行shuffle register
-
Shuffle Register会根据不同的条件决定注册不同的ShuffleHandle
-
不支持mapside combine && partitions小于 spark.shuffle.sort.bypassmerge Threshold——BypassMergeSortShuffleHandle
- 不支持mapside combine && serialize支持relocation&& partition小于 224——SerializedShuffleHandle
- !(不支持mapside combine && serialize支持relocation&& partition小于 224)——BaseShuffleHandle
-
3.3 Writer实现
-
三种ShuffleHandle对应了三种不同的ShuffleWriter的实现
-
BypassMergeSortShuffleWriter:HashShuffle
- 不需要排序,节省时间,写操作的时候会打开大量的文件
-
UnsafeShuffleWriter:TunstonShuffle
- 利用类似内存页存储序列化数据,数据写入后不再反序列化
- 只根据partition排序Long Array,数据不移动
-
SortSHuffleWriter:SortShuffle
- 支持combine,需要combine时,使用PartitionedAppendOnlyMap,本质是个HashTable;不需要combine的时候PartitionedPairBuffer本质是个Array
-
3.4 Reader实现
-
ShuffleReader网络请求流程
使用netty作为网络框架提供网络服务,并接受reducetask的fetch请求
首先发起openBlocks请求获得streamId,然后再处理stream或者chunk请求
位置信息记录在MapOutputTracker中
Reader实现:ShuffleBlockFetchIterator
-
区分local和remote节省网络消耗
-
防止OOM
- maxBytesInFlight:限制获取数据块的大小
- maxReqsInFlight:限制获取数据的数量
- maxBlocksInFlightPerAddresst:限制每一个地址上获取block的数量
- maxReqSizeShuffleToMemt:限制内存中的请求大小
- maxAttemptsOnNettyOOM:限制OOM的次数
Reader实现:External Shuffle Service——解耦数据计算和服务
为了解决Executor为了服务数据的fetch请求导致无法退出问题,我们在每个节点上部署一个External Shuffle Service,这样产生数据的Executor在不需要继续处理任务时,可以随意退出。
3.5 shuffle优化使用的技术
3.5.1 零拷贝
DMA:直接存储器存取,指外部设备不通过cpu而直接与系统内存交换数据的接口技术
-
零拷贝——Zero Copy
- sendfile+DMA gather copy
-
Netty 零拷贝
- 可堆外内存,避免 JVM 堆内存到堆外内存的数据拷贝。
- CompositeByteBuf 、 Unpooled.wrappedBuffer、 ByteBuf.slice ,可以合并、包装、切分数组,避免发生内存拷贝
- Netty 使用 FileRegion 实现文件传输,FileRegion 底层封装了 FileChannel#transferTo() 方法,可以将文件缓冲区的数据直接传输到目标 Channel,避免内核缓冲区和用户态缓冲区之间的数据拷贝
3.5.2避免shuffle
- 使用broadcast替代join,传统的join操作会导致shuffle操作。
- 使用可以map-side预聚合的算子
3.5.3 Shuffle 参数优化
- spark.default.parallelism && spark.sql.shuffle.partitions
- spark.hadoopRDD.ignoreEmptySplits
- spark.hadoop.mapreduce.input.fileinputformat.split.minsize
- spark.sql.file.maxPartitionBytes
- spark.sql.adaptive.enabled && spark.sql.adaptive.shuffle.targetPostShuffleInputSize
- spark.reducer.maxSizeInFlight
- spark.reducer.maxReqsInFlight spark.reducer.maxBlocksInFlightPerAddress
3.5.4 Shuffle 倾斜优化
- 什么叫倾斜?数据处理量不平均
- 有什么危害?影响处理效率,作业运行时间变长;Task OOM导致作业失败
- 解决倾斜方法举例
- 增大并发度【足够简单,但是只缓解,不根治】
- AQE:根据shuffle文件统计数据自动检测倾斜数据,将那些倾斜分区打散成小的子分区,然后各自进行join
4. Push Shuffle
-
shuffle过程存在哪些问题?
- 数据存储在本地磁盘,没有备份
- IO 并发:大量 RPC 请求(M*R)
- IO 吞吐:随机读、写放大(3X)
- GC 频繁,影响 NodeManager
-
为什么需要push shuffle?
- Avg IO size太小,造成了大量的随机IO,严重影响磁盘的吞吐量
- M*R次读请求,造成大量网络连接,影响稳定性
-
为了优化该问题,有很多公司都做了思路相近的优化,push shuffle
- Facebook: cosco
- LinkedIn:magnet 两份备份数据
- Uber:Zeus
- Alibaba: RSS
- Tencent: FireStorm
- Bytedance: Cloud Shuffle Service 高可用重试
- Spark3.2: push based shuffle
参考