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

75 阅读7分钟

这是我参与「第四届青训营 」笔记创作活动的第5天

01 Shuffle概述

1.1 MapReduce概述

  • 2004年谷歌发布了《MapReduce:Simplified Data Processing on Large Clusters》论文

  • 在开源实现的MapReduce中,存在Map、Shuffle、Reduce三个阶段

屏幕截图 2022-07-31 094720.jpg

1.1 Map阶段

√ Map阶段,是在单机上进行的针对一小块数据的计算过程

2.jpg

1.2 Shuffle阶段

√ Shuffle阶段,在map阶段的基础上,进行数据移动,为后续的reduce阶段做准备

3.jpg

4.jpg

1.3 Reduce过程

√ Reduce阶段,对移动后的数据进行处理,依然是在单机上处理一小份数据

5.jpg

6.jpg

1.4 为什么shuffle对性能非常重要

  • M * R次网络连接

  • 大量的数据移动

  • 数据丢失风险

  • 可能存在大量的排序操作

  • 大量的数据序列化、反序列化操作

  • 数据解压

7.jpg

总结

8.jpg 在大数据场景下,数据shuffle表示了不同分区数据交换的过程,不同的shuffle策略性能差异较大。目前在各个引擎中shuffle都是优化的重点,在spark框架中,shuffle是支撑spark进行大规模复杂数据处理的基石

02 Shuffle算子

2.1 Shuffle算子分类

Spark中会产生shuffle的算子大概可以分为4类

9.jpg

算子应用

  • Spark源码中RDD的单元测试

  • Spark源码中PairRDDFunctions的单元测试

val text = sc.textFile("mytextfile.txt")
val counts = text
    .flatMap(line => line.split(""))
    .map(word => (word,1))
    .reduceByKey(_+_)
counts.collect

2.2 Spark中对shuffle的抽象 - 宽依赖、窄依赖

**窄依赖:**父RDD的每个分片至多被子RDD中的一个分片所依赖

**宽依赖:**父RDD中的分片可能被子RDD中的多个分片所依赖

10.jpg

算子内部的依赖关系

ShuffleDependency

√ CoGroupedRDD

  • Cogroup
    • fullOuterJoin、rightOuterJoin、leftOuterJoin

    • join

√ ShuffledRDD

  • combineByKeyWithClassTag

    • combineByKey

    • reduceByKey

  • Coalesce

  • sortByKey

    • sortBy

2.2.1 Shuffle Dependency构造

  • 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

  • 两个接口
    • numberPartitions
    • getPartitioner
abstract class Partitioner extends Serializable{
    def numPartitions:Int
    def getPartition(key:Any):Int
}
  • 经典实现
    • HashPartitioner
class HashPartitioner(partitions:Int) extends Partitioner {
    require(partitions >= 0, s"Number of partitions ($partitions) cannot be nega...)
    
    def numPartitions:Int = partitions
    
    def getPartition(key:Any):Int = key match {
        case null => 0
        case _=> Utils.nonNegativeMod(key.hashCode,numPartitions)
    }
}

2.2.1 Aggregator

  • createCombiner:只有一个value的时候初始化的方法

  • mergeValue:合并一个value到Aggregator中

  • mergeCombiners:合并两个Aggregator

03 Shuffle过程

Shuffle实现的发展历程

  • Spark 0.8及以前Hash Based Shuffle

  • Spark 0.8.1 为Hash Based Shuffle引入File Consolidation机制

  • Spark 0.9 引入ExternalAppendOnlyMap

  • Spark 1.1 引入Sort Based Shuffle,但默认仍为Hash Based Shufle

  • Spark 1.2 默认的Shuffle方式改为Sort Based Shuffle

  • 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 Shuffle - 写数据

每个partition会映射到一个独立的文件

11.jpg

3.1 写数据优化

每个ppartition会映射到一个文件片段

12.jpg

3.2 Sort shuffle:写数据

每个task生成一个包含所有partition数据的文件

13.jpg

3.3 Shuffle - 读数据

每个reduce task分别获取所有map task生成的属于自己的片段

14.jpg

3.4 Shuffle过程的触发流程

val text = sc.textFile("mytextfile.txt")
val counts = text
    .flatMap(line => line.split(""))
    .map(word => (word,1))
    .reduceByKey(_+_)
counts.collect

15.jpg

3.5 Shuffle Handle的创建

Register Shuffle时做的最重要的事情是根据不同条件创建不同的shuffle Handle

16.jpg

3.6 Shuffle Handle与Shuffle Writer的对应关系

17.jpg

3.7 Writer实现 - BypassMergeShuffleWriter

18.jpg

  • 不需要排序,节省时间

  • 写操作的时候会打开大量文件

  • 类似于Hash Shuffle

UnsafeShuffleWriter

19.jpg

  • 使用类似内存页储存序列化数据

  • 数据写入后不再反序列化

20.jpg

  • 只根据partition排序Long Array

  • 数据不移动

21.jpg

  • 支持combine

-需要combine时,使用PartitionedAppendOnlyMap,本质是个HashTable

  • 不需要combine时PartitionedPairBuffer本质是个array

3.8 Reader实现 - 网络时序图

  • 使用基于netty的网络通信框架

  • 位置信息记录在MapOutputTracker中

  • 主要会发送两种类型的请求

    • OpenBlocks请求
    • Chunk清求或Stream请求

22.jpg

ShuffleBlockFetchlterator

23.jpg

  • 区分local和remote节省网络消耗

  • 防止OOM

    • maxBytesInFlight
    • maxReqsInFlight
    • maxBlocksInFlightPerAddress
    • maxReqSizeShuffleToMem
    • maxAttemptsOnNettyOOM

External Shuffle Service

ESS作为一个存在于每个节点上的agent为所有Shuffle Reader提供服务,从而优化了Spark作业的资源利用率,MapTask在运行结束后可以正常退出

24.jpg

3.9 Shuffle优化使用的技术 -Zero Copy

25.jpg 26.jpg

Netty Zero Copy

√ 可堆外内存,避免JVM堆内存到堆外内存的数据拷贝

√ CompositeByteBuf、Unpooled.wrappedBuffer、ByteBuf.slice,可以合并、包装、切分数组,避免发生内存拷贝

√ Netty使用FileRegion实现文件传输,FileRegion底层封装了FileChannel#transferTo()方法,可以将文件缓冲区的数据直接传输到目标Channel,避免内核缓冲区和用户态缓冲区之间的数据拷贝

3.10 常见问题

  • 数据存储在本地磁盘,没有备份

  • IO并发:大量RPC请求(M*R)

  • IO吞吐:随机读、写放大(3X)

  • GC频繁,影响NodeManager

3.11 Shuffle优化

  • 避免shuffle
    • 使用broadcast替代join
//传统的join操作会导致shuffle操作
//因为两个RDD中,相同的key都需要通过网络拉取到一个节点上,由一个task进行join操作
val rdd3 = rdd1.join(rdd2)

//broadcast+map的join操作,不会导致shuffle操作
//使用Broadcast将一个数据量较小的RDD作为广播变量
val rdd2Data = rdds.collect()
val rdd2DataBroadcast = sc.broadcast(rddsData)

//在rdd1.map算子中,可以从rdd2DataBroadcast中,获取rdd2的所有数据
//然后进行遍历,如果发现rdd2中某条数据的key与rdd1的当前数据的key是相同的,那么就判定可以进行
//此时就可以根据自己需要的方式,将rdd1当前数据与rdd2中可以连接的数据拼接在一起(String或...)
val rdd3 = rdd1.map(rdd2DataBroadcast...)

//注意,以上操作,建议仅仅在rdd2的数据量比较少(比如几百M,或者一两G)的情况下使用
//因为每个Executor的内存中,都会驻留一份rdd2的全量数据
  • 使用可以map-side预聚合的算子

27.jpg

28.jpg

3.12 Shuffle参数优化

  • 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

3.13 Shuffle 倾斜优化

  • 什么叫shuffle倾斜

  • 倾斜影响

    • 作业运行时间变长
    • Task OOM导致作业失败

29.jpg

常见的青协处理方法

  • 提高并行度
    • 优点:足够简单
    • 缺点:只缓解、不根治

30.jpg

Spark AQE Skew Join

AQE根据shuffle文件统计数据自动检测倾斜数据,将那些倾斜的分区打散成小的子分区,然后各自进行join

31.jpg

3.14 案例 - 参数优化

  • ad_show
    • number of files read:840042
    • number of total tasks:5553
    • size of files read:203.3 TiB
    • number of output rows:128676054598

32.jpg

3.15 参数调整

  • spark.sql.adaptive.shuffle.targetPostShuffleInputSize:64M -> 512M

  • spark.sql.files.maxPeartitionBytes:1G -> 40G

33.jpg

04 Push Shuffle

4.1 为什么需要Push Shuffle?

  • Avg IO size太小,造成了大量的随机IO,严重影响磁盘的吞吐

  • M * R次读请求,造成大量的网络连接,影响稳定性

34.jpg

4.2 Push Shuffle的实现

  • Facebook:cosco
  • Linkdin:magnet
  • Uber:Zeus
  • Alibaba:RSS
  • Tencent:FireStorm
  • Bytedance:CSS
  • Spark3.2:push based shuffle

4.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数据

35.jpg

  • bitmap:存储已merge的mapper id,防止重复merge
  • position offset:如果本次block没有正常merge,可以恢复到上一个block的位置
  • currentMapld:标识当前正在append的block,保证不同mapper的block能依次append

36.jpg

4.4 Magnet可靠性

  • 如果Map task输出的Block没有成功Push到magnet上,并且反复重试仍然失败,则reduce task直接从ESS上拉取原始block数据
  • 如果magnet上的block因为重复或者冲突等原因,没有正常完成merge的过程,则reduce task直接拉取未完成merge的block
  • 如果reduce拉取已经merge好的block失败,则会直接拉去merge前的原始block
  • 本质上,magnet中维护了两份shuffle数据的副本

4.5 Cloud Shuffle Service 思想

37.jpg

4.6 Cloud Shuffle Service架构

  • Zookeeper WorkerList[服务发现]
  • CSS Worker[Partitions / Disk | Hdfs]
  • Spark Diver[集成启动CSS Master]
  • CSS Master[Shuffle 规划/统计]
  • CSS ShuffleClient[Write / Read]
  • Spark Executor[Mapper + Reducer]

38.jpg

4.6.1 Cloud Shuffle Service写入流程

39.jpg

4.6.2 Cloud Shuffle Service读取流程

40.jpg

4.6.3 Cloud Shuffle Service AQE

一个Partition会最终对应到多个Epoch file,每个EPoch目前设置是512M

41.jpg

4.7 实践案例 - CSS优化

  • XX业务 小时级任务(1.2W cores)
  • 混部队列 2,5h -> 混部队列 + CSS 1.3h(50%提升)

42.jpg

05 总结

  • 1、Shuffle概述
    • 1、什么是shuffle,shuffle基本流程
    • 2、为什么shuffle对性能影响非常重要
  • 2、Shuffle算子
    • 1、常见的shuffle算子
    • 2、理解宽依赖和窄依赖,ShuffuleDependency及其相关组件
  • 3、Shuffle过程
    • 1、Spark中shuffle实现的历史
    • 2、Spark中主流版本的shuffle写入和读取过程
  • 4、Push shuffle
    • 1、Magnet Push Shuffle的设计思路
    • 2、Cloud Shuffle Service的设计实现思路