spark shuffle演进

1.Shuffle定义

Spark MapReduce 分布式计算框架中,数据被分成一块一块的分区,分布在集群中各节点上,每个计算任务一次处理一个分区,当需要对具有某种共同特征的一类数据进行计算时,就需要将集群中的这类数据汇聚到同一节点。这个按照一定的规 则对数据重新分区的过程就是 Shuffle


2.Spark Shuffle的两个阶段

SparkShuffle分为Write和Read两个阶段,分属于两个不同的Stage,前者是Parent Stage的最后一步,后者是Child Stage的第一步。
Stage的划分在 spark yarn 提交流程中已经了解到分为ResultStage和ShuffleMapStage。
Spark Shuffle 的流程简单抽象为以下几步:
Shuffle Write
    Map Side combine(if needed)
    Write to local output file
Shuffle Read
   Block fetch
   Reduce side combine
   Sort(if needed)

3.Spark Shuffle 技术演进

  1. Spark 1.1 以前是Hash Shuffle
  2. Spark 1.1 引入了Sort Shuffle
  3. Spark 1.6 Tungsten-sort并入Sort Shuffle(利用对外内存进行排序)
  4. Spark 2.0 Hash Shuffle退出历史舞台

3.1 Hash Shuffle V1 

相对于传统的 MapReduce Spark 假定大多数情况下 Shuffle 的数据不需要排序,强制排序反而会降低性能。因此不在 Shuffle Read 时做 Merge Sort ,如果需要合并的操作的话,则会使用聚合(agggregator ),即用了一个 HashMap (实际上是一个 AppendOnlyMap)来将数据进行合并。
Map Task 过程按照 Hash 的方式重组 Partition 的数据,不进行排序。每个 Map Task 为每个 Reduce Task 生成一个文件,通常会产生大量的文件(即对应为 M*R 个中间文件,其中 M 表示 Map Task 个数, R 表示 Reduce Task 个数),伴随大量的随机磁盘 I/O 操作与大量的内存开销。
 
Hash Shuffle V1 两个严重问题 :
  • 生成大量文件,占用文件描述符,同时引入 DiskObjectWriter 带来的 WriterHandler 的缓存也非常消耗内存
  • 如果在 Reduce Task 时需要合并操作的话,会把数据放在一个 HashMap 中进行合并,如果数据量较大,很容易引发 OOM

3.2 Hash Base Shuffle V2 -- File Consolidation

Hash Base Shuffle V2 核心思想: 允许不同的task复用同一批磁盘文件 ,有效将多个task的磁盘文件进行一定程度上的合并,从而大幅度减少磁盘文件的数量,进而提升shuffle write的性能。 一个 Executor 上所有的 Map Task 生成的分区文件只有一份,即将所有的 Map Task 相同的分区文件合并,这样每个 Executor 上最多只生成 N 个分区文件。

 
 
Hash Shuffle 规避了排序,提高了性能;总的来说在Hash Shuffle过程中生成海量的小文件(Hash Base Shuffle V2生成海量小文件的问题得到了一定程度的缓解)。 这样减少了文件数,但是假如下游 Stage 分区数 N 很大,还是会在每个 Executor 上生成 N 个文件,同样,如果一个 Executor 上有 K 个 Core ,还是会开 K*N Writer Handler ,这里仍然 容易导致OOM
 


3.3、Sort Base Shuffle V1

每个 Task 不会为后续的每个 Task 创建单独的文件,而是 将所有对结果写入同一个 文件 。该文件中的记录首先是按照 Partition Id 排序,每个 Partition 内部再按照Key 进行排序, Map Task 运行期间会顺序写每个 Partition 的数据, 同时生成一个 索引文件 记录每个 Partition 的大小和偏移
Reduce 阶段, Reduce Task 拉取数据做 Combine 时不再采用 HashMap ,而是采用ExternalAppendOnlyMap ,该数据结构在做 Combine 时, 如果内存不足,会刷写磁盘 ,避免大数据情况下的 OOM。
总体上看来 Sort Shuffle 解决了 Hash Shuffle 的所有弊端,但是因为需要其 Shuffle过程需要对记录进行排序,所以在性能上有所损失。
这里通过减少文件数来避免因为打开过多文件而导致的OOM,同时在reduce端引入磁盘溢写的方式来避免reduce Task的OOM
 
Tungsten-Sort Based Shuffle / Unsafe Shuffle
Spark 1.5.0 开始, Spark 开始了钨丝计划( Tungsten ),目的是优化内存和 CPU 的使用,进一步提升Spark 的性能。由于使用了 堆外内存 ,而它基于 JDK Sun Unsafe API,故 Tungsten-Sort Based Shuffle 也被称为 Unsafe Shuffle。
它的做法是 将数据记录用二进制的方式存储 ,直接在 序列化的二进制数据上 Sort 而不是在 Java 对象上,这样一方面可以 减少内存的使用和 GC 的开销 ,另一方面 避免 Shuffle 过程中频繁的序列化以及反序列化 。在排序过程中,它提供 cache-efficient sorter,使用一个 8 bytes 的指针,把排序转化成了一个指针数组的排序,极大的优化了排序性能。
但是使用 Tungsten-Sort Based Shuffle 有几个限制 Shuffle 阶段不能有 aggregate 操作 ,分区数不能超过一定大小( 2^24-1 ,这是可编码的最大 Parition Id),所以像 reduceByKey 这类有 aggregate 操作的算子是不能使用 Tungsten-Sort Based Shuffle,它会退化采用 Sort Shuffle
 

3.4 Sort Shuffle V2

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的演进就结束了 下面说说shuffle的细节。

4.Shuffle Writer

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

以上 ShuffleWriter 有各自的应用场景。分别如下:

  • 不满足以下条件使用 SortShuffleWriter,保底方案
  • 没有map端聚合(groubykey没有map端的聚合),RDDpartitions分区数小于16,777,216,且 Serializer支持 relocationSerializer 可以对已经序列化的对象进行排序,这种排序起到的效果和先对数据排序再序列化一致】,使用UnsafeShuffleWriter
  • 没有map端聚合操作 且 RDDpartition分区数小于200个而且不是聚合类的shuffle算子,使用 BypassMergerSortShuffleWriter  

Shuffle Writer 流程
数据先写入一个内存数据结构中。不同的shuffle算子,可能选用不同的数据结构
     如果是 reduceByKey
聚合类的算子,选用 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

5.Shuffle MapOutputTracker

Spark shuffle 过程分为 Writer Reader:
  • Writer负责生成中间数据
  • Reader负责整合中间数据
而中间数据的元信息,则由 MapOutputTracker 负责管理。 它负责 Writer 和Reader的沟通。 Shuffle Writer 会将中间数据保存到 Block 里面,然后将数据的位置发送给 MapOutputTracker。  Shuffle Reader 通过向 MapOutputTracker 获取中间数据的位置之后,才能读取到数据。
Shuffle Reader
需要提供 shuffleId mapId reduceId 才能确定一个中间数据:
  • shuffleId,表示此次shuffle的唯一id
  • mapId,表示map rdd 的分区索引,表示由哪个父分区产生的数据
  • reduceId,表示reduce端的分区索引,表示属于子分区的那部分数据

MapOutputTrackerexecutordriver端都存在:

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


6.Shuffle Reader

  1. Map Task 执行完毕后会将文件位置、计算状态等信息封装到 MapStatus 对象中,再由本进程中的 MapOutPutTrackerWorker 对象将其发送给Driver进程的MapOutPutTrackerMaster对象
  2. Reduce Task开始执行之前会先让本进程中的 MapOutputTrackerWorker 向Driver 进程中的 MapOutputTrackerMaster 发动请求,获取磁盘文件位置等信息
  3. 当所有的Map Task执行完毕后,Driver进程中的 MapOutputTrackerMaster 就掌握了所有的Shuffle文件的信息。此时MapOutPutTrackerMaster会告诉MapOutPutTrackerWorker磁盘小文件的位置信息
  4. 完成之前的操作之后,由 BlockTransforService Executor 所在的节点拉数据,默认会启动五个子线程。每次拉取的数据量不能超过48M

7.Hadoop Shuffle Spark Shuffle 的区别

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

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

8.Shuffle优化

开发过程中对 Shuffle 的优化:

  • 减少Shuffle过程中的数据量
  • 避免Shuffle
以下介绍 Shuffle 的优化主要是参数优化:
优化一:调节
map 端缓冲区大小
  • 合理设置参数,性能会有 1%~5% 的提升
  • spark.shuffle.file.buffer 默认值为32Kshuffle write阶段buffer缓冲大小。将数据写到磁盘文件之前,会先写入buffer缓冲区,缓冲写满后才溢写到磁盘。
  • 调节map端缓冲的大小,避免频繁的磁盘IO操作,进而提升任务整体性能
优化二:调节 reduce 端拉取数据缓冲区大小
  • spark.reducer.maxSizeInFlight 默认值为48M。设置shuffle read阶段buffer缓冲区大小,这个buffer缓冲决定了每次能够拉取多少数据
  • 在内存资源充足的情况下,可适当增加参数的大小(如96m),减少拉取数据的次数及网络传输次数,进而提升性能
  • 合理设置参数,性能会有 1%~5% 的提升
优化三:调节 reduce 端拉取数据重试次数及等待间隔
  • Shuffle read阶段拉取数据时,如果因为网络异常导致拉取失败,会自动进行重试
  • spark.shuffle.io.maxRetries 默认值3。最大重试次数
  • spark.shuffle.io.retryWait,默认值5s。每次重试拉取数据的等待间隔
  • 一般调高最大重试次数,不调整时间间隔
优化四:调节 Sort Shuffle 排序操作阈值
 
  • 如果shuffle reduce task的数量小于阈值,则shuffle write过程中不会进行排序操作,而是直接按未经优化的Hash Shuffle方式写数据,最后将每个task产生的所有临时磁盘文件都合并成一个文件,并创建单独的索引文件
  • spark.shuffle.sort.bypassMergeThreshold,默认值为200
  • 当使用SortShuffleManager时,如果的确不需要排序操作,建议将这个参数调大
优化五:调节 Shuffle 内存大小
  • Spark Shuffle 阶段分配了专门的内存区域,这部分内存称为执行内存
  • 如果内存充足,而且很少使用持久化操作,建议调高这个比例,给 shuffle 聚合操作更多内存,以避免由于内存不足导致聚合过程中频繁读写磁盘
  • 合理调节该参数可以将性能提升10%左右
 
 
 

 
 
 
 
 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值