大数据Shuffle原理和实践 | 青训营笔记

34 阅读5分钟

这是我参与「第四届青训营 」笔记创作活动的的第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数据都远程写到同一个文件
  • 备份:双磁盘副本