这是我参与「第四届青训营」笔记创作活动的第6天
0 引言
1 Shuffle概述
MapReduce概述:开始是Hadoop的MapReduce模块开源实现的分布式计算架构。
input -> map -> shuffle -> reduce -> output
Map阶段:在单机上进行的针对—小块数据的计算过程。(比如按行统计单词个数)
Shufle阶段:在map阶段的基础上,进行数据移动,为后续的reduce阶段做准备。(相同的单词在一组)
Reduce阶段:对移动后的数据进行处理,依然是在单机上处理一小份数据。(对同组相同单词合并统计个数)
Shuffle对性能非常重要原因(Shuffle可以优化,是Spark数据复杂处理的基石):
- M*R次网络连接
- 大量的数据移动
- 数据丢失风险
- 可能存在大量的排序操作
- 大量的数据序列化、反序列化操作
- 数据压缩
2 Shuffle算子
Shuffle算子分类/应用:
Spark原码中RDD的单元测试:github.com/apache/spar…
Spark原码中PairRDDFunctions的单元测试:github.com/apache/spar…
val text = sc.textFile( "'mytextfile.txt") //导入单词
val counts = text
.flatMap(line => line.split(" ")) //按照行空格分开每个单词
.map(word =>(word,1)) //将每个单词修改为(word,1)格式
.reduceByKey(_+_) //按照K就是单词进行统计,V就是1 进行求和
counts.collect //打印
Spark中对Shuffle的抽象:
- 窄依赖:父RDD的每个分片至多被子RDD中的一个分片所依赖
- 宽依赖:父RDD中的分片可能被子RDD中的多个分片所依赖(产生宽依赖就会增加两个Stage:Map、Reduce)
算子内部的依赖关系(宽依赖时):
Shuffle Dependency构造:
- A single key-value pair RDD, i.e.RDD[Product2[K,V]]。(KV对的RDD)
- Partitioner (available as partitioner property)。(传入K后给定一个分区)
- Serializer。(对象<->二进制数据流)
- Optional key ordering (of Scala's scala.math.Ordering type)。(对K进行排序)
- Optional Aggregator。
- mapSideCombine flag which is disabled (i.e. false) by default。(flag描述shuffle过程mapSideCombine)
Partitioner:两个接口numberPartitions和getPartition。经典实现HashPartitioner。
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)
}
}
Aggregator:
- createCombiner:只有一个value的时候初始化的方法
- mergeValue:合并一个value到Aggregator中
- mergeCombiners:合并两个Aggregator
3 Shuffle过程
-
Hash Shuffle写:
每个partition会映射到一个独立的文件。导致每个partition会生成一个file。
优化:每个partition会映射到一个文件片段。 -
Sort Shuffle写:
每个task生成一个包含所有partiton数据的文件。一个task在一个文件,文件各个位置存储对应的partiton。 -
Shuffle读:
每个reduce task分别获取所有map task生成的属于自己的片段。直接读取对应的task文件就可以。
Shuffle的过程触发流程:
CollectAction -> SubmitJob -> GetDependencies -> RegisterShuffle
Shuffle的Handle创建:
在Register Shuffle时根据不同条件创建不同的shuffle Handle。
BypassMergeSortShuffleHandle <-> BypassMergeSortShuffleWriter
SerializedShuffleHandle <-> UnsafeShuffleWriter
BaseShuffleHandle <-> SortShuffleWriter
Shuffle Writer实现:
// 通过shuffleHandle来决定是哪一种ShuffleWrite
handle match {
case unsafeShuffleHandle: SerializedShuffleHandle[K @unchecked, V @unchecked] =>
new UnsafeShuffleWriter(
env.blockManager,
shuffleBlockResolver.asInstanceOf[IndexShuffleBlockResolver],
context.taskMemoryManager(),
unsafeShuffleHandle,
mapId,
context,
env.conf)
case bypassMergeSortHandle: BypassMergeSortShuffleHandle[K @unchecked, V @unchecked] =>
new BypassMergeSortShuffleWriter(
env.blockManager,
shuffleBlockResolver.asInstanceOf[IndexShuffleBlockResolver],
bypassMergeSortHandle,
mapId,
context,
env.conf)
case other: BaseShuffleHandle[K @unchecked, V @unchecked, _] =>
new SortShuffleWriter(shuffleBlockResolver, other, mapId, context)
}
- BypassMergeShuffleWriter:不需要排序,节省时间;写操作的时候会打开大量文件类似于Hash Shuffle。【选这个时没有开启combine并且下游分区数<200,适用于groupByKey、sortByKey,且下游分区数不能超过阈值(默认200)】
- UnsafeShuffleWriter:使用类似内存页储存序列化数据;数据写入后不再反序列化。【序列化形式,要求shuffle不能带聚合器,像reduceByKey,groupByKey是不能用的。sortByKey可以,repartition也可以】
- SortShuffleWriter:支持combine;需要combine时,使用PartitionedAppendOnlyMap,本质是个HashTable;不需要combine时PartitionedPairBuffer本质是个array。【写入内存缓冲区】
Reader实现:
-
网络时序图 使用基于netty的网络通信框架;位置信息记录在MapOutputTracker中;主要会发送两种类型的请求OpenBlocks请求、hunk请青求或Stream请求。
-
ShuffleBlockFetchlterator
区分local和remote节省网络消耗;防止OOM(maxByteslnFlight/ maxReqslnFlight/ maxBlockslnFlightPerAddress/ maxReqSizeShuffleToMem/ maxAttemptsOnNettyOOM) -
External Shuffle Service ESS作为一个存在于每个节点上的agent为所有Shuffle Reader提供服务,从而优化了Spark作业的资源利用率,MapTask在运行结束后可以正常退出。
Shuffle优化使用的技术:
- Zero Copy:避免了数据在用户空间和内存空间之间的拷贝,从而提高了系统的整体性能。
- Netty Zero Copy:FileRegion中包装了NIO的FileChannel.transferTo()方法实现了零拷贝。而在Netty中还有另一种形式的零拷贝,即Netty允许我们将多段数据合并为一整段虚拟数据供用户使用,而过程中不需要对数据进行拷贝操作。
Spark Shuffle常见问题:
- 数据存储在本地磁盘,没有备份
- IO并发:大量 RPC请求(M*R)
- IO吞吐:随机读、写放大(3X)
- GC频繁,影响NodeManager
Shuffle优化/参数优化/倾斜优化
- 避免shuffle,使用broadcast替代join。【对于小文件(较小数据RDD)可以进行广播变量】
- 使用可以map-side预聚合的算子。【比如word count,在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
- 数据倾斜是某些task分配的数据过大,导致作业运行时间变长和Task OOM导致作业失败。
一般解决办法可以是 提高并行度,但是只能缓解、不能根治。
可以使用AQE根据shuffle文件统计数据自动检测倾斜数据,将那些倾斜的分区打散成小的子分区,然后各自进行join。【系统检测大的task数据,将他进行拆分成小的task执行】
4 Push Shuffle
对于下列问题,很多企业做了个性化开发,文中介绍Magnet用法和ByteDance即将开源的CloudShuffleService架构。
- Avg lO size太小,造成了大量的随机lO,严重影响磁盘的吞吐
- M*R次读请求,造成大量的网络连接,影响稳定性
Magnet实现原理:
- Spark driver组件,协调整体的shuffle操作
- map任务的shuffle writer过程完成后,增加了一个额外的操作push-merge,将数据复制一份推到远程shuffle服务上
- magnet shuffle service是一个强化版的ESS。将隶属于同一个shuffle partition的block,会在远程传输到magnet后被merge到一个文件中
- reduce任务从magnet shuffle service接收合并好的shuffle数据
Cloud Shuffle Service思想:
- IO聚合:所有Mapper的同一Partition数据都远程写到同一个文件(或者多个文件)。
- 备份:HDFS太重,自定义文件存储,使用双磁盘副本(成本低、速度快)。
- 写入速度:主从InMemory副本,异步刷盘,极小的失败几率去换取高速写入速度。
注意:写入数据会产生重复数据,在读数据需要去重。
参考引用
1.【大数据专场 学习资料二】第四届字节跳动青训营
2. Spark的Shuffle总结分析
3. Spark ShuffleWriter
4. 零拷贝(Zero-copy)
5. 你知道Netty的零拷贝机制原理吗?这次带你彻底搞懂!