Spark Shuffle 原理

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

HashShuffleManager

未优化的

:::tips
在 shuffle write 前,应用分区器,根据对应的分区规则,计算出数据 partition 编号,然后将数据写入 bucket 内存中,当数据达到一定大小或数据全部处理完后,将数据溢写磁盘持久化。之所以要持久化,一方面是要减少内存存储空间压力,另一方面也是为了容错降低数据恢复的代价。
:::
spark_hashshuffle.png
:::tips
上图有 4 个 ShuffleMapTask 要在同一个 worker node 上运行,CPU core 数为 2;其中有 2 个 Executor,每个 Executor 有 1 个 core,可以同时运行两个 task。每个 task 的执行结果会被溢写到本地磁盘上。每个 task 包含 R 个缓冲区,R = reducer 个数(也就是下一个 stage 中 task 的个数),缓冲区被称为 bucket,其大小为spark.shuffle.file.buffer.kb ,默认是 32KB(Spark 1.1 版本以前是 100KB)。
:::
老版本的 HashShuffleManager 存在的问题:

  1. 产成的FileSegment过多。每个ShuffleMapTask产生 R(下游Task的数量)个FileSegment, M 个 ShuffleMapTask 就会产生 M * R 个文件。一般 Spark job 的 M 和 R 都很大,因此磁盘上 会存在大量的数据文件。
  2. 缓冲区占用内存空间大。每个 ShuffleMapTask 需要开 R 个 bucket,M 个 ShuffleMapTask 就会产生 M * R 个 bucket。虽然一个 ShuffleMapTask 结束后,对应的缓冲区可以被回收,但 一个 worker node 上同时存在的 bucket 个数可以达到 cores R 个,占用的内存空间也就达到 了 cores * R * 32 KB。对于 8 核 1000 个 reducer 来说,占用内存就是 256MB。

优化的(FileConsolidation)

spark_hashshuffle2.png
:::tips
可以明显看出,在一个 core 上连续执行的 ShuffleMapTasks 可以共用一个输出文件 ShuffleFile。先执行完的 ShuffleMapTask 形成 ShuffleBlock i,后执行的 ShuffleMapTask 可以将输出数据直接追加到 ShuffleBlock i 后面,形成 ShuffleBlock i’,每个 ShuffleBlock 被称为 FileSegment。下一个 stage 的 reducer 只需要 fetch 整个 ShuffleFile 就行了。这样,每个 worker 持有的文件数降为 cores * R。但是缓存空间占用大还没有解决
:::
FileConsolidation 功能可以通过 spark.shuffle.consolidateFiles=true 来开启

总结

  1. 优点:
    1. 快-不需要排序,也不需要维持hash表
    2. 不需要额外空间用作排序
    3. 不需要额外IO-数据写入磁盘只需一次,读取也只需一次
  2. 缺点:
    1. 当partitions大时,输出大量的文件(cores * R),性能开始降低
    2. 大量的文件写入,使文件系统开始变为随机写,性能比顺序写要降低100倍
    3. 缓存空间占用比较大

**SortShuffleManager **

三种shuffle方式

  • BypassMergeSortShuffleHandle
  • SerializedShuffleHandle
  • BaseShuffleHandle

SortShuffleManager.scala
:::tips
三种ShuffleHandle对应了三种ShuffleWtier

  1. SerializedShuffleHandle
    1. UnsafeShuffleWriter
  2. BypassMergeSortShuffleHandle
    1. BypassMergeSortShuffleWriter
  3. BaseShuffleHandle
    1. SortShuffleWriter
      :::

Shuffle方式的决定机制

image.png
image.png
image.png
ShuffleHandle 方式选择的条件判断直观图示:
shuffle策略.png

BaseShuffleHandle

在该模式下,数据会先写入一个内存数据结构中,此时根据不同的 shuffle 算子,可能选用不同的数据结构。如果是 reduceByKey 这种聚合类的 shuffle 算子,那么会选用 Map 数据结构,一边通过 Map 进行聚合,一边写入内存;如果是 join 这种普通的 shuffle 算子,那么会选用 Array 数据结构, 直接写入内存。接着,每写一条数据进入内存数据结构之后,就会判断一下,是否达到了某个临界阈值。如果达到临界阈值的话,那么就会尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。
在溢写到磁盘文件之前,会先根据 key 对内存数据结构中已有的数据进行排序。排序过后,会分批将数据写入磁盘文件。默认的 batch 数量是 10000 条,也就是说,排序好的数据,会以每批 1 万条 数据的形式分批写入磁盘文件。写入磁盘文件是通过 Java 的 BufferedOutputStream 实现的。 BufferedOutputStream 是 Java 的缓冲输出流,首先会将数据缓冲在内存中,当内存缓冲满溢之后再一次写入磁盘文件中,这样可以减少磁盘 IO 次数,提升性能。
一个 task 将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作,也就会产生多个临时 文件。最后会将之前所有的临时磁盘文件都进行合并,这就是 merge 过程,此时会将之前所有临时磁盘文件中的数据读取出来,然后依次写入最终的磁盘文件之中。此外,由于一个 task 就只对应一 个磁盘文件,也就意味着该 task 为下游 stage 的 task 准备的数据都在这一个文件中,因此还会单 独写一份索引文件,其中标识了下游各个 task 的数据在文件中的 start offset 与 end offset。 SortShuffleManager 由于有一个磁盘文件 merge 的过程,因此大大减少了文件数量。
比如第一个 stage 有 50 个 task,总共有 10 个 Executor,每个 Executor 执行 5 个 task,而第二个 stage 有 100 个 task。由于每个 task 最终只有一个磁盘文件,因此此时每个 Executor 上只有 5 个磁盘文件,所有 Executor 只有 50 个磁盘文件。
image.png

深入研究

从ShuffleWriter进入SortShuffleWriter

SortShuffleWriter.scala
image.png
ExternalSorter.scala
image.png
image.png

进入maybeSpill()

image.png
:::tips
map(PartitionedAppendOnlyMap)或者 buffer(PartitionedPairBuffer)填满了,是否要 spill,跟 MR 完全不同;
它会先尝试申请更多的内存空间,尽量不 spill; 只有在申请不到更多内存,且已经填充的数据超过阈值比例,才 spill;
:::

**ByPassMergeShuffle **

Reducer 端任务数比较少的情况下,基于 Hash Shuffle 实现机制明显比基于 Sort Shuffle 实现机制要快,因此基于 Sort Shuffle 实现机制提供了一个带 Hash 风格的回退方案,就是 bypass 运行机制。 对于 Reducer 端任务数少于配置属性 spark.shuffle.sort.bypassMergeThreshold 设置的个数时,使用带 Hash 风格的回退计划。
bypass 运行机制的触发条件如下:

  • shuffle map task 数量小于 spark.shuffle.sort.bypassMergeThreshold=200 参数的值
  • 不是聚合类的 shuffle 算子,没有对 key 排序的需求

此时,每个 task 会为每个下游 task 都创建一个临时磁盘文件,并将数据按 key 进行 hash 然后根 据 key 的 hash 值,将 key 写入对应的磁盘文件之中。当然,写入磁盘文件时也是先写入内存缓冲, 缓冲写满之后再溢写到磁盘文件的。最后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件。
该过程的磁盘写机制其实跟未经优化的 HashShuffleManager 是一模一样的,因为都要创建数量惊人的磁盘文件,只是在最后会做一个磁盘文件的合并而已。因此少量的最终磁盘文件,也让该机制相对未经优化的 HashShuffleManager 来说,shuffle read 的性能会更好。
而该机制与普通 BaseShuffleHandle 运行机制的不同在于:

  • 磁盘写机制不同;
  • 不会进行排序。

也就是说,启用该机制的最大好处在于,shuffle write 过程中,不需要进行数据的排序操作,也就节 省掉了这部分的性能开销。
image.png

TungstenSortShuffle

**UnsafeShuffleWriter(SerializedShuffleHandle)**是对普通 sort 的一种优化,排序的不是内容本身,而是内容序列化后字节数组的指针(元数据),把数据的排序转变为了指针数组的排序,实现了直接对序列化后的二进制数据进行排序。由于直接基于二进制数据进行操作,所以在这里面没有序列化和反序列化的过程。内存的消耗降低,相应的也会减少 gc 的开销。
这些优化的实现导致引入了一个新的内存管理模型,即 Page,Page 其实也是一个 MemoryBlock, 不 过因为它是引入了指针 Pointer 的特性,所以又支持 off-heap 以及 on-heap 两种模式,可以直接访问系统的内存。
采用了用 8 位 byte 的数组指针,这个指针数字的前24位存储着数据记录的PartitionID ,中间 13 位存储着内存页的编号,其实也是内存块的编号,最后 24 位存储着数据记录在这个内存页中的地址偏移量。
这种方式是利用 java 中的 unsafe API 完成的;这个 unsafe 类中就封装了一些像 C/C++中的指针操作,可以直接访问系统的内存。既然能够像指针一样直接访问内存,那就会存在一定的风险,比如给指针 赋值或者指针指向的地址加上偏移量后可能会指向系统程序的地址,从而覆盖原来地址中的内容,很 有可能会直接导致系统的崩溃,所以说指针操作是不安全的,因此给它取名为 UnsafeShuffleWriter。
image.png
要实现 Tungsten Sort Shuffle 机制需要满足以下条件:

  • Shuffle 的序列化器支持序列化后的对象的重定位(当前仅支持 KryoSerializer、Spark SQL 框架 自定义的序列化器)。
  • Shuffle 依赖中不带聚合操作或没有对输出进行排序的要求。
  • Shuffle 过程中的输出分区个数少于 2^24(16777216 )个。

参考

  1. Alexey Grishchenko 2015, Spark Architecture: Shuffle, Distributed Systems Architecture.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值