这是我参与「第四届青训营 」笔记创作活动的第5天
01 Shuffle概述
1.1 MapReduce概述
-
2004年谷歌发布了《MapReduce:Simplified Data Processing on Large Clusters》论文
-
在开源实现的MapReduce中,存在Map、Shuffle、Reduce三个阶段
1.1 Map阶段
√ Map阶段,是在单机上进行的针对一小块数据的计算过程
1.2 Shuffle阶段
√ Shuffle阶段,在map阶段的基础上,进行数据移动,为后续的reduce阶段做准备
1.3 Reduce过程
√ Reduce阶段,对移动后的数据进行处理,依然是在单机上处理一小份数据
1.4 为什么shuffle对性能非常重要
-
M * R次网络连接
-
大量的数据移动
-
数据丢失风险
-
可能存在大量的排序操作
-
大量的数据序列化、反序列化操作
-
数据解压
总结
在大数据场景下,数据shuffle表示了不同分区数据交换的过程,不同的shuffle策略性能差异较大。目前在各个引擎中shuffle都是优化的重点,在spark框架中,shuffle是支撑spark进行大规模复杂数据处理的基石
02 Shuffle算子
2.1 Shuffle算子分类
Spark中会产生shuffle的算子大概可以分为4类
算子应用
-
Spark源码中RDD的单元测试
-
Spark源码中PairRDDFunctions的单元测试
val text = sc.textFile("mytextfile.txt")
val counts = text
.flatMap(line => line.split(""))
.map(word => (word,1))
.reduceByKey(_+_)
counts.collect
2.2 Spark中对shuffle的抽象 - 宽依赖、窄依赖
**窄依赖:**父RDD的每个分片至多被子RDD中的一个分片所依赖
**宽依赖:**父RDD中的分片可能被子RDD中的多个分片所依赖
算子内部的依赖关系
ShuffleDependency
√ CoGroupedRDD
- Cogroup
-
fullOuterJoin、rightOuterJoin、leftOuterJoin
-
join
-
√ ShuffledRDD
-
combineByKeyWithClassTag
-
combineByKey
-
reduceByKey
-
-
Coalesce
-
sortByKey
- sortBy
2.2.1 Shuffle Dependency构造
-
A single key-value pair RDD,i.e.RDD[Product2[K,V]],
-
Partitioner(available as partitioner property),
-
Serializer
-
Optional key ordering(of Scala's scala.math.Ordering type),
-
Optional Aggregator,
-mapSideCombine flag which is disabled(i.e.false) by default
Partitioner
- 两个接口
- numberPartitions
- getPartitioner
abstract class Partitioner extends Serializable{
def numPartitions:Int
def getPartition(key:Any):Int
}
- 经典实现
- HashPartitioner
class HashPartitioner(partitions:Int) extends Partitioner {
require(partitions >= 0, s"Number of partitions ($partitions) cannot be nega...)
def numPartitions:Int = partitions
def getPartition(key:Any):Int = key match {
case null => 0
case _=> Utils.nonNegativeMod(key.hashCode,numPartitions)
}
}
2.2.1 Aggregator
-
createCombiner:只有一个value的时候初始化的方法
-
mergeValue:合并一个value到Aggregator中
-
mergeCombiners:合并两个Aggregator
03 Shuffle过程
Shuffle实现的发展历程
-
Spark 0.8及以前Hash Based Shuffle
-
Spark 0.8.1 为Hash Based Shuffle引入File Consolidation机制
-
Spark 0.9 引入ExternalAppendOnlyMap
-
Spark 1.1 引入Sort Based Shuffle,但默认仍为Hash Based Shufle
-
Spark 1.2 默认的Shuffle方式改为Sort Based Shuffle
-
Spark 1.4 引入Tungsten-Sort Based Shuffle
-
Spark 1.6 Tungsten-Sort Based Shuffle并入Sort Based Shuffle
-
Spark 2.0 Hash Based Shuffle退出历史舞台
3.1 Hash Shuffle - 写数据
每个partition会映射到一个独立的文件
3.1 写数据优化
每个ppartition会映射到一个文件片段
3.2 Sort shuffle:写数据
每个task生成一个包含所有partition数据的文件
3.3 Shuffle - 读数据
每个reduce task分别获取所有map task生成的属于自己的片段
3.4 Shuffle过程的触发流程
val text = sc.textFile("mytextfile.txt")
val counts = text
.flatMap(line => line.split(""))
.map(word => (word,1))
.reduceByKey(_+_)
counts.collect
3.5 Shuffle Handle的创建
Register Shuffle时做的最重要的事情是根据不同条件创建不同的shuffle Handle
3.6 Shuffle Handle与Shuffle Writer的对应关系
3.7 Writer实现 - BypassMergeShuffleWriter
-
不需要排序,节省时间
-
写操作的时候会打开大量文件
-
类似于Hash Shuffle
UnsafeShuffleWriter
-
使用类似内存页储存序列化数据
-
数据写入后不再反序列化
-
只根据partition排序Long Array
-
数据不移动
- 支持combine
-需要combine时,使用PartitionedAppendOnlyMap,本质是个HashTable
- 不需要combine时PartitionedPairBuffer本质是个array
3.8 Reader实现 - 网络时序图
-
使用基于netty的网络通信框架
-
位置信息记录在MapOutputTracker中
-
主要会发送两种类型的请求
- OpenBlocks请求
- Chunk清求或Stream请求
ShuffleBlockFetchlterator
-
区分local和remote节省网络消耗
-
防止OOM
- maxBytesInFlight
- maxReqsInFlight
- maxBlocksInFlightPerAddress
- maxReqSizeShuffleToMem
- maxAttemptsOnNettyOOM
External Shuffle Service
ESS作为一个存在于每个节点上的agent为所有Shuffle Reader提供服务,从而优化了Spark作业的资源利用率,MapTask在运行结束后可以正常退出
3.9 Shuffle优化使用的技术 -Zero Copy
Netty Zero Copy
√ 可堆外内存,避免JVM堆内存到堆外内存的数据拷贝
√ CompositeByteBuf、Unpooled.wrappedBuffer、ByteBuf.slice,可以合并、包装、切分数组,避免发生内存拷贝
√ Netty使用FileRegion实现文件传输,FileRegion底层封装了FileChannel#transferTo()方法,可以将文件缓冲区的数据直接传输到目标Channel,避免内核缓冲区和用户态缓冲区之间的数据拷贝
3.10 常见问题
-
数据存储在本地磁盘,没有备份
-
IO并发:大量RPC请求(M*R)
-
IO吞吐:随机读、写放大(3X)
-
GC频繁,影响NodeManager
3.11 Shuffle优化
- 避免shuffle
- 使用broadcast替代join
//传统的join操作会导致shuffle操作
//因为两个RDD中,相同的key都需要通过网络拉取到一个节点上,由一个task进行join操作
val rdd3 = rdd1.join(rdd2)
//broadcast+map的join操作,不会导致shuffle操作
//使用Broadcast将一个数据量较小的RDD作为广播变量
val rdd2Data = rdds.collect()
val rdd2DataBroadcast = sc.broadcast(rddsData)
//在rdd1.map算子中,可以从rdd2DataBroadcast中,获取rdd2的所有数据
//然后进行遍历,如果发现rdd2中某条数据的key与rdd1的当前数据的key是相同的,那么就判定可以进行
//此时就可以根据自己需要的方式,将rdd1当前数据与rdd2中可以连接的数据拼接在一起(String或...)
val rdd3 = rdd1.map(rdd2DataBroadcast...)
//注意,以上操作,建议仅仅在rdd2的数据量比较少(比如几百M,或者一两G)的情况下使用
//因为每个Executor的内存中,都会驻留一份rdd2的全量数据
- 使用可以map-side预聚合的算子
3.12 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.13 Shuffle 倾斜优化
-
什么叫shuffle倾斜
-
倾斜影响
- 作业运行时间变长
- Task OOM导致作业失败
常见的青协处理方法
- 提高并行度
- 优点:足够简单
- 缺点:只缓解、不根治
Spark AQE Skew Join
AQE根据shuffle文件统计数据自动检测倾斜数据,将那些倾斜的分区打散成小的子分区,然后各自进行join
3.14 案例 - 参数优化
- ad_show
- number of files read:840042
- number of total tasks:5553
- size of files read:203.3 TiB
- number of output rows:128676054598
3.15 参数调整
-
spark.sql.adaptive.shuffle.targetPostShuffleInputSize:64M -> 512M
-
spark.sql.files.maxPeartitionBytes:1G -> 40G
04 Push Shuffle
4.1 为什么需要Push Shuffle?
-
Avg IO size太小,造成了大量的随机IO,严重影响磁盘的吞吐
-
M * R次读请求,造成大量的网络连接,影响稳定性
4.2 Push Shuffle的实现
- Facebook:cosco
- Linkdin:magnet
- Uber:Zeus
- Alibaba:RSS
- Tencent:FireStorm
- Bytedance:CSS
- Spark3.2:push based shuffle
4.3 Magnet实现原理
- Spark driver组件,协调整体的shuffle操作
- map任务的shuffle writer过程完成后,增加了一个额外的操作push-merge,将数据复制一份推到远程Shuffle服务上
- magnet shuffle service是一个强化版的ESS。将隶属于同一个shuffle partition的block,会在远程传输到magnet后被merge到一个文件中
- reduce任务从magnet shuffle service接收合并好的shuffle数据
- bitmap:存储已merge的mapper id,防止重复merge
- position offset:如果本次block没有正常merge,可以恢复到上一个block的位置
- currentMapld:标识当前正在append的block,保证不同mapper的block能依次append
4.4 Magnet可靠性
- 如果Map task输出的Block没有成功Push到magnet上,并且反复重试仍然失败,则reduce task直接从ESS上拉取原始block数据
- 如果magnet上的block因为重复或者冲突等原因,没有正常完成merge的过程,则reduce task直接拉取未完成merge的block
- 如果reduce拉取已经merge好的block失败,则会直接拉去merge前的原始block
- 本质上,magnet中维护了两份shuffle数据的副本
4.5 Cloud Shuffle Service 思想
4.6 Cloud Shuffle Service架构
- Zookeeper WorkerList[服务发现]
- CSS Worker[Partitions / Disk | Hdfs]
- Spark Diver[集成启动CSS Master]
- CSS Master[Shuffle 规划/统计]
- CSS ShuffleClient[Write / Read]
- Spark Executor[Mapper + Reducer]
4.6.1 Cloud Shuffle Service写入流程
4.6.2 Cloud Shuffle Service读取流程
4.6.3 Cloud Shuffle Service AQE
一个Partition会最终对应到多个Epoch file,每个EPoch目前设置是512M
4.7 实践案例 - CSS优化
- XX业务 小时级任务(1.2W cores)
- 混部队列 2,5h -> 混部队列 + CSS 1.3h(50%提升)
05 总结
- 1、Shuffle概述
- 1、什么是shuffle,shuffle基本流程
- 2、为什么shuffle对性能影响非常重要
- 2、Shuffle算子
- 1、常见的shuffle算子
- 2、理解宽依赖和窄依赖,ShuffuleDependency及其相关组件
- 3、Shuffle过程
- 1、Spark中shuffle实现的历史
- 2、Spark中主流版本的shuffle写入和读取过程
- 4、Push shuffle
- 1、Magnet Push Shuffle的设计思路
- 2、Cloud Shuffle Service的设计实现思路