大数据 Shuffle 原理与实践 | 青训营笔记
这是我参与「第四届青训营 -大数据场」笔记创作活动的第8天
一、Shuffle概述
1. MapReduce概述
-
MapReduce存在Map、Shuffle、Reduce三个阶段
- Map阶段:在单机上对一小块数据进行计算的过程
- Shuffle阶段:在map处理好的数据上进行数据移动,为reduce作准备
- Reduce阶段:对移动后的数据进行处理,在单机上处理一小块数据
2. Shuffle对性能重要
- M*R次网络连接 (网络请求、server压力大)
- 大量的数据移动
- 数据丢失风险
- 存在大量的排序操作
- 大量的数据序列化、反序列化操作 (转换为2进制数据流存储、读取,消耗CPU)
- 数据压缩和解压缩
3.总结
- 在大数据场景下数据 shuffle 表示了不同分区数据交换的过程,不同 shuffle 策略性能差异大。
- 目前各个引擎中 shuffle 都是优化的重点,在 spark 框架中 shuffle 是支撑 spark 进行大规模复杂数据处理的基石。
二、Shuffle算子
1. Shuffle算子分类
| Repartition | ByKey | Join | Distinct |
|---|---|---|---|
| coalesce | groupByKey | cogroup | distinct |
| repartition | reduceByKey | join | |
| aggregateByKey | leftOuterJoin | ||
| combineByKey | intersection | ||
| sortByKey | subtract | ||
| sortBy | subtractByKey |
2. Shuffle算子应用
3. Spark中对Shuffle的抽象
- 窄依赖:父RDD的每个分片至多被子RDD中一个分片所依赖
- 宽依赖:父RDD中的分片可能被子RDD中多个分片所依赖
4. 算子内部的依赖关系
4.1 shuffleDependency
-
CoGroupedRDD
-
CoGroup
- fullOuterJoin, left/rightOuterjoin
- join
-
-
ShuffledRDD
-
combineByKeyWithClassTag
- combineByKey
- reduceByKey
-
coalesce
-
sortByKey
- sortBy
-
4.2 构造
-
Shuffle Dependency
-
创建会产生shuffle的RDD时,RDD会创建Shuffle Dependency来描述Shuffle相关的信息
-
构造函数
- 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
-
用来将record映射到具体的partition的方法
-
接口
- numberPartitions
- getPartition
-
-
Aggregator
-
在map侧合并部分record的函数
-
接口
- createCombiner:只有一个value的时候初始化的方法
- mergeValue:合并一个value到Aggregator中
- mergeCombiners:合并两个Aggregator
-
三、Shuffle过程
1. Hash Shuffle
- 写数据:每个partition会映射到一个独立的文件,写满后flush到内存。
- 问题:生成的文件、同时打开的文件太多 (M*R),消耗占用的资源过多。
- 写数据优化:把每个partition映射成一个文件的片段 只需要申请可以申请到的文件(C*R)
- 问题:仍然需要打开过多文件 (R个)
2. Sort Shuffle
- 写数据:每个task生成一个包含所有partition数据的文件。
3. Shuffle-读数据
- 每个reduce task分别获取所有map task生成的属于自己的片段。
4. Shuffle过程的触发流程
5. Shuffle Handle的创建
6. Shuffle Handle 与Shuffle Writer的对应关系
7. Writer实现
7.1 BypassMergeShuffleWriter
- partition较少 200
- 不需要排序,节省时间
- 写操作的时候会打开大量文件
- 类似于hash shuffle,但会将partition file merge到一起
7.2 UnsafeShuffleWriter
-
不能超过2^24 (前24bit记录partition id,不能溢出)
-
对外内存:unsafe
- 没有垃圾回收开销
- 只按照partition进行排序,不排序record
- record放到内存页 写满后申请新的内存页
- 写满后触发spill,merge spill file生成两个文件
-
类似内存页储存序列化数据,写入后不再反序列化
-
只根据partition排序long array (记录record对应partition id、page num和data offset 不移动数据)
7.3 SortShuffleWriter
-
支持combine
- 需要combine时使用partitionedAppendOnlyMap,本质是个HashTable
- 不需要combine时PartitionedPairBuffer本质是个array
-
需要对内内存,因为需要比较key所以如果放在对外内存需要反序列化
8. Reader的实现
8.1 网络时序图
-
使用基于netty的网络通信框架
- 申请对外内存
-
位置信息记录在mapOutputTracker中
- 存放在本地和远程,若在远程需要发送请求
-
主要会发送两种类型请求
- 客户端发送openBlocks请求
- 客户端接收到响应后:发送Chunk请求或Stream请求
-
客户端收取数据后,存储并解析数据
8.2 ShuffleBlockFetchIterator
-
区分local和remote节省网络消耗
-
对远端构造fetch request对象,添加到队列中(顺序随机,防止热点)
-
fetchUpToMaxBytes:从队列中取出对象,发送request请求
-
fetchLocalBlocks:获取本地blocks
-
防止OOM内存溢出
- maxBytesInFlight:限制数据块大小
- maxReqsInFlight:限制请求数量
- maxBlocksInFlightPerAddress:限制每个地址上block获取数量
- maxReqSizeShuffletoMem:最大进入内存的请求size
- maxAttemptsOnNettyOOM:控制对外OOM次数
8.3 External Shuffle Service
- 解耦数据计算和数据shuffle。
- ESS作为一个存在于每个节点上的agent为所有shuffle reader提供服务,从而优化Spark作业的资源利用率。
- MapTask在运行结束后可以正常退出。
- Map汇报shuffle数据存储的文件位置到ESS,Reduce端读取ESS输出的Shuffle数据。
9. Shuffle优化使用的技术
Netty Zero Copy
- 可堆外内存,避免JVM堆内到堆外内存数据拷贝
- compositeByteBuf、Unpooled.wrappedBuffer、bytebuf.slice可以合并、包装、切分数组,避免发生内存拷贝 (合并数组可以直接包装整体,对外提供整体的内容)
- Netty使用FileRegion实现文件传输,FileRegion底层封装了FileChannel#transferTo方法,可以将文件缓冲区数据直接传送到目标channel,避免内核和用户侧缓冲区进行的拷贝
10. 常见问题
- 数据存储在本地磁盘,没有备份:数据丢失需要重算
- IO并发:大量RPC请求(M*R)
- IO吞吐:随机读、写放大(3X)
- GC频繁,影响NodeManager
11. Shuffle优化
- 避免shuffle使用broadcast替代join
- 使用可以map-side预聚合的算子
12. Shuffle常见优化
-
shuffle参数优化
-
shuffle倾斜优化
-
有的task非常热点
-
容易导致作业运行时间变长、TaskOOM作业失败
-
解决方法
- 提高并行度:足够简单/只缓解、不根治
- AQE skew join:根据shuffle文件统计数据自动检测倾斜,将倾斜分区打散成为小的子分区,然后各自进行join
-
四、Push Shuffle
1. 为什么需要Push Shuffle
- Avg IO size太小,造成大量的随机IO,严重影响磁盘的吞吐
- M*R次读请求,造成大量网络连接,影响稳定性
2. Push Shuffle的实现
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的位置
- currentMapId:标识当前正在append的block,保证不同的mapper的block能依次append
4. Magnet可靠性
- 如果Map task输出的Block没有成功Push到magnet上,并且反复重试仍然失败,则reducetask直接从ESS上拉取原始block数据
- 如果magnet上的block因为重复或者冲突等原因,没有正常完成merge的过程,则reducetask直接拉取未完成merge的block
- 如果reduce拉取已经merge好的block失败,则会直接拉取merge前的原始block本质上,magnet中维护了两份shuffle数据的副本
5. Cloud Shuffle Service
5.1 思想
5.2 架构
- Zookeeper WorkerList:服务发现
- CSS worker:向zookeeper注册,partitions/Disk|HDFS
- Spark Driver:集成启动CSS Master
- CSS Master:shuffle规划/统计
- CSS ShuffleClient:读写
- SparkExecutor:Mapper/Reducer
5.3 写入流程
5.4 读取流程
5.5 AQE
- 大文件会被读很多遍,跳过很多行
- 切分大文件变为一般大的文件 Epoch files 512MB
课程总结
1. Shuffle概述
- 什么是shuffle,shuffle的基本流程
- 为什么shuffle对性能影响非常重要
2. Shuffle算子
- 常见的shuffle算子
- 理解宽依赖和窄依赖,ShuffleDependency及其相关组件
3. Shuffle过程
- Spark中shuffle实现的历史
- Spark中主流版本的shuffle写入和读取过程
4.Push shuffle
- Magnet Push Shuffle的设计思路
- Cloud Shuffle Service 的设计实现思路