这是我参与「第五届青训营 」伴学笔记创作活动的第 22 天
这里把Shuffle单独做了一个分析,在结合具体的实践的基础上,主要围绕什么是 Shuffle、Shuffle 的基本过程有哪些、以及为什么 Shuffle 在大数据领域如此重要等问题,帮助大家对 Shuffle 建立初步的认知。Shuffle是非常重要的昂!!!
Shuffle概述
MapReduce概述
- 2004谷歌发布了 (MapReduce:Simplified Data Processing on Large Clusters) 论文
- 在开源实现的MapReduce中,存在Map、Shuffile. Reduce三个阶段
- Map:单机对于数据进行处理,排序
- Shuffle:数据移动,打散,为Reduce阶段做准备
- Reduce:对移动后的数据进行处理,依然是在单机上处理一小份数据。
为什么shuffle对性能非常重要
- M * R次网络连接:每一个Reducer都要访问所有的Mapper,去拿到这个Reducer对应的数据。
- 大量的数据移动:M * R次数据移动
- 数据丢失风险:网络异常等异常,重算
- 可能存在大量的排序操作:Map之后,可能需要排序 -> Shuffle,颜色进行排序
- 大量的数据序列化、反序列化操作
- 数据压缩
总结
- 在大数据场景下,数据shfle表示了不同分区数据交换的过程,不同的shufle策略性能差异较大。
- 目前在各个引擎中shuffle都是优化的重点,在spark框架中,shuffle是支撑spark进行大规模复杂数据处理的基石。
Shuffle算子
Shuffle算子分类
- spark中会产生shuffle等算子,大概可以分成4类:
Shuffle算子使用
- 数据移动的需求,就是Shuffle来实现的
Spark对于shuffle的抽象-宽依赖、窄依赖
- 窄依赖: 父RDD的每个分片至多被子RDD中的一个分片所依赖
- 宽依赖: 父RDD中的分片可能被子RDD中的多个分片所依赖
图片来源: (Rеѕіlіеnt Dіѕtrіbutеd Dаtаѕеtѕ: А Fаult-Тоlеrаnt Аbѕtrасtіоn fоr Іn-Меmоrу Сluѕtеr Соmрutіng) 论文
当出现宽依赖(数据移动),Spark就会拆分成两个Stage,一个是Map,一个是Reduce,中间就会产生Shuffle操作。
算子内部的依赖关系
-
ShuffleDependency(宽依赖对应的依赖关系)
-
CoGroupedRDD
-
Cogroup
- fullOuterJoin, rightOuterJoin, leftOuterJoin
- join
-
-
ShuffledRDD
-
combineByKeyWithClassTag
- combineByKey
- reduceByKey
-
Coalesce
-
sortByKey
- sortBy
-
-
ShuffleDependency是被RDD创建的
当我们调用CoGroup操作或者Shuffle操作的时候,RDD就会被创建昂!
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
负责把一个Key映射成一个数字(具体的分区)
-
两个接口
- numberPartitions
- getPartition
-
经典实现
- HashPartitioner
算Hash再取模,就很方便昂!!!
Aggregator
Shuffle的时候,重要的性能优化器。把一部分Reduce的工作,放到Map来做,例如
- 原始WordCount: 单词排序 -> Shuffle(Mapper -> Reducer) -> Reduce(数数 + Sum)并拿到结果。
- 加入Aggregator的WordCount: 单词排序(同时数数) -> Shuffle(Mapper -> Reducer) -> Reduce(Sum)
下面的,明显Shuffle的过程中,分发传递的数据量,就少了很多很多昂!
- createCombiner : 只有一个value的时候初始化的方法
- mergeValue : 合并一个value到Aggregator中
- mergeCombiners : 合并两个Aggregator
Shuffle过程
History
- 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 Shuffle
- 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 推出历史舞台
Hash Shuffle-写数据
写数据
- 每个parition会映射到一个独立的文件,总共就是生成 M * R 个文件。
- 问题:随着作业规模增大,生成的文件太多,打开的文件也很多。耗费的资源太多了!!!
Hash Shuffle-写数据优化
每个Parition不是映射成一个文件,而是映射成一个文件片段。M * R -> CPU Num * R
没有完全解决,依然还是非常占用资源昂!!!
Sort Shuffle-写数据
每个task生成一个包含所有partition数据的文件
占用很多的CPU,所有的数据都写在同一个buffer里面,通过排序的方式,把所有的partition放在一起。最终就只生成一个数据文件(还有一个小的记录偏移量和Segment的元数据文件昂)
Shuffle-读数据
每个reduce task分别获取所有map task生成的属于自己的片段
Shuffle过程的触发流程
- collect的时候,实际才开始触发后续的流程哈
Shuffle Handler的创建
Register Shuffle时做的最重要的事情是根据不同条件创建不同的shuffle Handle。
Shuffle Handle和Shuffle Writer的关系
Writer实现-BypassMergeShuffleWriter
这个就是HashShuffle,不排序,节省CPU
适用于Partition较少的情况,中间文件少(一般都要<200)
- 不需要排序,节省时间
- 写操作的时候会打开大量文件
- 类似于Hash Shuffle
前半部分和Hash Shuffle一致。后半部分做了升级,最终会把文件merge到一起,减少文件数量,生成两个文件,merge。
merge用到了操作系统中的特性:zero-copy
Writer实现-UnsafeShuffleWriter
Unsafe: 用了堆外内存,没有用Java本身的内存,Why?
- 没有Java对象相关的开销
- 没有垃圾回收的开销
- 使用类似内存页储存序列化数据
- 数据写入后不再反序列化
也会merge。如果内存申请不到洞见,就会spill,最终也会把很多生成的spill给merge成两个文件,data file & index file
- 堆外内存管理:
- 只根据partition排序Long Array
- 数据不移动
- 为啥partition数量有限制2^24? -> Long Array里面的前24个bit就装不下partition了
Writer实现-SortShuffleWriter
- 支持combine
- 需要combine时候 , 使用PartitionedAppendOnlyMap , 本质是个HashTable
- 不需要combine时,PartitionedPairBuffer 本质是个 array
这个是在堆内哈,实现思路是一样的,最终也是合并成了两个文件。支持Map侧的Combine,
首先对于Partition进行排序,再对于Key进行排序。
为啥不堆外?堆外序列化,没用啊,害得堆内对于Key进行排序呢。
Reader实现
网络时序图
-
使用基于netty的网络通信框架
-
位置信息记录在MapOutputTracker中
-
主要会发送两种类型的请求
- OpenBlocks请求
- Chunk请求或Stream请求
ShuffleBlockFetchIterator
-
区分local和remote节省网络消耗
-
防止OOM
- maxBytesInFlight
- maxReqsInFlight
- maxBlocksInFlightPerAddress
- maxReqSizeShuffle ToMem
- maxAttemptsOnNettyOOM
三种Writer,但是只有一种Reader
对于远程数据做一个分割(构造一个远端的FetchRequest对象),随机放到一个请求队列里面(如果不是随机的,可能会导致Fetch热点问题。例如全部访问A,再全部访问B这样昂!)
External Shuffle Service
ESS很重要!!!
为了解耦数据计算和数据读取服务。
- 原始:谁去Write了数据,谁就要继续存活着,等着其他的Task来Fetch这些数据。
- ESS:单独的服务,运行在每台主机上,管理该主机,所有Executor生成的数据。计算单元,写完数据后,Executor就可以退出啦!!!
- ESS作为一一个存在于每个节点上的agent为所有Shuffle Reader提供服务,从而优化了Spark作业的资源利用率MapTask在运行结束后可以正常退出
Shuffle优化使用的技术
Zero Copy
Netty Zero Copy
- 可堆外内存,避免JVM堆内存到堆外内存的数据拷贝。(避免对内外内存的数据拷贝,如果不需要使用的话)
- CompositeByteBuf、Unpooled.wrappedBuffer、 ByteBuf.slice ,可以合并、包装、切分数组,避免发生内存拷贝。(包装可以不用再重新分配内存,例如1G,2G。我直接抽象一个3G的数组用就行了昂。)
- Netty使用FileRegion实现文件传输,FileRegion 底层封装了FileChannel#transferTo() 方法,可以将文件缓冲区的数据直接传输到目标Channel,避免内核缓冲区和用户态缓冲区之间的数据拷贝
常见问题
- 数据存储在本地磁盘,没有备份
- IO并发:大量RPC请求(M*R)
- IO吞吐:随机读、写放大(3X)
- GC频繁,影响NodeManager
Shuffle优化
- 避免shuffle: 使用broadcase代替join
- 使用可以map-side预聚合的算子
Reference: tech.meituan.com/2016/05/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
Shuffle倾斜优化
-
什么叫shuffle倾斜
-
倾斜影响
- 作业运行时间变长
- Task OOM导致作业失败
-
解决方法:
- 调整参数:提高并行度!!!有效缓解倾斜。缺点:只能缓解,不能根治。
- AQE Skew Join
Spark AQE Skew Join
- AQE根据shuffle文件统计数据自动检测倾斜数据,将那些倾斜的分区打散成小的子分区,然后各自进行join。
处理更加均匀了,等价于,又套了一个MR,把手上过多的数据,分发下去计算,再进行聚合昂!!!
案例-参数优化
- 参数调整:
Push Shuffle
Why Push Shuffle
- Avg IO size太小,造成了大量的随机IO,严重影响磁盘的吞吐
- M * R次读请求,造成大量的网 络连接,影响稳定性
Push Shuffle的实现
- Facebook: Cosco
- Linkedln: magnet
- Uber: Zeus
- Alibaba: RSS
- Tencent: FireStorm
- Bytedance: CSS
- Spark3.2: push based shuffle
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的位置
- currentMapId :标识当前正在append的block,保证不同mapper的block能依次append
Magnet可靠性
- 如果Map task输出的Block没有成功Push到magnet上,并且反复重试仍然失败,则reduce task直接从ESS.上拉取原始block数据
- 如果magnet上的block因为重复或者冲突等原因,没有正常完成merge的过程,则reduce task直接拉取未完成merge的block
- 如果reduce拉取已经merge好的block失败,则会直接拉取merge前的原始block
- 本质上,magnet中维护了两份shuffle数据的副本
万事不决,直接拉block就可以了昂!本质上就是维护了两份副本,一个是原始的,一个是我Aggregate之后,push上去的昂。
Cloud Shuffle Service思想
Cloud Shuffle Service架构
- Zookeeper WorkerList [BESSTU]
- CSS Worker [Partitions 1 Disk I Hdfs]
- Spark Driver [# 5XE Css Master]
- CSS Master [Shuffle #ЛtJ 1 5tit]
- CSS ShuffleClient [Write 1 Read]
- Spark Executor [Mapper + Reducer]
CSS写入流程
CSS读取流程
P0-0要读
P0-1和P0'-1任选一个就行
如果都寄了呢?hhhh,所以说,我们是冒了风险的昂!!!
Cloud Shuffle Service AQE
一个Partition会最终对应到多个Epoch file ,每个EPoch目前设置是512MB
- 不用把超大文件读很多遍,按需分配小文件就可以了昂。(超大和超小都有问题,取了一个折中)
实践案例-CSS优化
- XX业务小时级任务(1.2w cores)
- 混部队列2.5h ->混部队列 + CSS 1.3h (50%提升)
课程总结
-
Shuffle 概述
- 什么是shuffle,shuffle的基本流程
- 为什么shufle对性能影响非常重要
-
Shuffle 算子
- 常见的shuffle算子
- 理解宽依赖和窄依赖,ShuffleDependency及其相关组件
-
Shuffle 过程
- Spark中shuffle实现的历史
- Spark中主流版本的shuffle写入和读取过程
-
Push shuffle
- Magnet Push Shuffle的设计思路
- Cloud Shuffle Service的设计实现思路
References
- Spark源码中的RDD单元测试
- Spark源码中的PariRDDFunction的单元测试
- Spark-shuffle-introduction: www.slideshare.net/colorant/sp…
- zero copy: developer.ibm.com/articles/j-…
- 数据倾斜&解决方案:tech.meituan.com/2016/05/12/…