大数据Shuffle原理与实践 | 青训营笔记

128 阅读4分钟

这是我参与「第四届青训营 」笔记创作活动的第5天

1. Shuffle概述

1.1 MapReduce 概述

  • 2004年谷歌发布了《MapReduce:Simplified Data Processing on Large Clusters》论文
  • 在开源实现的MapReduce中,存在Map、Shuffle、Reduce三个阶段

1.1.1 经典shuffle过程

image-20220809192839018

1.2 Map阶段

  • Map阶段,是在单机上进行的针对一小块数据的计算过程。

image-20220809193031680

1.3 Shuffle阶段

  • Shuffle阶段,在map阶段的基础上,进行数据移动,为后续的reduce阶段做准备。

image-20220809193219217

1.4 Reduce 过程

  • reduce阶段,对移动后的数据进行处理,依然是在单机上处理一小份数据。

image-20220809193323472

1.5 为什么shuffle如此重要

  • 数据shuffle表示了不同分区数据交换的过程,不同的shuffle策略性能差异较大。目前在各个引擎中shuffle都是优化的重点,在spark框架中,shuffle是支撑spark进行大规模复杂数据处理的基石。

2. Shuffle算子

2.1 Shuffle算子分类

  • Spark中会产生shuffle的算子大概可以分为4类

image-20220809193756645

2.2 Shuffle 算子使用案例

val text = sc.textFile("mytextfile.txt")
val counts = text
  .flatMap(line => line.split(" "))
  .map(word => (word,1))
  .reduceByKey(_+_)
counts.collect

2.3 Spark中对shuffle的抽象 - 窄依赖、宽依赖

  • 窄依赖

    • 父RDD的每个分片至多被子RDD中的一个分片所依赖
  • 宽依赖

    • 父RDD的每个分片可能被子RDD中的多个分片所依赖

2.4 算子内部的依赖关系

image-20220809194349641

2.4.1 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.

2.4.1.1 Partitioner

用来将record映射到具体的Partition的方法

  • 两个接口

    • numberPartitions
    • getPartition

image-20220809194812662

  • 经典实现

    • HashPartitioner

image-20220809194849812

2.4.1.2 Aggregator

在map侧合并部分record的函数

  • 接口

    • createCombiner:只有一个value的时候初始化的方法
    • mergeValue:合并一个value到Aggregator中
    • mergeCombiners:合并两个Aggregator

3. Shuffle过程

  • spark中的shuffle变迁过程

image-20220809195137234

  • HashShuffle

    • 优点:不需要排序
    • 缺点:打开,创建的文件过多
  • SortShuffle

    • 优点:打开的文件少、支持map-side combine
    • 缺点:需要排序
  • TungstenSortShuffle

    • 优点:更快的排序效率,更高的内存利用效率
    • 缺点:不支持map-side combine

3.1 Shuffle过程的触发流程

image-20220809200303720

3.2 Shuffle Handle的创建

  • Register Shuffle

    • 由action算子触发DAG Scheduler进行shuffle register
    • Shuffle Register会根据不同的条件决定注册不同的ShuffleHandle

image-20220809195817176

3.3 Shuffle Handle与Shuffle Writer的对应关系

image-20220809195942003

  • BypassMergeSortShuffleWriter:HashShuffle
  • UnsafeShuffleWriter:TunstonShuffle
  • SortSHuffleWriter:SortShuffle

3.4 Reader实现

image-20220809200433444

  • 使用基于netty的网络通信框架

  • 位置信息记录在MapOutPutTrack中

  • 主要会发送两种类型的请求

    • OpenBlocks请求
    • Chunk请求或Stream请求

3.4.1 ShuffleBlockFetchIterator

  • 区分local和remote节省网络消耗

  • 防止OOM

    • maxBytesInFlight
    • maxReqsInFlight
    • maxBlocksInFlightPerAddress
    • maxReqSizeShuffleToMem
    • maxAttemptsOnNettyOOM

3.4.2 External Shuffle Service

image-20220809201020207

  • ESS作为一个存在于每个节点上的agent为所有Shuffle Reader提供服务,从而优化了Spark作业的资源利用率
  • MapTask在运行结束后可以正常退出

3.5 Shuffle优化

  • 常见问题

    • 数据存储在本地磁盘,没有备份
    • IO并发:大量RPC请求(M*R)
    • IO吞吐:随机读、写放大(3X)
    • GC频繁,影响NodeManager
  • 避免shuffle

    • 使用broadcast替代join
//因为两个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预聚合的算子

3.5.1 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

3.5.2 Shuffle 倾斜优化

  • 什么叫倾斜?

image-20220809202627096

  • 有什么危害

    • 作业运行时间长
    • Task OOM导致作业失败
  • 解决倾斜方法举例

    • 增大并发度
    • AQE

3.5.3 AQE Skew Join

  • AQE根据shuffle文件统计数据自动检测倾斜数据,将那些倾斜的分区打散成小的子分区,然后各自进行join

4. Push Shuffle

4.1 为什么要push shuffle?

  • Avg IO size太小,造成了大量的随机IO,严重影响磁盘的吞吐
  • M*R次读请求,造成大量的网络连接,影响稳定性

image-20220809203244651

4.2 Push Shuffle的实现

  • 为了优化该问题,有很多公司都做了思路相近的优化,push shuffle

4.3 Magnet的主要流程

image-20220809203431951

image-20220809203443949

4.4 Cloud Shuffle Sservice 思想

image-20220809203709364

4.5 Cloud Shuffle Sservice 架构

image-20220809203748915

  • Zookeeper WorkerList [服务发现]
  • CSS Worker [Partitions / Disk | Hdfs]
  • Spark Driver [集成启动 CSS Master]
  • CSS Master [Shuffle 规划 / 统计]
  • CSS ShuffleClient [Write / Read]
  • Spark Executor [Mapper + Reducer]

4.6 Cloud Shuffle Sservice 读写流程

image-20220809203946164

4.7 Cloud Shuffle Sservice AQE

image-20220809204051644

  • 一个Partition会最终对应到多个Epoch file,每个EPoch目前设置是512MB