这是我参与「第四届青训营 」笔记创作活动的第6天
1. Shuffle 概述
1.1. MapReduce 概述
- 2004年Google发布了《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此网络连接
- 大量的数据移动
- 数据丢失风险
- 可能存在大量的排序操作
- 大量的数据序列化、反序列化操作
- 数据压缩
1.5. 总结
在大数据场景下,数据Shuffle表示了不同分区数据交换的过程,不同的Shuffle策略性能差异较大。
目前在各个引擎中shuffle都是优化的重点,在spark框架中,shuffle是支撑spark进行大规模复杂数据处理的基石
2. Shuffle 算子
2.1. Shuffle 算子分类
Spark中会产生shuffle的算子大概可以分为4类
2.1. Shuffle 算子应用
2.2. Spark中对Shuffle的抽象 - 宽依赖、窄依赖
- 窄依赖:父RDD的每个分片至多被子RDD中的一个分片所依赖
- 宽依赖:父RDD中的分片可能被子RDD中的多个分片所依赖
2.2. 算子内部的依赖关系
- ShuffleDependency
- CoGroupedRDD
- Cogroup
- fullOuterJoin、rightOuterJoin、leftOuterJoin
- join
- Cogroup
- shuffledRDD
- combineByKeyWithClassTag
- combineByKey
- reduceByKey
- Colaesce
- sortByKey
- sortBy
- combineByKeyWithClassTag
- CoGroupedRDD
2.2.1. Shuff Dependency 构造
- A single key-value pair RDD
- Partitioner
- Serializer
- Optional key ordering
- Optional Aggregator
- mapSideCombine flag which is disabled by default
2.2.1. Shuffle Dependency 构造 - Partitioner
- 两个接口
- numberPartitions
- getPartition
- 经典实现
- HashPartition
2.2.1. Shuffle Dependency 构造 - Aggregator
- createCombine:只有一个value的时候初始化的方法
- mergeValue:合并一个value到Aggregator中
- mergeCombiners:合并两个Aggregator
3. Shuffle 过程
Shuffle实现的发展过程
- Spark 0.8 及以前 Hash Based Shuff
- Spark 0.8.1 为 Hash Based Shuff 引入File Consolidation机制
- Spark 0.9 引入 ExternalAppendOnlyMap
- Spark 1.1 引入 Sort Based Shuff,但默认仍为Hash Based Shuff
- Spark 1.2 默认的Shuffle方式改为Sort Based Shuff
- 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 Shuff - 写数据
每个partition会映射到一个独立的文件
3.1. Hash Shuffle - 写数据的优化
每个partition会映射到一个文件片段
3.2. Sort Shuffle - 写数据
每个task 生成一个包含所有partition数据的文件
3.3. Shuffle - 读数据
每个reduce task 分别获取所有map task 生成的属于自己的片段
3.4. Shuffle 过程的触发流程
3.5. Shuffle Handle 的创建
Register Shuffle 时做的最重要的事情是根据不同条件创建不同的shuffle Handle
3.6. Shuffle Handle 与 Shuffle Writer 的对应关系
3.7. Writer 实现 - BypassMergeShuffleWriter
- 不需要排序,节省时间
- 写操作的时候会打开大量文件
- 类似于Hash Shuffle
3.7. Writer 实现 - UnsafeShuffleWriter
- 使用类似内存页储存序列化数据
- 数据写入后不再反序列化
- 只根据partition排序 Long Array
- 数据不移动
3.7. Writer实现 - SortShuffleWriter
- 支持combine
- 需要combine时,使用PartitionedAppendOnlyMap,本质是个HashTable
- 不需要combine时,PartitionedPairBuffer本质是个array
3.8. Reader实现 - 网络时序图
- 使用基于Netty的网络通信架构
- 位置信息记录在MapOutputTracker中
- 主要会发送两种类型的请求
- OpenBlocks请求
- Chunk请求或Stream请求
3.8. Reader实现 - ShuffleBlockFetchIterator
- 区分local和remote节省网络消耗
- 防止OOM
- maxBytesInFlight
- maxReqsInFlight
- maxBlocksInFlightPerAddress
- maxReqSizeShuffleToMem
- maxAttemptsOnNettyOOM
3.8. Reader 实现 - External Shuffle Service
ESS作为一个存在于每个节点上的agent为所有Shuffle Reader提供服务,从而优化了Spark作业的资源利用率,MapTask在运行结束后可以正常退出
3.9. Shuffle 优化使用的计数 - Zero Copy
3.9. Shuffle 优化使用的技术 - Netty Zero Copy
- 可堆外内存,避免JVM堆内存到堆外内存的数据拷贝
- CompositeByteBUf、Unpooled.wrappedBuffer、ByteBuf.slice,可以合并、包装、切分数组,避免发生内存拷贝
- Netty使用FileRegion实现文件传输,FileRegion底层封装了FileChannel#transferTo()方法,可以将文件缓冲区的数据直接传输到目标Channel,避免内存缓冲区和用户态缓冲区之间的数据拷贝
3.10 常见问题
- 数据存储在本地磁盘,没有备份
- IO并发:大量RPC请求(M * R)
- IO吞吐:随机读,写放大(3X)
- GC频繁,影响Node Manager
3.11. Shuffle优化
- 避免Shuffle
- 使用broadcast代替join
- 可以使用map-side预聚合的算子
3.12. Shuffle参数优化
3.13. Shuffle倾斜优化
- 什么叫shuffle倾斜
- 倾斜影响
- 作业运行时间变长
- Task OOM导致作业失败
3.13. 常见的倾斜处理办法
- 提高并行度
- 优点:足够简单
- 缺点:只缓解、不根治
3.13. Spark AQE Skew Join
AQE根据shuffle文件统计数据自动检测倾斜数据,将那些倾斜的分区打散成小的子分区,然后各自进行join
3.14. 案例 - 参数优化
3.15. 参数调整
4. Push Shuffle
4.1. 为什么需要Push Shuffle
- Avg IO size 太小,造成了大量的随机IO,严重影响磁盘的吞吐
- M * R次读请求,造成大量的网络连接,影响稳定性