青训营 大数据 Shuffle 原理与实践
2022の夏天,半壶水响叮当的我决定充实一下自我
一、内容介绍
青训营
总述
- shuffle概述:shuffle是什么,shuffle的基本过程是什么
- spark中的shuffle算子:学习使用spark中的会产生shuffle的算子,了解其基本特性
- spark中的shuffle过程:spark中shuffle的核心原理和实现细节
- Push based shuffle:push shuffle社区的实现方案以及字节自己的实现方案
目标
- 了解spark中shuffle的发展历史以及主要实现机制。包括如何划分stage、partition分区、spill、combine等。
- 了解spark shuffle的底层实现原理以及Push-based shuffle总体架构方案。
- 了解shuffle优化,包括实际业务场景下如何避免产生shuffle、减少shuffle数据量、shuffle参数优化等。
预知
-
熟悉spark中会产生shuffle的算子:
- 重分区
- ByKey
- Join
-
使用spark的RDD算子,完成WordCount功能并运行,并观察作业的stage划分
-
使用spark分析CSV文件,并编写相关的sql,其中包含join,并观察作业的stage划分以及sql plan
-
熟悉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三个阶段。
Partition:在map,reduce过程中,分离数据集
Shuffle:并发合并移动数据集 -
Map阶段,是在单机上进行的针对一小块数据的计算过程
-
Shufle阶段,在map阶段的基础上,进行数据移动,为后续的reduce阶段做准备。
-
reduce阶段,对移动后的数据进行处理,依然是在单机上处理一小份数据
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类
- 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中的多个分片所依赖
map:C->D 窄依赖,不影响其他数据 union:D/E -> F 窄依赖 数据移动:(groupBy)A->B,(join)B/F->G 宽依赖 宽依赖:Spark拆分运算为两个Stage:map与reduce,中间产生Shuffle操作
3.4 Spark中算子内部的依赖关系
即调用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 >= 0, s"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
4.2. Shuffle 写数据
4.2.1 Hash Shuffle
- 写数据
每个partition会映射到一个独立的文件,写满则存入磁盘,写O(m)无限文件(问题:生成文件太多,对文件系统压力大,同时打开文件太多)
- 写数据优化
每个partition会映射到一个文件片段,打开r个文件,写O(c*r=m,c:CPU核数)无限文件
4.2.2 Sort shuffle
思路:不给每个Partition一个PairBuffer,数据写一个Buffer里面,内存满时,sort方式将相同partiton合并(缺点:更多CPU,优点:task只创建两个文件,一个index(偏移量),一个FileSegment)
- 写数据
每个task生成一个包含所有partiton数据的文件
4.3. shuffle 读数据
每个reduce task分别获取所有map task生成的属于自己的片段
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算子,反馈计算结果
4.5. Shuffle Handle的创建
Register Shuffle时做的最重要的事情是根据不同条件创建不同的shuffle Handle
4.6. Shuffle Handle 与Shuffle Writer的对应关系
4.7. Writer 实现- BypassMergeShuffleWriter
- 不需要排序,节省时间,适用partiton较少数据
- 写操作的时候会打开大量文件(不同partiton能直接灌入不同文件,默认200个文件)
- 类似于Hash Shuffle,但会merge,生成两种文件index,data
4.7.1 Writer 实现-UnsafeShuffleWriter
-
排序,适用partiton较多数据
-
使用类似内存页储存序列化数据
-
数据写入后不再反序列化
-
堆外内存管理,记原原信息
-
根据partition 只排序 Long Array
-
数据不移动(非宽依赖)
4.7.2 Writer 实现-SortShuffleWriter
- 不对外,直接堆内
- 支持combine(合并)
- 需要combine时,使用PartitionedAppendOnlyMap,本质是个HashTable(哈希表)
- 不需要combine时PartitionedPairBuffer本质是个array(列)
4.8. Reader实现-网络时序图
- 使用基于netty的网络通信框架
- 位置信息记录在MapOutputTracker中
- 主要会发送两种类型的请求
- OpenBlocks请求
- Chunk请求或Stream请求
4.8.1 Reader 实现- ShuffleBlockFetchlterator
- 区分local和remote节省网络消耗
- 防止OOM
- maxByteslnFlight
- maxReqslnFlight
- maxBlockslnFlightPerAddress
- maxReaSizeShuffleToMem
- maxAttemptsOnNettyOOM
4.8.2 Read 实现- External Shuffle Service
ESS作为一个存在于每个节点上的agent(代理)为所有Shuffle Reader提供服务,从而优化了Spark作业的资源利用率,MapTask在运行结束后可以正常退出
ESS能解开(解耦)传输多个reduce端口数据
4.9. Shuffle 优化使用的技术–Zero Copy
DMA(Direct Memory Access):直接存储器存取,是指外部设备不通过CPU而直接与系统内存交换数据的接口技术。
input流上文:use态
内核态:Application context
output流下文:NIC buffer:网卡
-
不使用zero copy(零拷贝)
-
使用sendfile(发送文件)
-
使用sendfile +DMA gather copy(减少数据传输)
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预聚合的算子
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导致作业失败
- 解决倾斜方法举例
- 增大并发度/提高并行度
- 优点:足够简单
- 缺点:只缓解、不根治
- AQE(Spark AOE Skew Join)
- AQE根据shuffle文件统计数据自动检测倾斜数据,将那些倾斜的分区打散成小的子分区,然后各自进行join
- 增大并发度/提高并行度
- 什么叫倾斜?有什么危害:
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. 参数调整
五、Push Shuffle
Push Shuffle各社区包括Spark3.2的实现方案以及字节方案
5.1 为什么需要Push Shuffle
- Avg I0 size太小,造成了大量的随机IO,严重影响磁盘的吞吐
- M * R次读请求,造成大量的网络连接,影响稳定性
5.2 Push Shuffle的实现
为了优化该问题,有很多公司都做了思路相近的优化,push shuffle(提前合并数据)
- Facebook: cosco
- LinkedIn:magnet
- Uber:Zeus
- Alibaba: RSS
- Tencent: FireStorm
- Bytedance: Cloud Shuffle Service
- Spark3.2: push based 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数据
-
bitmap:存储已merge的mapper id,防止重复merge
-
position offset:如果本次block没有正常merge,可以恢复到上一个block的位置
-
currentMapld(锁,仅本区回溯):标识当前正在append的block,保证不同mapper的block能依次append
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思想
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写入流程
选择以左(风险低,两个一起出错)
5.6.2 Cloud Shuffle Service读取流程
write以右
5.6.3 Cloud Shuffle Service AQE
为了不在读多边,仅读一次,不再追求连续文件
一个Partition会最终对应到多个Epoch file,每个EPoch目前设置是512MB
5.7. 实践案例- CSS优化
- XX业务小时级任务(1.2wcores)
- 混部队列2.5h ->混部队列+CSS1.3h (50%提升)
晚安玛卡巴卡
快乐暑假