Shuffle 原理与实践

672 阅读12分钟

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

2022の夏天,半壶水响叮当的我决定充实一下自我


一、内容介绍

    青训营

总述

  1. shuffle概述:shuffle是什么,shuffle的基本过程是什么
  2. spark中的shuffle算子:学习使用spark中的会产生shuffle的算子,了解其基本特性
  3. spark中的shuffle过程:spark中shuffle的核心原理和实现细节
  4. Push based shuffle:push shuffle社区的实现方案以及字节自己的实现方案

目标

  1. 了解spark中shuffle的发展历史以及主要实现机制。包括如何划分stage、partition分区、spill、combine等。
  2. 了解spark shuffle的底层实现原理以及Push-based shuffle总体架构方案。
  3. 了解shuffle优化,包括实际业务场景下如何避免产生shuffle、减少shuffle数据量、shuffle参数优化等。

预知

  1. 熟悉spark中会产生shuffle的算子:

    1. 重分区
    2. ByKey
    3. Join
  2. 使用spark的RDD算子,完成WordCount功能并运行,并观察作业的stage划分

  3. 使用spark分析CSV文件,并编写相关的sql,其中包含join,并观察作业的stage划分以及sql plan

  4. 熟悉netty的基本用法,尝试了解netty server和client的经典用法


二、shuffle概述

Shuffle是什么,为什么需要 Shuffle,Shuffle的基本过程是怎么样的
    Shuffle:在map阶段的基础上,进行数据移动,为后续的reduce阶段做准备

2.1 Map Reduce概述

  • 2004年谷歌发布了《MapReduce:Simplified Data Processing on LargeClusters》论文/地图推理:大型集群论文上的简化数据处理

  • 在开源实现的MapReduce中,存在Map、Shuffle、Reduce三个阶段。 image (4).jpeg Partition:在map,reduce过程中,分离数据集
    Shuffle:并发合并移动数据集

  • Map阶段,是在单机上进行的针对一小块数据的计算过程 image (1).gif

  • Shufle阶段,在map阶段的基础上,进行数据移动,为后续的reduce阶段做准备。 image (2).gif

  • reduce阶段,对移动后的数据进行处理,依然是在单机上处理一小份数据 image (3).gif

2.2 为什么 Shuffle 对性能十分重要

  • M*R次网络连接
  • 大量的数据移动
  • 数据丢失风险
  • 可能存在大量的排序操作
  • 大量的数据序列化、反序列化操作(内存可视数据转二进制数据流,保存文件,再读文件转内存,转对象),此操作消耗CPU
  • 数据压缩(存储时压缩与解压)

2.3 总结

批式计算发展:Mapreduce -> Spark -> Spark3.2(Push Shuffle)
在大数据场景下,数据shufile表示了不同分区数据交换的过程,不同的shuffle 策略性能差异较大。
目前在各个引擎中shuffle都是优化的重点,在spark框架中,shuffle是支撑spark进行大规模复杂数据处理的基石。


三、shuffle算子

介绍:Spark中常用的shuffle算子,因为数据移动产生

3.1 shuffle算子分类

  • Spark中会产生shuffle的算子大概可以分为4类 image.png
  • Partition(分区):在map,reduce过程中,分离数据集为小块
  • Key(Key - value):聚合相同性质key类
  • jion : 将本身没在一起数据按某种条件链接计算(SQL笛卡尔)
  • distinct : 唯一,消重,可归为Key

3.2 Shuffle算子应用

Spark源码中RDD的单元测试
spark/RDDSuite.scala at master · apache/spark · GitHub Spark源码中PairRDDFunctions的单元测试
spark/PairRDDFunctionsSuite.scala at master · apache/spark · GitHub

val text = sc.textFile ( "mytextfile.txt" ) //打开文件
val counts = text
    .flatMap( line => line.split(" ") ) //拆分行,空格split,拆分单词
    .map(word => (word ,1)) //每单词map转pair(单词,1)
    .reduceByKey(_+_) //触发Shuffle算子,sum数据,a+b
counts.collect //action算子,反馈计算结果

3.3 Spark中对shuffle的抽象–宽依赖、窄依赖

  • 窄依赖:父RDD的每个分片至多被子RDD 中的一个分片所依赖
  • 宽依赖:父RDD中的分片可能被子RDD中的多个分片所依赖 image (50).png map:C->D 窄依赖,不影响其他数据 union:D/E -> F 窄依赖 数据移动:(groupBy)A->B,(join)B/F->G 宽依赖 宽依赖:Spark拆分运算为两个Stage:map与reduce,中间产生Shuffle操作

3.4 Spark中算子内部的依赖关系

image.png 即调用join,创建CoGroupedRDD,调用创建ShuffleDependency算子
形成上述树状结构

3.4.1 ShuffleDependency构造

  • A single key-value pair RDD, i.e.RDD[Product2[K,V]]/KVpair键值对RDD,即RDD[Product 2[K,V]]

  • Partitioner (available as partitioner property)/分区(给key时产生key对应分区)

  • Serializer/串行化器,对象映射为二进制数据流(反之)

  • Optional key ordering (of Scala's scala.math.Ordering type)/可选Key排序(Scala的scala.math.Orting类型)

  • Optional Aggregator/可选聚合器

  • mapSideCombine flag which is disabled (i.e. false) by default/默认情况下禁用(即false)的mapSideCombin标志

3.4.2 Shuffle Dependency构造– Partitioner(分区)

Partitioner(分区)简介:将Key映射为数字,数字 代表具体分区

两个接口:

  • numberPartitions(一共多少分区)
  • getPartition(Key对应分区)

经典实现:

  • HashPartitioner(默认/金典实现,传入总Partitioner数数量,接口1:返回数量,接口2:取Key的hash扣(Code数),总Partitioner取模,返回非负值,得Key对应Partitioner)
abstract class Partitioner extends serializable {
    def numPartitions: Int
    def getPartition(key : Any ) : Int
    }
class HashPartitioner(partitions: Int) extends Partitioner {
    require(partitions >= 0s"Number of partitions ($partitions) cannot be nega1
    
    def numPartitions: Int = partitions

    def getPartition(key : Any) : Int = key match {
        case null => 0
        case _ => Utils.nonNegativeMod(key.hashcode, numPartitions)
        }
    }

3.4.3 Shuffle Dependency构造-Aggregator(性能优化器)

Aggregator(性能优化器)简介:将Reduce部分工作移至Map
如:wordcount:排序,分发(文本)至Reducer数数(数据量大),故,在Map将重复数据统计单词个数,减少分发量

  • createCombiner :只有一个value的时候初始化的方法
  • mergeValue :合并一个value 到Aggregator中,如:累加时Aggregator对应+1
  • mergeCombiners :合并两个Aggregator,如:+sum+count

四、shuffle过程

Spark中shuffle的核心原理

4.1. 发展

主要分为Hash Shuffle 和 Sort shuffle image.png

4.2. Shuffle 写数据

4.2.1 Hash Shuffle

  • 写数据 每个partition会映射到一个独立的文件,写满则存入磁盘,写O(m)无限文件(问题:生成文件太多,对文件系统压力大,同时打开文件太多) image (51).png
  • 写数据优化 每个partition会映射到一个文件片段,打开r个文件,写O(c*r=m,c:CPU核数)无限文件 image (52).png

4.2.2 Sort shuffle

思路:不给每个Partition一个PairBuffer,数据写一个Buffer里面,内存满时,sort方式将相同partiton合并(缺点:更多CPU,优点:task只创建两个文件,一个index(偏移量),一个FileSegment)

  • 写数据 每个task生成一个包含所有partiton数据的文件 image (53).png

4.3. shuffle 读数据

每个reduce task分别获取所有map task生成的属于自己的片段 image (54).png

4.4. Shuffle过程的触发流程

val text = sc.textFile ( "mytextfile.txt" ) //打开文件
val counts = text
    .flatMap( line => line.split(" ") ) //拆分行,空格split,拆分单词
    .map(word => (word ,1)) //每单词map转pair(单词,1)
    .reduceByKey(_+_) //触发Shuffle算子,sum数据,a+b
counts.collect //action算子,反馈计算结果

image.png

4.5. Shuffle Handle的创建

Register Shuffle时做的最重要的事情是根据不同条件创建不同的shuffle Handle image (5).jpeg

4.6. Shuffle Handle 与Shuffle Writer的对应关系

image (6).jpeg

4.7. Writer 实现- BypassMergeShuffleWriter

  • 不需要排序,节省时间,适用partiton较少数据
  • 写操作的时候会打开大量文件(不同partiton能直接灌入不同文件,默认200个文件)
  • 类似于Hash Shuffle,但会merge,生成两种文件index,data image (7).jpeg

4.7.1 Writer 实现-UnsafeShuffleWriter

  • 排序,适用partiton较多数据

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

  • 数据写入后不再反序列化 image (8).jpeg

  • 堆外内存管理,记原原信息

  • 根据partition 只排序 Long Array

  • 数据不移动(非宽依赖) image (9).jpeg

4.7.2 Writer 实现-SortShuffleWriter

  • 不对外,直接堆内
  • 支持combine(合并)
  • 需要combine时,使用PartitionedAppendOnlyMap,本质是个HashTable(哈希表)
  • 不需要combine时PartitionedPairBuffer本质是个array(列) image (10).jpeg

4.8. Reader实现-网络时序图

  • 使用基于netty的网络通信框架
  • 位置信息记录在MapOutputTracker中
  • 主要会发送两种类型的请求
  1. OpenBlocks请求
  2. Chunk请求或Stream请求 image (55).png

4.8.1 Reader 实现- ShuffleBlockFetchlterator

  • 区分local和remote节省网络消耗
  • 防止OOM
  1. maxByteslnFlight
  2. maxReqslnFlight
  3. maxBlockslnFlightPerAddress
  4. maxReaSizeShuffleToMem
  5. maxAttemptsOnNettyOOM image (56).png

4.8.2 Read 实现- External Shuffle Service

ESS作为一个存在于每个节点上的agent(代理)为所有Shuffle Reader提供服务,从而优化了Spark作业的资源利用率,MapTask在运行结束后可以正常退出
ESS能解开(解耦)传输多个reduce端口数据 image (57).png

4.9. Shuffle 优化使用的技术–Zero Copy

DMA(Direct Memory Access):直接存储器存取,是指外部设备不通过CPU而直接与系统内存交换数据的接口技术。

input流上文:use态

内核态:Application context
output流下文:NIC buffer:网卡

  • 不使用zero copy(零拷贝) image (4).gif

  • 使用sendfile(发送文件) image (7).gif

  • 使用sendfile +DMA gather copy(减少数据传输) image (6).gif

4.9.1 Shuffle优化使用的技术:Netty Zero Copy

  • 可堆外内存,避免JVM堆内存到堆外内存的数据拷贝
  • CompositeByteBuf 、Unpooled.wrappedBuffer、ByteBuf.slice,可以合并、包装、切分数组,避免发生内存拷贝
  • Netty使用FileRegion 实现文件传输,FileRegion底层封装了FileChannel#transferTo(方法,可以将文件缓冲区的数据直接传输到目标Channel,避免内核缓冲区和用户态缓冲区之间的数据拷贝

4.10. 常见问题

  • 数据存储在本地磁盘,没有备份
  • IO并发:大量RPC请求(M*R)
  • IO吞吐:随机读、写放大(3X,三次读)
  • GC频繁(创建回收),影响NodeManager

4.11. Shuffle优化

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

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

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

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

4.12. Shuffle参数优化

  • spark.default.parallelism && spark.sql.shuffle.partitions(默认并发度,高随机读,低task负荷大)

  • spark.hadoopRDD.ignoreEmptySplits(开始减少不必要partiton,如空)

  • spark.hadoop.mapreduce.input.fileinputformat.split.minsize(partiton合并,task多处理)

  • spark.sql.file.maxPartitionBytes(SparkSQL,规定数据量,切分)

  • spark.sql.adaptive.enabled && spark.sql.adaptive.shuffle.targetPostShuffleInputSize(AQE)

  • spark.reducer.maxSizeInFlight(防止read,降速,防磁盘爆,Shuffle大小)

  • spark.reducer.maxReqsInFlight
    spark.reducer.maxBlocksInFlightPerAddress

4.13. Shuffle倾斜优化

  • Shuffle 倾斜优化

    • 什么叫倾斜?有什么危害:
      • 作业运行时间变长
      • Task OOM导致作业失败
      • image (13).jpeg
    • 解决倾斜方法举例
      • 增大并发度/提高并行度
        • image (14).jpeg
        • 优点:足够简单
        • 缺点:只缓解、不根治
      • AQE(Spark AOE Skew Join)
        • AQE根据shuffle文件统计数据自动检测倾斜数据,将那些倾斜的分区打散成小的子分区,然后各自进行join
        • image (59).png
        • image (60).png

4.14. 案例-参数优化

  • ad_show
    • number of files read: 840,042. /读取的文件数:840,042
    • number of total tasks: 5,553. /总任务数:5 553项
    • size of files read: 203.3 TiB/ 读取的文件大小:203.3TiB
    • number of output rows: 128,676,054,598/输出行数:128 676 054 598
select
    ad_type,
    sum( click ),
    sum( duration )
from ad_show
where date = '20220101'
group by
    ad_typel

4.15. 参数调整

image.png


五、Push Shuffle

Push Shuffle各社区包括Spark3.2的实现方案以及字节方案

5.1 为什么需要Push Shuffle

  • Avg I0 size太小,造成了大量的随机IO,严重影响磁盘的吞吐
  • M * R次读请求,造成大量的网络连接,影响稳定性 image (15).jpeg

5.2 Push Shuffle的实现

为了优化该问题,有很多公司都做了思路相近的优化,push shuffle(提前合并数据)

5.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数据 image (61).png

  • bitmap:存储已merge的mapper id,防止重复merge

  • position offset:如果本次block没有正常merge,可以恢复到上一个block的位置

  • currentMapld(锁,仅本区回溯):标识当前正在append的block,保证不同mapper的block能依次append image (16).jpeg

5.4 Magnet可靠性(回溯进度)

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

5.5 Cloud Shuffle Service思想

image.png

5.6 Cloud Shuffle Service架构

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

5.6.1 Cloud Shuffle Service写入流程

选择以左(风险低,两个一起出错) image (62).png

5.6.2 Cloud Shuffle Service读取流程

write以右 image (63).png

5.6.3 Cloud Shuffle Service AQE

为了不在读多边,仅读一次,不再追求连续文件
一个Partition会最终对应到多个Epoch file,每个EPoch目前设置是512MB
image.png

5.7. 实践案例- CSS优化

  • XX业务小时级任务(1.2wcores)
  • 混部队列2.5h ->混部队列+CSS1.3h (50%提升) image (64).png

晚安玛卡巴卡

快乐暑假