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

127 阅读6分钟

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

1、 Shuffle概述

1.1 MapReduce 概述

  • Map阶段,是在单机上进行的针对一小块数据的计算过程
  • Shuffle 阶段,在map阶段的基础上,进行数据移动,为后续的reduce阶段做准备
  • reduce阶段,对移动后的数据进行处理,依然是在单机上处理一小份数据

1.2 经典Shuffle过程

image.png

1.3 为什么shuffle如此重要

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

1.4 总结

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

2、Shuffle算子

2.1 Shuffle 算子分类

  • Spark中会产生shuffle的算子大概可以分为4类 image.png
  • 算子使用例子
val counts = text
  .flatMap(line => line.split(" "))
  .map(word => (word,1))
  .reduceByKey(_+_)
counts.collect

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

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

image.png

2.3 算法内部的依赖关系

image.png

2.4 Shuffle Dependency 构造

  • 创建会产生shuffle的RDD时,RDD会创建Shuffle Dependency来描述Shuffle相关的信息
  • 构造函数
    • 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.

Shuffle Dependency 构造 —— Partitioner

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

Shuffle Dependency 构造 —— Aggregator

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

3、 Shuffle过程

3.1 spark中的shuffle的发展过程

image.png

3.2 HashShuffle

  • 写数据
    • 每个partition会映射到一个独立的文件 image.png
  • 写数据优化
    • 每个partition会映射到一个文件片段 image.png
  • 优点:不需要排序
  • 缺点:打开,创建的文件过多

3.2 SortShuffle

  • 写数据
    • 每个task生成一个包含所有partition数据的文件 image.png

3.3 Shuffle —— 读数据

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

  • 优点:打开的文件少、支持map-side combine

  • 缺点:需要排序

  • TungstenSortShuffle

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

3.4 Register Shuffle

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

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

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

3.5 ShuffleReader网络请求流程

网络时序图

使用netty作为网络框架提供网络服务,并接受reducetask的fetch请求 首先发起openBlocks请求获得streamId,然后再处理stream或者chunk请求

  • 特点:

    • 使用基于net比y的网络通信框架

    • 位置信息总记录在MapOutputTrackert中

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

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

ShuffleBlockFetchIterator

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

  • 防止OOM

    • maxBytesInFlight
    • maxReqsInFlight
    • maxBlocksInFlightPerAddress
    • maxReqSizeShuffleToMem
    • maxAttemptsOnNettyOOM

External Shuffle Service

为了解决Executor为了服务数据的fetch请求导致无法退出问题,我们在每个节点上部署一个External Shuffle Service,这样产生数据的Executor在不需要继续处理任务时,可以随意退出。

3.6 shuffle优化

  • 避免shuffle ——使用broadcast替代join
   //传统的join操作会导致shuffle操作。
   //因为两个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预聚合的算子

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

    • 什么叫倾斜?有什么危害
    • 解决倾斜方法举例 - 增大并行度 - AQE
  • 零拷贝

    • sendfile+DMA gather copy
  • Netty 零拷贝

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

4、Push Shuffle

  • 上一部分所讲的shuffle过程存在哪些问题?

    • 数据存储在本地磁盘,没有备份
    • IO 并发:大量 RPC 请求(M*R)
    • IO 吞吐:随机读、写放大(3X)
    • GC 频繁,影响 NodeManager
  • 为了优化该问题,有很多公司都做了思路相近的优化,push shuffle

4.1 Magnet实现原理

  • 主要流程

image.png

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

  • Magnet可靠性

    • 如果Map task输出的Block没有成功Push到magnet上,并且反复王试仍然失败,则reduce task直接从ESS上拉取原始block数据
    • 如果magnet上的block因为重复或者冲突等原因,没有正常完成merge的过程,则reduce task直接拉取未完成merge的block
    • 如果reduce拉取已经merge好的block失败,则会直接拉取merge前的原始block
    • 本质上,magnet中维护了两份shuffle数据的副本

4.2 Cloud Shuffle Service

  • Cloud Shuffle Service架构

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

  • Cloud Shuffle Service 支持AQE

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