这是我参与「第四届青训营 」笔记创作活动的的第36天
大数据Shuffle原理和实践
Shuffle概述
示意图:blog.peakbreaker.com/post/2020-0…
- map阶段:在单机上进行的针对一小块数据的计算过程
- shuffle阶段:在map阶段的基础上,针对多台机器,将相同颜色的数据移动到一起
- reduce操作:对移动后的数据进行处理,在单机上处理一小份数据
为什么shuffle对性能非常重要
- M*R次网络连接
- 大量数据移动
- 存在大量的排序操作
- 大量的数据序列化、反序列化操作
- 数据压缩
Shuffle算子
算子分类
比如keyBy操作,key就相当于上图中数据块的颜色,根据数据的key对数据重新进行组织
Spark对shuffle的抽象
-
窄依赖:父RDD的每个分片至多被子RDD中的一个分片所依赖
-
宽依赖:被多个分片依赖,就会移动数据
- 拆分成map stage和reduce stage,两个stage间产生shuffle操作
Shuffle Dependency构造
Partitioner
-
两个接口
- numberPartitions: int
- getPartition(key: Any): int
-
实现
- HashPartitioner
Aggregator
解决的问题:比如wordcount中,输入为apple apple apple banana。方法一:将这四个文本全部传给reduce,reduce进行count操作
或者在map
- createCombiner
- mergeValue
- mergeCombiners
Shuffle过程
Sort shuffle - 写数据
每个task生成一个包含所有partition数据的文件(还有一个index文件记录每个File segment的偏移量)
内存满,通过排序把相同partition的数据放在一起
Shuffle - 读数据
每个reduce task分别获取所有map task生成属于自己的片段
val text = sc.textFile("mytextfile.txt")
val counts = text
.flatMap(line => line.split(" "))
.map(word => (word,1))
.reduceByKey(_+_)
counts.collect
执行counts.collect时才生成ShuffleRDD
Register Shuffle时做的最重要的事情是根据不同条件创建不同的shuffle Handle
Writer实现
不同的shuffle Handle对应不同的shuffle Writer
- BypassMergeShuffleWriter:类似Hash Shuffle,不需要排序,写操作会打开大量文件;但不同于Hash Shuffle,最后还会把文件merge在一起,也就是一个task最后只有两个文件
- UnsafeShuffleWriter:使用Java堆外内存,分成内存页,把数据写到内存页,如果内存写满,执行spill,并申请新的内存页,最后执行merge
根据partition排序Long Array,数据不移动!前24个bit记录partition,因此要设置partition数量小于
-
SortShuffleWriter:未使用堆外内存。对key作比较。支持combine
- 需要combine时,所有record数据放在PartitionAppendOnlyMap,本质是HashTable,排序时需要快速地找到key的位置
- 不需要combine时,PartitionPairBuffer本质是Array
Reader实现
使用netty的网络通信框架
OpenBlocks请求:请求本地文件
Stream请求:服务端打开文件,客户端放在磁盘
Chunk请求:客户端放在内存
实现:ShuffleBlockFetchIterator
随机将请求放到队列,防止所有的map task向A节点同时发出请求,造成请求的热点
防止OOM:获取数据块大小、请求数量、每个地址获取的数据块数量、最大放在内存中的请求量、nettyOOM的次数
Shuffle优化使用:Zero Copy
DMA:直接存储器存取,外部设备不通过CPU直接与内存交换数据
图1:四次上下文切换,四次数据拷贝
图2:sendfile系统调用,简化两个通道的数据传输过程。减少了CPU拷贝和上下文切换次数(1,3两次DMA copy,2 CPU参与)
图3:省略了一次CPU参与,把读缓冲数据的meta元信息,记录到网络缓冲区,DMA根据内存地址和偏移量,直接从内存中读取到网络缓冲区,两次copy都是DMA copy
Netty Zero Copy
- 堆外内存,避免JVM堆内存到堆外内存的数据拷贝
- 避免发生内存拷贝(包装:将两个数组包装为1个数组,不需要额外申请空间)
- 操作系统Zero Copy,避免用户态和内核态得数据拷贝
常见问题
-
数据存储在本地磁盘,没有备份
-
大量RPC请求(M*R)
-
降低IO吞吐,随机读,写放大
- spill操作会执行写磁盘,merge操作又要再写一遍
-
GC频繁,RPC请求频繁创建对象,频繁回收,影响NodeManager
map-side预聚合(combine)
相当于Aggregator,在map侧进行一些操作,如对word出现次数求和
Shuflle倾斜优化
task处理的数据不均匀
- 作业运行时间变长、Task出现OOM
处理方法:
-
提高并行度(只缓解不根治)
-
AQE Skew Join
- 将倾斜的分区打散成小的子分区,各自进行join
实际案例
select
ad_type,
sum(click),
sum(duration)
from ad_show
where date = '20220101'
group by
ad_type
增大Chunk Size,减少了随机读带来的消耗。Map Side处采取combine减少Shuffle数据量
Push Shuffle
- Avg IO size太小,造成大量随机IO
- 平均块大小和延迟的关系
通过push的方法将shuffle聚合到一起,减少随机读
Magnet实现
- Spark driver组件,协调整体的shuffle操作
- map任务的shuffle writer完成后,增加一个push-merge操作,将数据复制推送到远程的Shuffle服务
- 将隶属于同一个shuffle partition的block,merge到一个文件中
- reduce任务从magnet中接收合并好的shuffle数据
Cloud Shuffle Service
- IO聚合:所有Mapper的同一partition数据都远程写到同一个文件
- 备份:双磁盘副本