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

169 阅读12分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 22 天

这里把Shuffle单独做了一个分析,在结合具体的实践的基础上,主要围绕什么是 Shuffle、Shuffle 的基本过程有哪些、以及为什么 Shuffle 在大数据领域如此重要等问题,帮助大家对 Shuffle 建立初步的认知。Shuffle是非常重要的昂!!!

Shuffle概述

MapReduce概述

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

image-20230205172841379

  • Map:单机对于数据进行处理,排序
  • Shuffle:数据移动,打散,为Reduce阶段做准备
  • Reduce:对移动后的数据进行处理,依然是在单机上处理一小份数据。

为什么shuffle对性能非常重要

  • M * R次网络连接:每一个Reducer都要访问所有的Mapper,去拿到这个Reducer对应的数据。
  • 大量的数据移动:M * R次数据移动
  • 数据丢失风险:网络异常等异常,重算
  • 可能存在大量的排序操作:Map之后,可能需要排序 -> Shuffle,颜色进行排序
  • 大量的数据序列化、反序列化操作
  • 数据压缩

总结

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

Shuffle算子

Shuffle算子分类

  • spark中会产生shuffle等算子,大概可以分成4类:

image-20230205174123700

Shuffle算子使用

Spark源码中的RDD单元测试

Spark源码中的PariRDDFunction的单元测试

  • 数据移动的需求,就是Shuffle来实现的

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

image-20230205174804700

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

图片来源: (Rеѕіlіеnt Dіѕtrіbutеd Dаtаѕеtѕ: А Fаult-Тоlеrаnt Аbѕtrасtіоn fоr Іn-Меmоrу Сluѕtеr Соmрutіng) 论文

当出现宽依赖(数据移动),Spark就会拆分成两个Stage,一个是Map,一个是Reduce,中间就会产生Shuffle操作。

算子内部的依赖关系

  • ShuffleDependency(宽依赖对应的依赖关系)

    • CoGroupedRDD

      • Cogroup

        • fullOuterJoin, rightOuterJoin, leftOuterJoin
        • join
    • ShuffledRDD

      • combineByKeyWithClassTag

        • combineByKey
        • reduceByKey
      • Coalesce

      • sortByKey

        • sortBy

ShuffleDependency是被RDD创建的

当我们调用CoGroup操作或者Shuffle操作的时候,RDD就会被创建昂!

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

负责把一个Key映射成一个数字(具体的分区)

  • 两个接口

    • numberPartitions
    • getPartition

image-20230205175955231

  • 经典实现

    • HashPartitioner

image-20230205180008398

算Hash再取模,就很方便昂!!!

Aggregator

Shuffle的时候,重要的性能优化器。把一部分Reduce的工作,放到Map来做,例如

  • 原始WordCount: 单词排序 -> Shuffle(Mapper -> Reducer) -> Reduce(数数 + Sum)并拿到结果。
  • 加入Aggregator的WordCount: 单词排序(同时数数) -> Shuffle(Mapper -> Reducer) -> Reduce(Sum)

下面的,明显Shuffle的过程中,分发传递的数据量,就少了很多很多昂!

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

Shuffle过程

History

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

Hash Shuffle-写数据

写数据

image-20230206093423532

  • 每个parition会映射到一个独立的文件,总共就是生成 M * R 个文件。

www.slideshare.net/colorant/sp…

  • 问题:随着作业规模增大,生成的文件太多,打开的文件也很多。耗费的资源太多了!!!

Hash Shuffle-写数据优化

image-20230206094332310

每个Parition不是映射成一个文件,而是映射成一个文件片段。M * R -> CPU Num * R

没有完全解决,依然还是非常占用资源昂!!!

Sort Shuffle-写数据

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

image-20230206101438956

占用很多的CPU,所有的数据都写在同一个buffer里面,通过排序的方式,把所有的partition放在一起。最终就只生成一个数据文件(还有一个小的记录偏移量和Segment的元数据文件昂)

Shuffle-读数据

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

image-20230206101655381

Shuffle过程的触发流程

  • collect的时候,实际才开始触发后续的流程哈

image-20230206101814987

image-20230206101833814

Shuffle Handler的创建

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

image-20230206101957866

Shuffle Handle和Shuffle Writer的关系

image-20230206102028247

Writer实现-BypassMergeShuffleWriter

image-20230206102758676

这个就是HashShuffle,不排序,节省CPU

适用于Partition较少的情况,中间文件少(一般都要<200)

  • 不需要排序,节省时间
  • 写操作的时候会打开大量文件
  • 类似于Hash Shuffle

前半部分和Hash Shuffle一致。后半部分做了升级,最终会把文件merge到一起,减少文件数量,生成两个文件,merge。

merge用到了操作系统中的特性:zero-copy

Writer实现-UnsafeShuffleWriter

image-20230206102429926

Unsafe: 用了堆外内存,没有用Java本身的内存,Why?

  1. 没有Java对象相关的开销
  2. 没有垃圾回收的开销
  • 使用类似内存页储存序列化数据
  • 数据写入后不再反序列化

也会merge。如果内存申请不到洞见,就会spill,最终也会把很多生成的spill给merge成两个文件,data file & index file

  • 堆外内存管理:

image-20230206102846860

  • 只根据partition排序Long Array
  • 数据不移动
  • 为啥partition数量有限制2^24? -> Long Array里面的前24个bit就装不下partition了

Writer实现-SortShuffleWriter

image-20230206103211902

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

这个是在堆内哈,实现思路是一样的,最终也是合并成了两个文件。支持Map侧的Combine,

首先对于Partition进行排序,再对于Key进行排序。

为啥不堆外?堆外序列化,没用啊,害得堆内对于Key进行排序呢。

Reader实现

网络时序图

image-20230206104201872

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

  • 位置信息记录在MapOutputTracker中

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

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

ShuffleBlockFetchIterator

image-20230206104323156

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

  • 防止OOM

    • maxBytesInFlight
    • maxReqsInFlight
    • maxBlocksInFlightPerAddress
    • maxReqSizeShuffle ToMem
    • maxAttemptsOnNettyOOM

三种Writer,但是只有一种Reader

对于远程数据做一个分割(构造一个远端的FetchRequest对象),随机放到一个请求队列里面(如果不是随机的,可能会导致Fetch热点问题。例如全部访问A,再全部访问B这样昂!)

External Shuffle Service

image-20230206104937454

ESS很重要!!!

  • 为了解耦数据计算和数据读取服务。

    • 原始:谁去Write了数据,谁就要继续存活着,等着其他的Task来Fetch这些数据。
    • ESS:单独的服务,运行在每台主机上,管理该主机,所有Executor生成的数据。计算单元,写完数据后,Executor就可以退出啦!!!
  • ESS作为一一个存在于每个节点上的agent为所有Shuffle Reader提供服务,从而优化了Spark作业的资源利用率MapTask在运行结束后可以正常退出

Shuffle优化使用的技术

Zero Copy

image-20230206105728530

developer.ibm.com/articles/j-…

Netty Zero Copy

  • 可堆外内存,避免JVM堆内存到堆外内存的数据拷贝。(避免对内外内存的数据拷贝,如果不需要使用的话)
  • CompositeByteBuf、Unpooled.wrappedBuffer、 ByteBuf.slice ,可以合并、包装、切分数组,避免发生内存拷贝。(包装可以不用再重新分配内存,例如1G,2G。我直接抽象一个3G的数组用就行了昂。)
  • Netty使用FileRegion实现文件传输,FileRegion 底层封装了FileChannel#transferTo() 方法,可以将文件缓冲区的数据直接传输到目标Channel,避免内核缓冲区和用户态缓冲区之间的数据拷贝

常见问题

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

Shuffle优化

image-20230206110021746

  • 避免shuffle: 使用broadcase代替join

image-20230206110500451

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

Reference: tech.meituan.com/2016/05/12/…

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倾斜优化

image-20230206110809932

  • 什么叫shuffle倾斜

  • 倾斜影响

    • 作业运行时间变长
    • Task OOM导致作业失败
  • 解决方法:

    • 调整参数:提高并行度!!!有效缓解倾斜。缺点:只能缓解,不能根治。
    • AQE Skew Join

Spark AQE Skew Join

image-20230206111023450

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

处理更加均匀了,等价于,又套了一个MR,把手上过多的数据,分发下去计算,再进行聚合昂!!!

案例-参数优化

image-20230206112014727

  • 参数调整:

image-20230206112031028

Push Shuffle

Why Push Shuffle

image-20230206112214793

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

Push Shuffle的实现

  • Facebook: Cosco
  • Linkedln: magnet
  • Uber: Zeus
  • Alibaba: RSS
  • Tencent: FireStorm
  • Bytedance: CSS
  • Spark3.2: push based shuffle

Magnet实现原理

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

image-20230206112556437

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

万事不决,直接拉block就可以了昂!本质上就是维护了两份副本,一个是原始的,一个是我Aggregate之后,push上去的昂。

Cloud Shuffle Service思想

image-20230206113108115

Cloud Shuffle Service架构

image-20230206113252879

  • Zookeeper WorkerList [BESSTU]
  • CSS Worker [Partitions 1 Disk I Hdfs]
  • Spark Driver [# 5XE Css Master]
  • CSS Master [Shuffle #ЛtJ 1 5tit]
  • CSS ShuffleClient [Write 1 Read]
  • Spark Executor [Mapper + Reducer]

CSS写入流程

image-20230206113939200

CSS读取流程

image-20230206114147208

P0-0要读

P0-1和P0'-1任选一个就行

如果都寄了呢?hhhh,所以说,我们是冒了风险的昂!!!

Cloud Shuffle Service AQE

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

image-20230206114359672

  • 不用把超大文件读很多遍,按需分配小文件就可以了昂。(超大和超小都有问题,取了一个折中)

实践案例-CSS优化

  • XX业务小时级任务(1.2w cores)
  • 混部队列2.5h ->混部队列 + CSS 1.3h (50%提升)

image-20230206114541413

课程总结

  1. Shuffle 概述

    • 什么是shuffle,shuffle的基本流程
    • 为什么shufle对性能影响非常重要
  1. Shuffle 算子

    • 常见的shuffle算子
    • 理解宽依赖和窄依赖,ShuffleDependency及其相关组件
  1. Shuffle 过程

    • Spark中shuffle实现的历史
    • Spark中主流版本的shuffle写入和读取过程
  1. Push shuffle

    • Magnet Push Shuffle的设计思路
    • Cloud Shuffle Service的设计实现思路

References

  1. Spark源码中的RDD单元测试
  2. Spark源码中的PariRDDFunction的单元测试
  3. Spark-shuffle-introduction: www.slideshare.net/colorant/sp…
  4. zero copy: developer.ibm.com/articles/j-…
  5. 数据倾斜&解决方案:tech.meituan.com/2016/05/12/…