Spark分布式内存计算框架-04 深入解析 Spark Shuffle 与内存管理:机制、调优与源码实战

Spark内存计算框架

Spark Core

Spark的shuffle过程

  • 任何一个分布式的计算系统,shuffle 都是最为致命的性能瓶颈,因为 shuffle 会产生数据的移动和网络拷贝,走网络拷贝就不是程序能决定的了,涉及到网络硬件的数据传输,所以任何时候,shuffle 都会产生性能的问题,spark 的 shuffle 经过多年发展已经逐渐趋于成熟,主要有早期的 HashShuffleManager 以及现在默认使用的 SortShuffleManager。
  • Spark 的 shuffle 演进历史:
    • 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退出历史舞台
  • Spark 的所有配置项:https://spark.apache.org/docs/2.3.3/configuration.html
  • spark-shuffle 参数配置官方说明:https://spark.apache.org/docs/2.3.3/configuration.html#shuffle-behavior
1. HashShuffleManager
未经优化的HashShuffleManager
  • 在 Spark 早期版本(1.2.0)之前都是使用的 HashShuffleManager,其会产生大量的小文件,具体实现过程如下图所示:

在这里插入图片描述

  • 在 mapTask 过程按照 Hash 的方式重组 partition 的数据,不进行排序。每个 mapTask 为每个 reduceTask 生成一个文件,通常会产生大量的文件(即对应为 M*R 个中间文件,其中 M 表示 mapTask 个数,R 表示 reduceTask 个数),伴随大量的随机磁盘 I/O 操作与大量的内存开销
  • HashShuffleManager缺陷:
    • mapTask 非常容易造成 OOM:如果产生大量的 MapTask-Buffer 很容易将缓冲区直接撑爆;
    • reduceTask 非常容易造成 OOM:如果 ReduceTask-Buffer 大量获取小文件很容易将缓冲区直接撑爆;
    • reduceTask 去拉取 mapTask 输出数据,大量小文件容易造成网络波动,产生大量小IO,增加机器负荷,容易引起网络失败而导致拉取失败。
经过优化的HashShuffleManager
  • 原始的 HashShuffleManager 会产生大量的小文件,造成网络以及磁盘的大量浪费,所以为了解决大量小文件的问题,后来引起一种改进的 HashShuffleManager。
  • 针对上面的小文件过多的问题,引入了 File Consolidation 机制
  • 一个 Executor 上所有的 mapTask 针对同一个分区(同一个 reduceTask)只生成一个文件,即将所有的 mapTask 相同的分区文件合并,这样每个 Executor 上最多只生成 N 个分区文件。

在这里插入图片描述

  • 尽管进过优化之后的 HashShuffleManager 有一定程度的小文件数量的减少,但是还是会产生很多小文件的问题。
  • 这样就减少了文件数,但是假如下游 Stage 的分区数 N 很大,还是会在每个 Executor 上生成 N 个文件。
  • 同样,如果一个 Executor 上有 K 个 Core,还是会开 K*N 个 Writer Handler,所以这里仍然容易导致OOM。
HashShuffleManager 源码解析
  • 导入 spark 1.2 版本的 spark-core 的 jar 包,然后就可以查看早期spark版本当中关于HashShuffleManager的源码
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-core_2.11</artifactId>
    <version>1.2.0</version>
</dependency>

在这里插入图片描述

  • 第一步:初始化 shuffle 管理器,也就是 ShuffleBlockManager,默认使用的是 FileShuffleBlockManager 这个实现类;
  • 第二步:注册 shuffle 管理器,通过 RDD 之间的依赖关系,进行注册 shuffle 管理器;
  • 第三步:溢写数据,通过 getWriter 方法,获取 HashShuffleWriter 这个对象,通过这个对象调用 write 方法来进行数据的写出;
  • 第四步:reduce 端接收数据,通过 getReader 方法,获取 HashShuffleReader 这个对象,通过这个对象调用 read 方法来进行数据的读取。
2. SortShuffleManager
  • 为了更好地解决 HashShuffleManager 的问题,Spark 参考了 MapReduce 中 Shuffle 的处理方式,引入基于排序的 Shuffle 写操作机制。
  • 总体上看来 Sort Shuffle 解决了 Hash Shuffle 的所有弊端,但是因为需要其 Shuffle 过程需要对记录进行排序,所以在性能上有所损失。
  • SortShuffleManager 的运行机制主要分成两种:
    • ① 普通运行机制:默认
    • ② bypass 运行机制:当 shuffle read task 的数量小于等于 spark.shuffle.sort.bypassMergeThreshold 参数的值(默认为200)时,就会启用bypass机制。
普通运行机制
  • 在普通模式下,每个 task 当中处理的数据,会先写入一个内存数据结构当中
    • 内存数据结构是 Map 或者 Array,根据不同的 shuffle 算子,选用不同的数据结构;
    • 如果是 reduceByKey 这类聚合 shuffle 算子,那么就会选用 Map 数据结构;
    • 如果是 join 这种普通 shuffle 算子,那么就会选用 Array 数据结构;
    • 每次写入一条数据,判断内存阈值,达到阈值,溢写到磁盘,清空内存结构数据。
  • 溢写之前,会根据 key 对内存数据结构进行排序,排序之后分批次写入磁盘,每批次默认写入 10000 条,使用 java 的 BufferedOutputStream 来实现的,可以减少磁盘 IO 次数,提升性能。
  • task 多次溢写,形成多个小文件,小文件最终进行合并,就是 merge 过程
    • 此时将之前所有的溢写文件全部读取出来,然后依次写入最终的磁盘文件当中形成一个大文件;
    • 为了解决大文件分配到各个下游 task 的数据标识问题,还会写入一份索引文件,索引文件标识了下游每个 task 当汇总所属数据的 start offset 以及 end offset。
  • SortShuffleManager 由于有一个磁盘文件 merge 的过程,因此大大减少了文件数量。比如一个 stage 有 50 个 task,总共有 10 个 Executor,每个 Executor 执行 5 个 task,由于每个 task 最终只有一个磁盘文件,因此此时每个 Executor 上只有 5 个磁盘文件,所有 Executor 只有 50 个磁盘文件。

在这里插入图片描述

byPass运行机制
  • byPass机制的触发条件:
    • shuffle reduce task 的数量小于 spark.shuffle.sort.bypassMergeThreshold 参数的值(默认200)
    • 不是预聚合类的 shuffle 算子(也就是没有 map-side aggregation 的 shuffle 算子(例如groupByKey或者groupBy等))

在这里插入图片描述

  • 此时 task 会为每个下游 task 都创建一个临时磁盘文件,对数据按照 key 进行 hash 取值,然后将对应的数据写入到对应的磁盘文件。最终进行磁盘文件的合并,并创建索引文件确定最后大的磁盘文件里面的数据属于哪一个下游的 reducetask。

在这里插入图片描述

  • 该机制与普通的 SortShuffleManager 不同在于:
    • 磁盘写入机制不同
    • 不会对数据进行排序
SortShuffleManager源码解析
  • 第一步:初始化 shuffle 管理器,也就是 IndexShuffleBlockResolver;
  • 第二步:注册 shuffle 管理器,通过 RDD 之前的依赖关系,来进行注册 shuffle 管理器,这里使用到的 shuffle 管理器主要有三个。分别是 BypassMergeSortShuffleHandle、SerializedShuffleHandle 和 BaseShuffleHandle。
    • ① BypassMergeSortShuffleHandle:如果 reduceTask 数量小于 200,且没有 map 端的聚合,那么就会是用 bypass 这种机制;
    • ② SerializedShuffleHandle:如果 reduceTask 数量大于 200,或者 map 端使用的 shuffle 是需要进行聚合的,那么就使用普通的序列化这种机制;
    • ③ BaseShuffleHandle:如果以上两种都不满足,那么就使用最基础的这种,需要对数据进行序列化。
  • 第三步:溢写数据,通过 getWriter 方法,获取 HashShuffleWriter 这个对象,通过这个对象调用 write 方法来进行数据的写出;
  • 第四步:reduce 端接受数据,通过 getReader 方法,获取 HashShuffleReader 这个对象,通过这个对象调用 read 方法来进行数据的读取;
  • 第五步:释放 IndexShuffleBlockResolver,shuffle 管理器用完了之后,就进行释放 shuffle 管理器;
  • 第六步:停止运行 shuffle,shuffle 整个阶段全部运行完成就结束 shuffle 过程。
3. Spark的Shuffle常用参数调优
spark.shuffle.file.buffer
  • 默认值:32K
  • 参数说明:该参数用于设置shuffle write task的BufferedOutputStream的buffer缓冲大小。将数据写到磁盘文件之前,会先写入buffer缓冲
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

讲文明的喜羊羊拒绝pua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值