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

114 阅读7分钟

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

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

Shuffle介绍

在开源实现的MapReduce中,存在Map、Shuffle、Reduce三个阶段。

image-20220801094750050.png

为什么 shuffle 对性能非常重要

  • M * R 次网络连接
  • 大量数据移动
  • 数据丢失风险
  • 可能存在大量的排序操作
  • 大量的数据序列化、反序列化操作
  • 数据压缩

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

Shuffle 算子

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

repartitionByKeyjoinDistinct
coalescegroupByKeycogroupdistinct
repartitionreduceByKeyjoin
aggregateByKeyleftOuterJoin
combineByKeyintersection
sortByKeysubtract
sortBysubtractByKey

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

image-20220801032113040.png

  • 窄依赖:表示每一个父(上游)RDD的Partition最多被子(下游)RDD的一个Partition使用,窄依赖我们形象的比喻为独生子女。
    • NarrowDependency
    • OneToOneDependency
    • RangeDependency
    • PruneDependency
  • 宽依赖(会产生Shuffle):表示同一个父(上游)RDD的Partition被多个子(下游)RDD的Partition依赖,会引起Shuffle,总结:宽依赖我们形象的比喻为多生。
    • ShuffleDependency:创建会产生shuffle的RDD时,RDD会创建Shuffle Dependency来描述Shuffle相关的信息。下面为算子内部的依赖关系
      • CoGroupedRDD
        • Cogroup
          • fullOuterJoin、rightOuterJoin、 leftOuterJoin
          • join
      • ShuffledRDD
        • combineByKeyWithClassTag
          • combineByKey
          • reduceByKey
        • Coalesce
        • sortByKey
          • sortBy

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

  • 用来将record映射到具体的partition的方法
  • 接口
    • numberPartitions
    • getPartition
  • 经典实现
    • HashPartitioner

image-20220801101450467.png

Aggregator

  • 在map侧合并部分record的函数
  • 接口
    • createCombiner:只有一个value的时候初始化的方法
    • mergeValue:合并一个value到Aggregator中
    • mergeCombiners:合并两个Aggregator

Shuffle 过程

Shuffle 实现的发展历程

  • 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 退出历史舞台

Shuffle 写数据

Hash Shuffle

写数据

每个partition会映射到一个独立的文件

image (1).png

写数据优化

每个partition会映射到一个文件片段

606406c0-7230-481c-8e1f-b0610377be57.png

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

Sort Shuffle

写数据

每个task生成一个包含所有partiton数据的文件

1a5bd618-be67-479f-8078-dd0337a97b12.png

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

Tungsten Sort Shuffle

image.png

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

Shuffle 读数据

每个reduce task分别获取所有map task生成的属于自己的片段

89244d56-00f4-42b6-91c0-b52d38b0ae4e.png

Shuffle 过程的触发流程

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

image-20220801133244323.png

Shuffle Handle 的创建

Register Shuffle 时做的最重要的事情是根据不同条件创建不同的 shuffle Handle

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

image-20220801135521872.png

  • spark.shuffle.sort.bypassMergeThreshold默认为200

Shuffle Writer 的实现

三种ShuffleHandle对应了三种不同的ShuffleWriter的实现

image-20220801134301599.png

  • BypassMergeSortShuffleWriter:HashShuffle image-20220801134609707.png

    • 不需要排序,节省时间
    • 写操作的时候会打开大量文件
  • UnsafeShuffleWriter:TunstonShuffle

    image-20220801134856337.png

    • 使用类似内存页储存序列化数据

    • 数据写入后不再反序列化

    • 只适用partition数量较小的情况,使用堆外内存,没有垃圾回收的开销,没有对象模型类型开销

    • 只根据 partition 排序 Long Array

      image-20220801135127337.png

    • 数据不移动

    • 只保留24 bit用于存储 partition 数,其他空间用于存储这些Long Array数据,所以超过2242^{24}的partition数就不适用该 Writer

  • SortSHuffleWriter:SortShuffle image-20220801135800710.png

    • 支持combine
    • 需要combine时,使用PartitionedAppendOnlyMap,本质是个HashTable
    • 不需要combine时PartitionedPairBuffer本质是个array

Shuffle Reader 的实现

网络时序图

image-20220801140820559.png

  • 使用基于netty的网络通信框架,并接受reducetask的fetch请求
    • 使用堆外内存,零拷贝
  • 位置信息记录在MapOutputTracker中
  • 主要会发送两种类型的请求
    • 首先发起openBlocks请求获得streamId
    • 然后再处理Chunk请求或Stream请求

Shuffle Block FetchIterator

f4b32a52-fd11-403b-ac8d-29a8ab268be1.png

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

  • 防止OOM

    • maxBytesInFlight

    • maxReqsInFlight

    • maxBlocksInFlightPerAddress

    • maxReqSizeShuffleToMem

    • maxAttemptsOnNettyOOM

External Shuffle Service

67bf2c59-9350-4fbd-9377-344b225de0ff.png

为了解决Executor为了服务数据的fetch请求导致无法退出问题,我们在每个节点上部署一个External Shuffle Service,这样产生数据的Executor在不需要继续处理任务时,可以随意退出。从而优化了Spark作业的资源利用率,MapTask在运行结束后可以正常退出。

Shuffle 优化

Zero Copy(零拷贝)

DMA(Direct Memory Access) : 直接存储器存取,是指外部设备不通过 CPU 而直接与系统内存交换数据的接口技术。

不使用 zero copy

image-20220801142204849.png

使用sendfile

image-20220801142454380.png

使用sendfile + DMA gather copy

image-20220801142516789.png

Netty 零拷贝

  • 可堆外内存,避免 JVM 堆内存到堆外内存的数据拷贝。
  • CompositeByteBuf 、 Unpooled.wrappedBuffer、 ByteBuf.slice ,可以合并、包装、切分数组,避免发生内存拷贝
  • Netty 使用 FileRegion 实现文件传输,FileRegion 底层封装了 FileChannel#transferTo() 方法,可以将文件缓冲区的数据直接传输到目标 Channel,避免内核缓冲区和用户态缓冲区之间的数据拷贝

Shuffle 优化

  • 避免 shuffle

    • 使用 broadcast 替代 join

    image-20220801154536628.png

  • 使用可以map-side预聚合的算子

    • 没有使用map-side预聚合前 image-20220801154618087.png
    • 使用map-side预聚合后 image-20220801154635076.png
  • 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.enark.sql.adaptivetPostShufflelnputSize
spark.reducer.maxSizelnFlight
spark.reducer.maxReqslnFlight
spark.reducer.maxBlockslnFlightPerAddress

Shuffle 倾斜优化

image-20220801155215221.png

  • 数据倾斜影响
    • 作业运行时间变长
    • Task OOM 导致作业失败
  • 可以通过提高并行度解决 image-20220801155502517.png
    • 优点:足够简单
    • 缺点:只缓解、不根治问题
  • Spark AQE Skew Join:AQE 根据shuffle文件统计数据自动检测倾斜数据,将那些倾斜的分区打散成小的分区,然后各自进行join。 image-20220801155731646.png

案例

image-20220801160035574.png

优化前

image-20220801160207831.png

spark.sql.adaptive.shuffle.targetPostShufflelnputSize: 64M -> 512M
spark.sql.files.maxPartitionBytes: 1 G - > 40G

优化后

image-20220801160221296.png

Push Shuffle

为什么需要Push Shuffle?

Shuffle阶段常见问题

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

image-20220801160545591.png

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

Push Shuffle 的实现

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

Magnet 实现原理

Magnet主要流程

image-20220801160911588.png

主要为边写边push的模式,在原有的shuffle基础上尝试push聚合数据,但并不强制完成,读取时优先读取push聚合的结果,对于没有来得及完成聚合或者聚合失败的情况,则fallback到原模式。

  • Spark driver组件,协调整体的shuffle操作
  • map任务的shuffle writer过程完成后,增加了一个额外的操作push-merge,将数据复制一份推到远程shuffle服务上
  • magnet shuffle service是一个强化版的ESS。将隶属于同一个shuffle partition的block,会在远程传输到magnet 后被merge到一个文件中
  • reduce任务从magnet shuffle service 接收合并好的shuffle数据

image-20220801164100903.png

  • 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数据的副本

Cloud Shuffle Service

Cloud Shuffle Service 思想

  • IO集合:所有Mapper的同一Partition数据都远程写到同一个文件(或者多个文件)
  • 备份:HDFS太重,使用双磁盘副本(成本低、速度快)
  • 写入速度:主从InMemory副本,异步刷盘,极小的失败几率去换取高速写入速度

Cloud Shuffle Service 架构

0164a207-7d97-4ff6-9b1d-a0e68977c793.png

  • 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 读写流程

e1d6d671-0933-4783-828d-d8bf0bbfc88d.png

Cloud Shuffle Service AQE

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

在聚合文件时主动将文件切分为若干块,当触发AQE时,按照已经切分好的文件块进行拆分。

image-20220801170338848.png