这是我参与「第四届青训营 」笔记创作活动的的第9天
shuffle 概述
提出
04年Google发布的关于处理大规模集群简化数据处理的论文,mapreduce三阶段:map,shuffle,reduce
Map阶段
在单机上进行的针对一小块数据的计算过程
Shuffle阶段
在map的基础上,进行数据移动,为后续的reduce做准备
Reduce阶段
对移动后的数据进行处理,依然是单机上处理一小份数据
为何shuffle对性能非常重要
- M * R 次网络连接
- 大量的数据移动
- 数据丢失风险
- 可能存在大量的排序操作
- 大量的数据序列化,反序列化操作
- 数据压缩
在大数据场景下,数据shuffle表示了不同分区数据交换的过程,不同的shuffle策略性能差异较大。
目前在各个引擎中shuffle都是优化的重点,在spark框架中,shuffle是支撑spark进行大规模复杂数据处理的基石。
shuffle 算子分类
map和flatMap
Spark 中 map函数会对每一条输入进行指定的操作,然后为每一条输入返回一个对象。 而flatMap函数则是两个操作的集合——正是“先映射后扁平化”
reduceByKey
reduceByKey会寻找相同key的数据,当找到这样的两条记录时会对其value(分别记为x,y)做(x,y) => x+y的处理,即只保留求和之后的数据作为value。反复执行这个操作直至每个key只留下一条记录。reduceByKey(_+_)是reduceByKey((x,y) => x+y)的简洁表达。
file1.txt
Hello World
Hello Spark
Hello Spark
file2.txt
Hello World
Hello Spark
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local").setAppName("WordCount")
val sc = new SparkContext(sparkConf)
val text = sc.textFile("datas/*")
val counts = text
.flatMap(line => line.split(" "))
.map(word => (word, 1))
.reduceByKey(_ + _)
val tuples: Array[(String, Int)] = counts.collect()
tuples.foreach(println)
sc.stop()
}
输出
(Hello,5)
(World,2)
(Spark,3)
shuffle抽象:宽窄依赖
依赖构造
Partitioner
- 两个接口
- numberPartitions
- getPartition
- 经典实现
- HashPartitioner
Aggregator
- createCombiner:只有一个value的时候初始化的方法
- mergeValue:合并一个value到Aggregator中
- mergeCombiners:合并两个Aggregator
shuffle 过程
shuffle 发展历程
Hash shuffle
写数据
每个partition会映射到一个独立文件
写数据优化
每个partition会映射到一个文件片段
sort shuffle
写数据
每个task生成一个包含所有partition数据的文件
读数据
每个reduce task分别获取所有map task生成的属于自己的片段
现在在哪?
wordcount例子
val text = sc.textFile("datas/*")
val counts = text
.flatMap(line => line.split(" "))
.map(word => (word, 1))
.reduceByKey(_ + _)
val tuples: Array[(String, Int)] = counts.collect()
在collect中会调用spark中DAGscheduler(调度模块DAGScheduler和TaskScheduler)submit job,其中会分析counts对象RDD依赖关系,该对象包含了reduceByKey算子,所以在查找依赖关系时,会创建shuffleRDD的对象,这个对象在dagScheduler中getDependencies时,会创建shuffle dependencies,然后registerShuffle注册,后续dag会生成两个stage,map,reduce;这两个stage分别提交给executor,计算开始
ShuffleRegister
Shuffle Handle 的创建
Register Shuffle 时做的最重要的事情是根据不同条件创建不同的 shuffle Handle
- 由action算子触发DAG Scheduler进行shuffle register
- Shuffle Register会根据不同的条件决定注册不同的ShuffleHandle
- spark.shuffle.sort.bypassMergeThreshold默认为200
Shuffle Writer 的实现
三种ShuffleHandle对应了三种不同的ShuffleWriter的实现
-
BypassMergeSortShuffleWriter:HashShuffle
- 不需要排序,节省时间
- 写操作的时候会打开大量文件
-
UnsafeShuffleWriter:TunstonShuffle
-
使用类似内存页储存序列化数据
-
数据写入后不再反序列化
-
只适用partition数量较小的情况,使用堆外内存,没有垃圾回收的开销,没有对象模型类型开销
-
只根据 partition 排序 Long Array
-
数据不移动
-
只保留24 bit用于存储 partition 数,其他空间用于存储这些Long Array数据,所以超过2242^{24}224的partition数就不适用该 Writer
-
-
SortSHuffleWriter:SortShuffle
- 支持combine
- 需要combine时,使用PartitionedAppendOnlyMap,本质是个HashTable
- 不需要combine时PartitionedPairBuffer本质是个array
Shuffle Reader 的实现
网络时序图
-
使用基于netty的网络通信框架,并接受reducetask的fetch请求
- 使用堆外内存,零拷贝
-
位置信息记录在MapOutputTracker中
-
主要会发送两种类型的请求
- 首先发起openBlocks请求获得streamId
- 然后再处理Chunk请求或Stream请求
Shuffle Block FetchIterator
-
区分local和remote节省网络消耗
-
防止OOM
- maxBytesInFlight
- maxReqsInFlight
- maxBlocksInFlightPerAddress
- maxReqSizeShuffleToMem
- maxAttemptsOnNettyOOM
External Shuffle Service
为了解决Executor为了服务数据的fetch请求导致无法退出问题,我们在每个节点上部署一个External Shuffle Service,这样产生数据的Executor在不需要继续处理任务时,可以随意退出。从而优化了Spark作业的资源利用率,MapTask在运行结束后可以正常退出。
Shuffle 优化
Zero Copy(零拷贝)
DMA(Direct Memory Access) : 直接存储器存取,是指外部设备不通过 CPU 而直接与系统内存交换数据的接口技术。
不使用 zero copy
使用sendfile
使用sendfile + DMA gather copy
Netty 零拷贝
- 可堆外内存,避免 JVM 堆内存到堆外内存的数据拷贝。
- CompositeByteBuf 、 Unpooled.wrappedBuffer、 ByteBuf.slice ,可以合并、包装、切分数组,避免发生内存拷贝
- Netty 使用 FileRegion 实现文件传输,FileRegion 底层封装了 FileChannel#transferTo() 方法,可以将文件缓冲区的数据直接传输到目标 Channel,避免内核缓冲区和用户态缓冲区之间的数据拷贝
Shuffle 优化
-
避免 shuffle
- 使用 broadcast 替代 join
-
使用可以map-side预聚合的算子
- 未使用map-side预聚合
- 使用map-side预聚合
- 未使用map-side预聚合
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.enark.sql.adaptivetPostShufflelnputSize
- spark.reducer.maxSizelnFlight
- spark.reducer.maxReqslnFlight
- spark.reducer.maxBlockslnFlightPerAddress
Shuffle 倾斜优化
-
倾斜影响
- 作业运行时间变长
- Task OOM 导致作业失败
-
提高并行度
- 优点:足够简单
- 缺点:只缓解、不根治问题
Spark AQE Skew Join
AQE 根据shuffle文件统计数据自动检测倾斜数据,将那些倾斜的分区打散成小的分区,然后各自进行join。
案例:参数优化
- spark.sql.adaptive.shuffle.targetPostShufflelnputSize: 64M -> 512M
- spark.sql.files.maxPartitionBytes: 1 G - > 40G
Push Shuffle
为什么需要Push Shuffle?
Shuffle阶段常见问题
- Avg IO size太小,造成了大量的随机IO,严重影响磁盘的吞吐
- M*R次读请求,造成大量的网络连接,影响稳定性
- 数据存储在本地磁盘,没有备份
- IO 吞吐:随机读、写放大(3X)
- GC 频繁,影响 NodeManager
Magnet 实现原理
Magnet主要流程
- Spark driver组件,协调整体的shuffle操作
- map任务的shuffle writer过程完成后,增加了一个额外的操作push-merge,将数据复制一份推到远程shuffle服务上
- magnet shuffle service是一个强化版的ESS。将隶属于同一个shuffle partition的block,会在远程传输到magnet 后被merge到一个文件中
- reduce任务从magnet shuffle service 接收合并好的shuffle数据
Magnet实现原理
- bitmap:存储已merge的mapper id,防止重复merge
- position offset:如果本次block没有正常merge,可以恢复到上一个block的位置
- currentMapId: 标识当前正append的block,保证不同mapper 的block能依次 append
Magnet 可靠性
- 如果Map task输出的Block没有成功Push到magnet上,并且反复重试仍然失败,则reduce task直接从ESS上拉取原始block数据
- 如果magnet上的block因为重复或者冲突等原因,没有正常完成merge的过程,则reduce task直接拉取末完成merge的block
- 如果reduce拉取已经merge好的block失败,则会直接拉取merge前的原始block
- 本质上,magnet中维护了两份shuffle数据的副本
Cloud Shuffle Service
Cloud Shuffle Service 思想
Cloud Shuffle Service 架构
- Zookeeper WorkerList [服务发现]
- CSS Worker [Partitions / Disk | Hdfs]
- Spark Driver [集成启动 CSS Master]
- CSS Master [Shuffle 规划 / 统计]
- CSS ShuffleClient [Write / Read]
- Spark Executor [Mapper + Reducer]
Cloud Shuffle Service 读写流程
Cloud Shuffle Service AQE
一个Partition会最终对应到多个Epoch file,每个EPoch 目前设置是 512MB
在聚合文件时主动将文件切分为若干块,当触发AQE时,按照已经切分好的文件块进行拆分。