这是我参与 「第四届青训营 」 笔记创作活动的第5天
shuffle概述
-
Mapreduce
- 《MapReduce:Simplified Data Processing on Large Clusters》
-
经典shuffle过程
- map阶段
- shuffle阶段
- reduce阶段
-
为什么shuffle如此重要
- 数据shuffle表示了不同分区数据交换的过程,不同的shuffle策略性能差异较大。目前在各个引擎中shuffle都是优化的重点,在spark框架中,shuffle是支撑spark进行大规模复杂数据处理的基石。
shuffle算子
-
常见的触发shuffle的算子
-
repartition
- coalesce、repartition
-
ByKey
- groupByKey、reduceByKey、aggregateByKey、combineByKey、sortByKeysortBy
-
Join
- cogroup、join
-
| repartition | ByKey | Join | distinct |
|---|---|---|---|
| coalesce | groupByKey | cogroup | |
| repartition | reduceByKey | join | |
| aggregateByKey | |||
| combineByKey | |||
| sortByKeysortBy |
- 算子使用例子
val text = sc.textFile("mytextfile.txt")
val counts = text
.flatMap(line => line.split(" "))
.map(word => (word,1))
.reduceByKey(_+_)
counts.collect
复制代码
-
Shuffle Dependency
-
创建会产生shuffle的RDD时,RDD会创建Shuffle Dependency来描述Shuffle相关的信息
-
构造函数
- 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
-
用来将record映射到具体的partition的方法
-
接口
- numberPartitions
- getPartition
-
abstract class partition extends
-
-
Aggregator
-
在map侧合并部分record的函数
-
接口
- createCombiner:只有一个value的时候初始化的方法
- mergeValue:合并一个value到Aggregator中
- mergeCombiners:合并两个Aggregator
-
例如对于所有的单词,将其进行排序
-
在reducer进行count
-
shuffle过程(重点)
-
spark中的shuffle变迁过程
-
HashShuffle--写数据优化
- 优点:不需要排序
- 缺点:打开,创建的文件过多
-
SortShuffle
- 优点:打开的文件少、支持map-side combine
- 缺点:需要排序
-
TungstenSortShuffle
- 优点:更快的排序效率,更高的内存利用效率
- 缺点:不支持map-side combine
-
-
Register Shuffle
- register shuffle 时做的最重要的事情是根据不同条件创建不同的shuffle handle
- 由action算子触发DAG Scheduler进行shuffle register
- Shuffle Register会根据不同的条件决定注册不同的ShuffleHandle
-
三种ShuffleHandle对应了三种不同的ShuffleWriter的实现
-
-
BypassMergeSortShuffleWriter:HashShuffle
-
- 不需要排序,节省时间
- 写操作的时候会打开大量文件
- 类似于Hash Shuffle
- UnsafeShuffleWriter:TunstonShuffle
- 使用类似内存页存储序列化数据
- 数据写入后不再反序列化
- 只根据 partition 排序 Long Array
- 数据不移动
- SortSHuffleWriter:SortShuffle
- 支持 combine
- 需要combine时,使用partitionedAppendOnlyMap,本质是个HashTable
- 不需要combine时partitionedpairBuffer本质是个array
-
ShuffleReader网络请求流程
使用netty作为网络框架提供网络服务,并接受reducetask的fetch请求
首先发起openBlocks请求获得streamId,然后再处理stream或者chunk请求
位置信息记录在mapOutputtracker中,主要会发送两种类型的请求
-
ShuffleBlockFetchIterator
-
区分local和remote节省网络消耗
-
防止OOM
- maxBytesInFlight
- maxReqsInFlight
- maxBlocksInFlightPerAddress
- maxReqSizeShuffleToMem
- maxAttemptsOnNettyOOM
-
-
External Shuffle Service
为了解决Executor为了服务数据的fetch请求导致无法退出问题,我们在每个节点上部署一个External Shuffle Service,这样产生数据的Executor在不需要继续处理任务时,可以随意退出。
-
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预聚合的算子
-
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 倾斜优化
-
什么叫倾斜?有什么危害
-
解决倾斜方法举例
- 增大并发度
- AQE
-
-
-
零拷贝
- 不使用zero copy
- 使用sendfile
- 使用sendfile + DMA gather copy
-
Netty 零拷贝(netty zero copy)
- 可堆外内存,避免 JVM 堆内存到堆外内存的数据拷贝。
- CompositeByteBuf 、 Unpooled.wrappedBuffer、 ByteBuf.slice ,可以合并、包装、切分数组,避免发生内存拷贝
- Netty 使用 FileRegion 实现文件传输,FileRegion 底层封装了 FileChannel#transferTo() 方法,可以将文件缓冲区的数据直接传输到目标 Channel,避免内核缓冲区和用户态缓冲区之间的数据拷贝
Push Shuffle
-
上一部分所讲的shuffle过程存在哪些问题?
- 数据存储在本地磁盘,没有备份,一旦数据丢失,重算数据代价大
- IO 并发:大量 RPC 请求(M*R),消耗cpu
- IO 吞吐:随机读(降低读)、写放大(3X),
- GC 频繁,影响 NodeManager
-
为了优化该问题,有很多公司都做了思路相近的优化,push shuffle
-
为什么需要 push shuffle?
- AVG IO size 太小,造成大量的随机IO,严重影响磁盘的吞吐
- M*R 次读请求,造成大量的网络连接,影响性能
-
Magnet主要流程
- bitmap:存储已merge的mapper id ,防止重复merge
- position offset
-
magnet可靠性
- 如果map task 输出的Blook
主要为边写边push的模式,在原有的shuffle基础上尝试push聚合数据,但并不强制完成,读取时优先读取push聚合的结果,对于没有来得及完成聚合或者聚合失败的情况,则fallback到原模式。
-
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]
-
Cloud Shuffle Service 读写流程
-
Cloud Shuffle Service 支持AQE
- 在聚合文件时主动将文件切分为若干块,当触发AQE时,按照已经切分好的文件块进行拆分。