Spark Shuffle原理

Shuffle详解

以Shuffle为边界,Spark将一个Job划分为不同的Stage。Spark的Shuffle分为Write和Read两个阶段,分属于两个不同的Stage,前者是Parent Stage的最后一步,后者是Child Stage的第一步

Spark 的 Stage 分为两种

  • ResultStage。负责返回计算结果
  • ShuffleMapStage。其他的均为ShuffleMapStage

如果按照 map 端和 reduce 端来分析的话:

  • ShuffleMapStage可以即是map端任务,又是reduce端任务
  • ResultStage只能充当reduce端任务

Spark Shuffle的流程简单抽象为以下几步:

  • Shuffle Write

  • Map side combine (if needed)

  • Write to local output file

  • Shuffle Read

  • Block fetch

  • Reduce side combine

  • Sort (if needed)

  • Shuffle涉及到了本地磁盘(非hdfs)的读写和网络的传输类的磁盘IO以及序列化等耗时操作。

Spark的Shuffle经历了Hash、Sort、Tungsten-Sort(堆外内存)三阶段发展历程:

  • 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并入Sort Based Shuffle
  • Spark 2.0 Hash Based Shuffle退出历史舞台

Shuffle的发展历程简述

1、Hash Base Shuffle V1

  • Shuffle Map Task过程按照 Hash 的方式重组 Partition 的数据,不进行排序。每个Map Task需要为每个下游的Reduce Task创建一个单独的文件

  • Shuffle过程中会生成海量的小文件。同时打开过多文件、伴随大量的随机磁盘 I/O 操作与大量的内存开销

    • 存在问题:

    • 生成大量文件,占用文件描述符,同时引入 DiskObjectWriter 带来的 Writer Handler 的缓存也非常消耗内存

    • 如果在 Reduce Task 时需要合并操作的话,会把数据放在一个 HashMap 中进行合并,若数据量较大,很容易引发 OOM

2、Hash Bash Shuffle V2--File Consolidation

 - 针对第一问题,spark引入了File Consolidation机制。一个 Executor 上所有的 Map Task 生成的分区文件只有一份,即将所有的 Map Task 相同的分区文件合并,这样每个 Executor 上最多只生成 N 个分区文件
 - 允许不同的task复用同一批磁盘文件,有效将多个task的磁盘文件进行一定程度上的合并,从而大幅度减少磁盘文件的数量,进而提升shuffle write的性能。一定程度上解决了Hash V1中的问题,但不彻底。
 - Hash Shuffle 规避了排序,提高了性能;

3、sort Base Shuffles

  • Sort Base Shuffle大大减少了shuffle过程中产生的文件数,提高Shuffle的效率;

  • 每个 Task 不会为后续的每个 Task 创建单独的文件,而是将所有对结果写入同一个文件。该文件中的记录首先是按照Partition Id 排序,每个 Partition 内部再按照 Key 进行排序,Map Task 运行期间会顺序写每个 Partition 的数据,同时生成一个索引文件记录每个 Partition 的大小和偏移量

  • 在 Reduce 阶段,Reduce Task 拉取数据做 Combine 时不再采用 HashMap,而是采用ExternalAppendOnlyMap,该数据结构在做 Combine 时,如果内存不足,会刷写磁盘,避免大数据情况下的 OOM

  • Sort Shuffle 解决了 Hash Shuffle 的所有弊端,但是因为需要其 Shuffle 过程需要对记录进行排序,所以在性能上有所损失。

  • Tungsten-Sort Based Shuffle / Unsafe Shuffle

    • 它的做法是将数据记录用二进制的方式存储,直接在序列化的二进制数据上 Sort 而不是在 Java 对象上,这样一方面可以减少内存的使用和 GC 的开销,另一方面避免 Shuffle 过程中频繁的序列化以及反序列化。在排序过程中,它提
      供 cache-efficient sorter,使用一个 8 bytes 的指针,把排序转化成了一个指针数组的排序,极大的优化了排序性能。

    • 限制:

    • Shuffle 阶段不能有 aggregate 操作

    • 分区数不能超过一定大小(2^24-1,这是可编码的最大 Parition Id),

    • 像 reduceByKey 这类有 aggregate 操作的算子是不能使用Tungsten-Sort Based Shuffle,它会退化采用 Sort Shuffle

SortShuffleV2

  • 从 Spark1.6.0 开始,把 Sort Shuffle 和 Tungsten-Sort Based Shuffle 全部统一到 Sort Shuffle 中,如果检测到满足Tungsten-Sort Based Shuffle 条件会自动采用 Tungsten-Sort Based Shuffle,否则采用 Sort Shuffle
  • 从Spark2.0 开始,Spark 把 Hash Shuffle 移除, Spark2.x 中只有一种 Shuffle,即为 Sort Shuffle

Shuffle Writer

在DAG阶段以shuffle为界,划分stage,上游stage做map task,每个map task将计算结果数据分成多份,每一份对应到下游stage的每个partition中,并将其临时写到磁盘,该过程叫做shuffle write;

  • ShuffleWriter(抽象类),有3个具体的实现:

    • SortShuffleWriter。sortShulleWriter 需要在 Map 排序
    • UnsafeShuffleWriter。使用 Java Unsafe 直接操作内存,避免Java对象多余的开销和GC 延迟,效率高
    • BypassMergeSortShuffleWriter。和Hash Shuffle的实现基本相同,区别在于map task输出汇总一个文件,同时还会产生一个index file
  • ShuffleWriter 有各自的应用场景。分别如下:

    • 不满足以下条件使用 SortShuffleWriter
    • 没有map端聚合,RDD的partitions分区数小于16,777,216,且 Serializer支持 relocation,使用UnsafeShuffleWriter
    • 没有map端聚合操作 且 RDD的partition分区数小于200个,使用 BypassMergerSortShuffleWriter
  • bypass运行机制

  • shuffle map task数量 <= spark.shuffle.sort.bypassMergeThreshold (缺省200)

  • 不是聚合类的shuffle算子

  • Bypass机制 Writer 流程如下:

    • 每个Map Task为每个下游 reduce task 创建一个临时磁盘文件,并将数据按key进行hash然后根据hash值写入内存缓冲,缓冲写满之后再溢写到磁盘文件

    • 最后将所有临时磁盘文件都合并成一个磁盘文件,并创建索引文件

    • Bypass方式的Shuffle Writer机制与Hash Shuffle是类似的,在shuffle过程中会创建很多磁盘文件,最后多了一个磁盘文件合并的过程。Shuffle Read的性能会更好

    • Bypass方式与普通的Sort Shuffle方式的不同在于

    • 磁盘写机制不同

    • 根据key求hash,减少了数据排序操作,提高了性能

  • Shuffle Writer 流程

    • 数据先写入一个内存数据结构中:不同的shuffle算子,可能选用不同的数据结构(reduceByKey会选用Map结构;join类的shuffle算子,选用Array数据结构)
    • 检查是否达到内存阈值。
    • 数据排序:在溢写到磁盘文件之前,会先根据key对内存数据结构中已有的数据进行排序。排序过后,会分批将数据写入磁盘文件。默认是10000条
    • 数据写入缓冲区:写入磁盘文件是通过Java的 BufferedOutputStream 实现的
    • 重复写多个临时文件:一个 Task 将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作,会产生多个临时文件
    • 临时文件合并:将所有的临时磁盘文件进行合并,这就是merge过程。
    • 写索引文件:标识了下游各个 Task 的数据在文件中的 start offset 与 end offset

Shuffle MapOutputTracker

  • MapOutputTracker负责管理Writer和Reader的沟通.

  • Shuffle Writer会将中间数据保存到Block里面,然后将数据的位置发送给MapOutputTracker

  • Shuffle Reader通过向 MapOutputTracker 获取中间数据的位置之后,才能读取到数据。

  • Shuffle Reader 需要提供 shuffleId、mapId、reduceId 才能确定一个中间数据

    • shuffleId,表示此次shuffle的唯一id
    • mapId,表示map端 rdd 的分区索引,表示由哪个父分区产生的数据
    • reduceId,表示reduce端的分区索引,表示属于子分区的那部分数据
  • MapOutputTracker在executor和driver端都存在:

    • MapOutputTrackerMaster 和 MapOutputTrackerMasterEndpoint(负责通信) 存在于driver
    • MapOutputTrackerWorker 存在于 executor 端
    • MapOutputTrackerMaster 负责管理所有 shuffleMapTask 的输出数据,每个 shuffleMapTask 执行完后会把执行结果(MapStatus对象)注册到MapOutputTrackerMaster
    • MapOutputTrackerMaster 会处理 executor 发送的 GetMapOutputStatuses 请求,并返回serializedMapStatus 给 executor 端
    • MapOutputTrackerWorker 负责为 reduce 任务提供 shuffleMapTask 的输出数据信息(MapStatus对象)
    • 如果MapOutputTrackerWorker在本地没有找到请求的 shuffle 的 mapStatus,则会向MapOutputTrackerMasterEndpoint 发送 GetMapOutputStatuses 请求获取对应的 mapStatus

Shuffle Reader

  • 下游stage做reduce task,每个reduce task通过网络拉取上游stage中所有map task的指定分区结果数据,该过程叫做shuffle read

  • Map Task 执行完毕后会将文件位置、计算状态等信息封装到 MapStatus 对象中,再由本进程中的MapOutPutTrackerWorker 对象将其发送给Driver进程的MapOutPutTrackerMaster对象

  • Reduce Task开始执行之前会先让本进程中的 MapOutputTrackerWorker 向 Driver 进程中的MapOutputTrackerMaster 发动请求,获取磁盘文件位置等信息

  • 当所有的Map Task执行完毕后,Driver进程中的 MapOutputTrackerMaster 就掌握了所有的Shuffle文件的信息。此时MapOutPutTrackerMaster会告诉MapOutPutTrackerWorker磁盘小文件的位置信息

  • 完成之前的操作之后,由 BlockTransforService 去 Executor 所在的节点拉数据,默认会启动五个子线程。每次拉取的数据量不能超过48M

Hadoop Shuffle 与 Spark Shuffle 的区别

共同点:

  • 二者从功能上看是相似的;从High Level来看,没有本质区别,实现(细节)上有区别

区别:

  • Hadoop中有一个Map完成,Reduce便可以去fetch数据了,不必等到所有Map任务完成;而Spark的必须等到父stage完成,也就是父stage的 map 操作全部完成才能去fetch数据。这是因为spark必须等到父stage执行完,才能执行子stage,主要是为了迎合stage规则
  • Hadoop的Shuffle是sort-base的,那么不管是Map的输出,还是Reduce的输出,都是partition内有序的,而spark不要求这一点
  • Hadoop的Reduce要等到fetch完全部数据,才将数据传入reduce函数进行聚合,而 Spark是一边fetch一边聚合

Shuffle优化

Spark作业的性能主要就是消耗了shuffle过程,因为该环节包含了众多低效的IO操作:磁盘IO、序列化、网络数据传输等;如果要让作业的性能更上一层楼,就有必要对 shuffle 过程进行调优

开发过程中对 Shuffle 的优化:

  • 减少Shuffle过程中的数据量
  • 避免Shuffle

Shuffle 的优化主要是参数优化

  • 优化一:调节 map 端缓冲区大小

    • spark.shuffle.file.buffer 默认值为32K。调节map端缓冲的大小,避免频繁的磁盘IO操作,进而提升任务整体性能
  • 优化二:调节 reduce 端拉取数据缓冲区大小

    • spark.reducer.maxSizeInFlight 默认值为48M,设置shuffle read阶段buffer缓冲区大小,这个buffer缓冲决定了每次能够拉取多少数据
    • 在内存资源充足的情况下,可适当增加参数的大小(如96m),减少拉取数据的次数及网络传输次数,进而提升性能
  • 优化三:调节 reduce 端拉取数据重试次数及等待间隔

    • spark.shuffle.io.maxRetries,默认值3。最大重试次数
    • spark.shuffle.io.retryWait,默认值5s。每次重试拉取数据的等待间隔
    • 一般调高最大重试次数,不调整时间间隔
  • 优化四:调节 Sort Shuffle 排序操作阈值

    • spark.shuffle.sort.bypassMergeThreshold,默认值为200
    • 当使用SortShuffleManager时,如果的确不需要排序操作,建议将这个参数调大
  • 优化五:调节 Shuffle 内存大小

    • Spark给 Shuffle 阶段分配了专门的内存区域,这部分内存称为执行内存
    • 如果内存充足,而且很少使用持久化操作,建议调高这个比例,给 shuffle 聚合操作更多内存,以避免由于内存不足导致聚合过程中频繁读写磁盘
    • 合理调节该参数可以将性能提升10%左右
    • 尽量减少shuffle次数
// 两次shuffle
rdd.map(...).repartition(1000).reduceByKey(_ + _, 3000)
// 一次shuffle
rdd.map(...).repartition(3000).reduceByKey(_ + _)
  • 必要时主动shuffle,通常用于改变并行度,提高后续分布式运行速度
    rdd.repartiton(largerNumPartition).map(…)…

  • 使用treeReduce & treeAggregate替换reduce & aggregate。数据量较大时,reduce & aggregate一次性聚合,shuffle量太大,而treeReduce & treeAggregate是分批聚合,更为保险。


思维导图

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值