这是我参与「第四届青训营 」笔记创作活动的第6天
Shuffle原理
大量的网络连接和数据移动,数据的丢失风险,涉及排序、序列化或反序列化,数据压缩让shuffle对于性能的要求十分的高。
shuffle算子
分类
一般有3类
- 改变分区
- 聚合ByKey
- join
- 去重Distinct(特殊的ByKey)
在Spark中Shuffle被抽象为宽依赖和窄依赖
算子内部的依赖关系
graph LR;
A(ShuffleDependency)
A1(CoGroupedRDD)
A11(CoGroup)
A111(fullOuter, JoinrightOuterJoin, leftOuterJoin)
A112(join)
A2(ShuffledRDD)
A21(combineByKeyWithClassTag)
A211(combineByKey)
A212(reduceByKey)
A22(Coalesce)
A23(sortByKey)
A231(sortBy)
A --> A1
A --> A2
A1 --> A11
A2 --> A21
A2 --> A22
A2 --> A23
A11 --> A111
A11 --> A112
A21 --> A211
A21 --> A212
A23 --> A231
shuffle 过程
Hash Shuffle
写数据时,每个partition会映射到一个独立文件(运行过程中生成和打开的文件会很多)
优化: 每个partition会映射到一个文件片段(老问题仍然存在)
Sort Shuffle
写数据时,每个task生成一个包含partition数据的文件(sort会很消耗cpu)
读数据是,每个reduce task分别获取每个map task生成的属于自己的片段
Shuffle过程的触发流程
graph LR;
A(Conllect Action)
B(Submit Job)
C(Get Dependences)
D(Register Shuffle)
A --> B --> C --> D
Register Shuffle时做的最重要的事情就是 根据不同的条件创建不同的Shuffle Handle ,不同的Shuffle Handle对应不同的Shuffle Writer
Writer实现
- BypassMergeShuffleWriter: 不排序,写操作产生大量文件,类似Hash Shuffle
- UnsafeShuffleWriter: 只根据partition排序Long Array,数据不移动,使用堆外内存
- SortShuffleWriter: 支持combine (需要combine时,使用PartitionedAppendOnlyMap本质是HashTable,不需要combine时,PartitionedPairBuffer本质是Array) ,对partition,keyvalue排序,使用堆内内存
Reader实现 - ShuffleBlockFetcherIterator
BlockStoreShuffleReader.read()读取数据有五步:
- 初始化ShuffleBlockFetcherIterator,负责从executor中获取 shuffle 块
- 将shuffle 块反序列化为record迭代器
- reduce端聚合数据
- reduce端排序数据
- 返回结果集迭代器
Shuffle 优化
Zero Copy
DMA(Direct Memory Access): 直接存储器存取,指外部设备不通过CPU,直接和系统内存交换数据的接口技术。
zero copy就像DMA一样通过减少CPU的参与来优化Shuffle
Netty Zero Copy
- 可堆外内存:避免JVM堆内存到堆外存的数据拷贝
- 一些对象可以合并、包装、切分数组,避免发生内存拷贝
- Netty使用FileRegion实现文件传输,FileRegion底层封装了 FileChannel#transferto() 方法,可以将文件缓冲区的数据直接传输到目标Channel,避免内核缓冲区和用户态缓冲区之间的数据拷贝。
使用broadcast代替join
传统的Join操作是会导致Shuffle的,若某一个RDD较小可以使用broadcast代替join避免Shuffle操作。
map-side预聚合
例如进行wordcount,在map之后进行一次count,而之后的reduce则是进行sum操作,这样可以压缩Shuffle的数据量。
参数优化
- 默认的并发度: spark.default.parallelism && spark.sql.shuffle.partitions
- 去掉空的partition: spark.hadoopRDD.ignoreEmptySplits
- 读取hdfs数据时可做合并: spark.hadoop.mapreduce.input.fileinputformat.split.minszie
- 读取数据时规定task应处理的数据量: spark.sql.file.maxPartitionBytes
- 动态调整reduce的partition数量: spark.sql.adaptive.enabled && spark.sql.adaptive.shuffle.targetPostShuffleInputSize
倾斜优化
shuffle倾斜指的是源数据的原因导致reduce收到的数据严重倾斜于某一个reduce task。这样会影响作业的运行时间,甚至是task OOM导致作业失败。
常见的处理方式:
- 提高并行度
- Spark3.0 AQE