大数据 Shuffle 原理与实践

343 阅读7分钟

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

shuffle 概述

提出

04年Google发布的关于处理大规模集群简化数据处理的论文,mapreduce三阶段:map,shuffle,reduce

image.png

Map阶段

在单机上进行的针对一小块数据的计算过程

Map阶段

Shuffle阶段

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

Shuffle阶段

Reduce阶段

对移动后的数据进行处理,依然是单机上处理一小份数据

Reduce过程

为何shuffle对性能非常重要

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

image.png

在大数据场景下,数据shuffle表示了不同分区数据交换的过程,不同的shuffle策略性能差异较大。

目前在各个引擎中shuffle都是优化的重点,在spark框架中,shuffle是支撑spark进行大规模复杂数据处理的基石。

shuffle 算子分类

image.png

image.png

map和flatMap

Spark 中 map函数会对每一条输入进行指定的操作,然后为每一条输入返回一个对象。 而flatMap函数则是两个操作的集合——正是“先映射后扁平化”

reduceByKey

reduceByKey会寻找相同key的数据,当找到这样的两条记录时会对其value(分别记为x,y)做(x,y) => x+y的处理,即只保留求和之后的数据作为value。反复执行这个操作直至每个key只留下一条记录。reduceByKey(_+_)reduceByKey((x,y) => x+y)的简洁表达。

file1.txt

Hello World
Hello Spark
Hello Spark

file2.txt

Hello World
Hello Spark
def main(args: Array[String]): Unit = {
  val sparkConf = new SparkConf().setMaster("local").setAppName("WordCount")
  val sc = new SparkContext(sparkConf)
  val text = sc.textFile("datas/*")
  val counts = text
    .flatMap(line => line.split(" "))
    .map(word => (word, 1))
    .reduceByKey(_ + _)
  val tuples: Array[(String, Int)] = counts.collect()
  tuples.foreach(println)
  sc.stop()
}

输出

(Hello,5)
(World,2)
(Spark,3)

shuffle抽象:宽窄依赖

image.png

image.png

依赖构造

Partitioner

  • 两个接口
    • numberPartitions
    • getPartition
  • 经典实现
    • HashPartitioner

image.png

image.png

Aggregator

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

shuffle 过程

shuffle 发展历程

image.png

Hash shuffle

写数据

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

image.png

写数据优化

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

image.png

sort shuffle

写数据

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

image.png

读数据

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

image.png

现在在哪?

image.png

wordcount例子

val text = sc.textFile("datas/*")
  val counts = text
    .flatMap(line => line.split(" "))
    .map(word => (word, 1))
    .reduceByKey(_ + _)
  val tuples: Array[(String, Int)] = counts.collect()

在collect中会调用spark中DAGscheduler(调度模块DAGScheduler和TaskScheduler)submit job,其中会分析counts对象RDD依赖关系,该对象包含了reduceByKey算子,所以在查找依赖关系时,会创建shuffleRDD的对象,这个对象在dagScheduler中getDependencies时,会创建shuffle dependencies,然后registerShuffle注册,后续dag会生成两个stage,map,reduce;这两个stage分别提交给executor,计算开始

ShuffleRegister

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}224的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

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

image.png

Push Shuffle

为什么需要Push Shuffle?

Shuffle阶段常见问题

image-20220801160545591.png

  • Avg IO size太小,造成了大量的随机IO,严重影响磁盘的吞吐
  • M*R次读请求,造成大量的网络连接,影响稳定性
  • 数据存储在本地磁盘,没有备份
  • IO 吞吐:随机读、写放大(3X)
  • GC 频繁,影响 NodeManager

Magnet 实现原理

Magnet主要流程

image-20220801160911588.png

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

Magnet实现原理

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 思想

image.png

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