大数据Shuffle原理与实践|青训营笔记
这是我参与「第四届青训营 」笔记创作活动的的第4天。
一、课程概要
- shuffle概述:shuffle的定义和基本过程
- Spark中shuffle算子的使用和特性
- Spark中shuffle的核心原理和实现细节
- push based shuffle的实现方案
二、课程详细内容
1. shuffle概述
1.1 MapReduce概述
- MapReduce存在Map、Shuffle、Reduce三个阶段
- Map阶段:在单机上对一小块数据进行计算的过程
- shuffle阶段:在map处理好的数据上进行数据移动,为reduce作准备
- reduce阶段:对移动后的数据进行处理,在单机上处理一小块数据
1.2 shuffle对性能重要
- M*R次网络连接 (网络请求、server压力大)
- 大量的数据移动
- 数据丢失风险
- 存在大量的排序操作
- 大量的数据序列化、反序列化操作 (转换为2进制数据流存储、读取,消耗CPU)
- 数据压缩和解压缩
在大数据场景下数据shuffle表示了不同分区数据交换的过程,不同shuffle策略性能差异大。目前各个引擎中shuffle都是优化的重点,在spark框架中shuffle是支撑spark进行大规模复杂数据处理的基石。
2. shuffle算子
2.1 shuffle算子分类
- repartition:重新改变分区 coalesce/repartition
- ByKey:group/agg/combine/sort
- Join:cogroup/join
- Distinct
2.2 shuffle算子应用
val counts = text
.flatMap(line->line.split(" "))
.map(word->(word,1))
.reduceByKey(_+_)
counts.collect
reduceByKey触发shuffle。
2.3 Spark中对shuffle的抽象
是否有数据的移动。
- 窄依赖:父RDD的每个分片至多被子RDD中一个分片所依赖
- 宽依赖:父RDD中的分片可能被子RDD中多个分片所依赖
2.4 算子内部的依赖关系
-
shuffleDependency
- CoGroupedRDD
- CoGroup
- fullOuterJoin, left/rightOuterjoin
- join
- CoGroup
- ShuffledRDD
- combineByKeyWithClassTag
- combineByKey
- reduceByKey
- coalesce
- sortByKey
- sortBy
- combineByKeyWithClassTag
- CoGroupedRDD
-
shuffleDependency构造
- single key-value pair RDD
- partitioner
- 两个接口:numberPartitions/getPartition
- 经典实现:HashPartitioner
- serializer
- optional key ordering
- optional aggregator
- createCombiner:单value初始化方法
- mergeValue:合并一个value到aggregator中
- mergeCombiners:合并两个aggregator
- mapSideCombine flag disabled by default
3. shuffle过程
3.1 Hash Shuffle
- 写数据:每个partition会映射到一个独立的文件,写满后flush到内存。
- 问题:生成的文件、同时打开的文件太多 (M*R),消耗占用的资源过多。
- 写数据优化:把每个partition映射成一个文件的片段 只需要申请可以申请到的文件(C*R)
- 问题:仍然需要打开过多文件 (R个)
3.2 Sort Shuffle
- 写数据:每个task生成一个包含所有partition数据的文件。
3.3 读数据
每个reduce task分别获取所有map task生成的属于自己的片段。
3.4 shuffle过程的触发流程
graph LR
CollectAction --> SubmitJob --> GetDependencies --> RegisterShuffle
3.5 Shuffle Handle的创建
-
不支持mapSide Combine,partition小于一定值(200):BypassMergeSortShuffleHandle (hash shuffle)
-
不支持mapSide Combine,partition小于2^24:SerializedShuffleHandle
-
其他:原始sortShuffleHandle
3.6 writer实现
3.6.1 bypassMergeShuffleWriter
- partition较少 200
- 不需要排序,节省时间
- 写操作的时候会打开大量文件
- 类似于hash shuffle,但会将partition file merge到一起
3.6.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 不移动数据
3.6.3 SortShuffleWriter
- 支持combine
- 需要combine时使用partitionedAppendOnlyMap,本质是个HashTable
- 不需要combine时PartitionedPairBuffer本质是个array
- 需要对内内存,因为需要比较key所以如果放在对外内存需要反序列化
3.7 Reader的实现
3.7.1 网络时序图
- 使用基于netty的网络通信框架
- 申请对外内存
- 位置信息记录在mapOutputTracker中
- 存放在本地和远程,若在远程需要发送请求
- 主要会发送两种类型请求
- 客户端发送openBlocks请求
- 客户端接收到响应后:发送Chunk请求或Stream请求
- 客户端收取数据后,存储并解析数据
3.7.2 ShuffleBlockFetchIterator
- 区分local和remote节省网络消耗
- 对远端构造fetch request对象,添加到队列中(顺序随机,防止热点)
- fetchUpToMaxBytes:从队列中取出对象,发送request请求
- fetchLocalBlocks:获取本地blocks
- 防止OOM内存溢出
- maxBytesInFlight:限制数据块大小
- maxReqsInFlight:限制请求数量
- maxBlocksInFlightPerAddress:限制每个地址上block获取数量
- maxReqSizeShuffletoMem:最大进入内存的请求size
- maxAttemptsOnNettyOOM:控制对外OOM次数
3.7.3 External Shuffle Service
- 解耦数据计算和数据shuffle。
- ESS作为一个存在于每个节点上的agent为所有shuffle reader提供服务,从而优化Spark作业的资源利用率。
- MapTask在运行结束后可以正常退出。
- Map汇报shuffle数据存储的文件位置到ESS,Reduce端读取ESS输出的Shuffle数据
3.8 Zero Copy - shuffle优化
-
DMA direct memory access:直接存储器存取 外部设备不通过CPU直接与系统内存交换数据的接口技术
- 不使用:打开inputStream和outputStream,kernel->app读入然后app->kernel写出 (发生4次context switch,2次DMA+2次CPU拷贝)
- 使用sendFile:inputStream直接写到outputStream (2次context switch,2次DMA+1次CPU拷贝)
- 使用sendfile+DMA gather copy(0次context switch,2次DMA拷贝):readbuffer记录到descriptor,DMA根据descriptor直接从readbuffer拷贝到NIC buffer
-
合并文件时可以应用,无需拷贝文件到用户态
-
发送数据时可以应用,直接将文件数据提供给网卡
-
Netty Zero Copy
- 可堆外内存,避免JVM堆内到堆外内存数据拷贝
- compositeByteBuf、Unpooled.wrappedBuffer、bytebuf.slice可以合并、包装、切分数组,避免发生内存拷贝 (合并数组可以直接包装整体,对外提供整体的内容)
- Netty使用FileRegion实现文件传输,FileRegion底层封装了FileChannel#transferTo方法,可以将文件缓冲区数据直接传送到目标channel,避免内核和用户侧缓冲区进行的拷贝
3.9 常见问题
- 数据存储在本地磁盘,没有备份:数据丢失需要重算
- IO并发:大量RPC请求(M*R)
- IO吞吐:随机读、写放大(3X)
- GC频繁,影响NodeManager
3.10 常见优化
- 避免shuffle:使用broadcast替代join
- 使用可以map-side预聚合的算子
- shuffle参数优化 (见ppt)
- 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
- shuffle倾斜优化
- 有的task非常热点
- 容易导致作业运行时间变长、TaskOOM作业失败
- 解决方法
- 提高并行度:足够简单/只缓解、不根治
- AQE skew join:根据shuffle文件统计数据自动检测倾斜,将倾斜分区打散成为小的子分区,然后各自进行join
4. push shuffle
4.1 为什么需要push shuffle
- avg IO size太小,造成大量的随机IO,严重影响磁盘的吞吐
- M*R次读请求,造成大量网络连接,影响稳定性
4.2 push shuffle的实现
在读取之前将数据合并到一起。
4.2.1 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
-
可靠性
- 如果maptask输出block没有成功push到magnet上并且反复重试失败,则reduce task直接从ESS上拉取原始block数据
4.2.2 Cloud Shuffle Service
- IO聚合:所有mapper的同一partition数据都远程写到同一个文件
- 架构
- zookeeper 服务发现
- CSS worker:向zookeeper注册,partitions/Disk|HDFS
- Spark Driver:集成启动CSS Master
- CSS Master:shuffle规划/统计
- CSS ShuffleClient:读写
- SparkExecutor:Mapper/Reducer
- 写入流程
- 向worker0,worker1同时写入 若pushData失败,则reallocation,改向worker2 worker3写入 (最终获得3个有效文件和一个失败Epoch file
- AQE
- 大文件会被读很多遍,跳过很多行
- 切分大文件变为一般大的文件 Epoch files 512MB
三、实践例
- 参数调整
- targetPostShuffleInputSize:64M->512M
- maxPartitionBytes:1G->40G
- 增大MapTask数据处理量后,由于算子存在map-side agg,减少整体shuffle数据量
- 通过参数调整增大了chunk size,减缓了随机读取,减少了shuffle过程中IOPS,避免长时间blocked Time
四、个人总结
- 难点
- shuffle的原理与实现:三种不同的shuffle
- shuffle优化,push shuffle
- 个人总结:本次课程主要学习了Spark shuffle的原理与相关实现,介绍了针对不同partition数的三种shuffle策略。同时学习了为了减轻磁盘随机读取而采用的优化push shuffle策略。课后应当查询更多相关源代码进一步了解shuffle的实现。