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

163 阅读6分钟

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

0 引言

整体架构.png

1 Shuffle概述

MapReduce概述:开始是Hadoop的MapReduce模块开源实现的分布式计算架构。

input -> map -> shuffle -> reduce -> output

Map阶段:在单机上进行的针对—小块数据的计算过程。(比如按行统计单词个数)

Shufle阶段:在map阶段的基础上,进行数据移动,为后续的reduce阶段做准备。(相同的单词在一组)

Reduce阶段:对移动后的数据进行处理,依然是在单机上处理一小份数据。(对同组相同单词合并统计个数)

Shuffle对性能非常重要原因(Shuffle可以优化,是Spark数据复杂处理的基石): shuffle.jpeg

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

2 Shuffle算子

Shuffle算子分类/应用: 算子分类.png Spark原码中RDD的单元测试:github.com/apache/spar…
Spark原码中PairRDDFunctions的单元测试:github.com/apache/spar…

val text = sc.textFile( "'mytextfile.txt")  //导入单词
val counts = text
    .flatMap(line => line.split(" "))   //按照行空格分开每个单词
    .map(word =>(word,1))  //将每个单词修改为(word,1)格式
    .reduceByKey(_+_)  //按照K就是单词进行统计,V就是1 进行求和
counts.collect  //打印

Spark中对Shuffle的抽象: 宽窄依赖.png

  • 窄依赖:父RDD的每个分片至多被子RDD中的一个分片所依赖
  • 宽依赖:父RDD中的分片可能被子RDD中的多个分片所依赖(产生宽依赖就会增加两个Stage:Map、Reduce)

算子内部的依赖关系(宽依赖时): ShuffleDependency.png

Shuffle Dependency构造:

  • A single key-value pair RDD, i.e.RDD[Product2[K,V]]。(KV对的RDD)
  • Partitioner (available as partitioner property)。(传入K后给定一个分区)
  • Serializer。(对象<->二进制数据流)
  • Optional key ordering (of Scala's scala.math.Ordering type)。(对K进行排序)
  • Optional Aggregator。
  • mapSideCombine flag which is disabled (i.e. false) by default。(flag描述shuffle过程mapSideCombine)

Partitioner:两个接口numberPartitions和getPartition。经典实现HashPartitioner。

abstract class Partitioner extends serializable {
    def numPartitions: Int
    def getPartition(key: Any) : Int
}

class HashPartitioner(partitions: Int) extends Partitioner {
    require(partitions >= 0,s"Number of partitions ($partitions) cannot be nega1")
    def numPartitions: Int = partitions
    def getPartition( key: Any) : Int = key match {
        case null => 0
        case _ => Utils.nonNegativeMod(key.hashcode,numPartitions)
    }
}

Aggregator:

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

3 Shuffle过程

  • Hash Shuffle写:
    每个partition会映射到一个独立的文件。导致每个partition会生成一个file。
    优化:每个partition会映射到一个文件片段。

  • Sort Shuffle写:
    每个task生成一个包含所有partiton数据的文件。一个task在一个文件,文件各个位置存储对应的partiton。

  • Shuffle读:
    每个reduce task分别获取所有map task生成的属于自己的片段。直接读取对应的task文件就可以。

Shuffle的过程触发流程:

CollectAction -> SubmitJob -> GetDependencies -> RegisterShuffle

Shuffle的Handle创建:
在Register Shuffle时根据不同条件创建不同的shuffle Handle。 ShuffleHandle.jpeg

BypassMergeSortShuffleHandle <-> BypassMergeSortShuffleWriter
SerializedShuffleHandle <-> UnsafeShuffleWriter
BaseShuffleHandle <-> SortShuffleWriter

Shuffle Writer实现:

// 通过shuffleHandle来决定是哪一种ShuffleWrite
handle match {
  case unsafeShuffleHandle: SerializedShuffleHandle[K @unchecked, V @unchecked] =>
    new UnsafeShuffleWriter(
      env.blockManager,
      shuffleBlockResolver.asInstanceOf[IndexShuffleBlockResolver],
      context.taskMemoryManager(),
      unsafeShuffleHandle,
      mapId,
      context,
      env.conf)
  case bypassMergeSortHandle: BypassMergeSortShuffleHandle[K @unchecked, V @unchecked] =>
    new BypassMergeSortShuffleWriter(
      env.blockManager,
      shuffleBlockResolver.asInstanceOf[IndexShuffleBlockResolver],
      bypassMergeSortHandle,
      mapId,
      context,
      env.conf)
  case other: BaseShuffleHandle[K @unchecked, V @unchecked, _] =>
    new SortShuffleWriter(shuffleBlockResolver, other, mapId, context)
}
  • BypassMergeShuffleWriter:不需要排序,节省时间;写操作的时候会打开大量文件类似于Hash Shuffle。【选这个时没有开启combine并且下游分区数<200,适用于groupByKey、sortByKey,且下游分区数不能超过阈值(默认200)】
  • UnsafeShuffleWriter:使用类似内存页储存序列化数据;数据写入后不再反序列化。【序列化形式,要求shuffle不能带聚合器,像reduceByKey,groupByKey是不能用的。sortByKey可以,repartition也可以】
  • SortShuffleWriter:支持combine;需要combine时,使用PartitionedAppendOnlyMap,本质是个HashTable;不需要combine时PartitionedPairBuffer本质是个array。【写入内存缓冲区】

Reader实现:

  • 网络时序图 使用基于netty的网络通信框架;位置信息记录在MapOutputTracker中;主要会发送两种类型的请求OpenBlocks请求、hunk请青求或Stream请求。 网络时序图.png

  • ShuffleBlockFetchlterator
    区分local和remote节省网络消耗;防止OOM(maxByteslnFlight/ maxReqslnFlight/ maxBlockslnFlightPerAddress/ maxReqSizeShuffleToMem/ maxAttemptsOnNettyOOM)

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

Shuffle优化使用的技术:

  • Zero Copy:避免了数据在用户空间和内存空间之间的拷贝,从而提高了系统的整体性能。
  • Netty Zero Copy:FileRegion中包装了NIO的FileChannel.transferTo()方法实现了零拷贝。而在Netty中还有另一种形式的零拷贝,即Netty允许我们将多段数据合并为一整段虚拟数据供用户使用,而过程中不需要对数据进行拷贝操作。

Spark Shuffle常见问题:

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

Shuffle优化/参数优化/倾斜优化

  • 避免shuffle,使用broadcast替代join。【对于小文件(较小数据RDD)可以进行广播变量】
  • 使用可以map-side预聚合的算子。【比如word count,在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
  • 数据倾斜是某些task分配的数据过大,导致作业运行时间变长和Task OOM导致作业失败。
    一般解决办法可以是 提高并行度,但是只能缓解、不能根治。
    可以使用AQE根据shuffle文件统计数据自动检测倾斜数据,将那些倾斜的分区打散成小的子分区,然后各自进行join。【系统检测大的task数据,将他进行拆分成小的task执行】

4 Push Shuffle

对于下列问题,很多企业做了个性化开发,文中介绍Magnet用法和ByteDance即将开源的CloudShuffleService架构。

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

Magnet实现原理: Magnet实现原理.png

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

Cloud Shuffle Service思想:

  • IO聚合:所有Mapper的同一Partition数据都远程写到同一个文件(或者多个文件)。
  • 备份:HDFS太重,自定义文件存储,使用双磁盘副本(成本低、速度快)。
  • 写入速度:主从InMemory副本,异步刷盘,极小的失败几率去换取高速写入速度。
    注意:写入数据会产生重复数据,在读数据需要去重。

参考引用

1.【大数据专场 学习资料二】第四届字节跳动青训营
2. Spark的Shuffle总结分析
3. Spark ShuffleWriter
4. 零拷贝(Zero-copy)
5. 你知道Netty的零拷贝机制原理吗?这次带你彻底搞懂!