Spark 从 0 到 1 学习(5) —— SparkShuffle详解

1. SparkShuffle 概念

reduceByKey 会将上一个 RDD 中的每个 key 对应的所有 value 聚合成一个 value, 然后生成一个新的 RDD,元素类型是 <K,V> 对的形式,这样每个 key 对应一个聚合起来的 value。

这样我们就会想到几个问题:

  • 聚合之前,每个 key 对应的 value 不一定都是在一个 partition 中,也不太可能在同一个节点上,因为 RDD 是分布式的弹性数据集,RDD 的 partition 极有可能分布在各个节点上。

针对这样的问题,我们如何聚合?

  • Shuffle Write:上一个stage 的每个 map task 就必须保证将自己处理的当前分区的数据相同的 key 写入一个分区文件中,可能会写入多个不同的分区文件中。
  • Shuffle Read:reduce task 就会从上一个 stage 的所有 task所在的机器上寻找属于自己的那份分区文件,这样就可以保证每个 key 所对应的 value 都会汇聚到同一个节点上去处理和聚合。

Spark 中有两种 Shuffle 管理类型,HashShuffleManagerSortShuffleManager。Spark1.2 之前是 HashShuffleManager,Spark1.2 引入SortShuffleManager。在 Spark2.0+ 版本中已经将HashShuffleManager丢弃。

2. HashShuffleManager

2.1 普通机制

  • 普通机制示意图

在这里插入图片描述

  • 执行流程

    1. 每一个 map task 将不同结果写到不同的 buffer 中,每个 buffer 的大小为 32k。buffer 起到缓存的作用。
    2. 每个 buffer 文件最后对应一个磁盘小文件。
    3. reduce task 来拉取对应的磁盘小文件。
  • 总结

    1. map task 的计算结果会根据分区器 (默认是hasPartitioner) 来决定写入到哪个磁盘小文件中去。reduce task 会去 map 端拉取相应的磁盘小文件。

    2. 产生的磁盘小文件个数:

      M(map task 个数) * R(reduce task 个数)
      
  • 存在的问题

    产生的磁盘小文件过多,会导致以下问题:

    1. 在 Shuffle Writer 过程中会产生很多写磁盘小文件的对象。
    2. 在 Shuffle Read 过程中会产生很多读磁盘小文件的对象。
    3. 在 JVM 堆内存中对象过多会造成频繁的gc,gc无法解决运行需要的内存的话,就会 OOM。
    4. 在数据传输过程中会有频繁的网络通信,频繁的网络通信出现通信故障的可能性大大增加,一旦网络通信出现故障会导致 shuffle file cannot find,由于这个错误导致 task失败。TaskScheduler 不负责重试,由DAGSchedule负责重试 Stage。

2.2 合并机制

  • 合并机制示意图

在这里插入图片描述

  • 执行流程

    1. 多个 map task 公用一个buffer,然后写入磁盘文件。
    2. reduce task 来拉取磁盘小文件
  • 总结、

    产生的磁盘小文件的个数:

    C(core 的个数) * R(reduce task 个数)
    

3. SortShuffleManager

3.1 普通机制

  • 普通机制示意图

在这里插入图片描述

  • 执行流程

    1. map task 的计算结果会写入到一个内存数据结构里面,内存数据结构默认是 5M。
    2. 在 shuffle 的时候会有一个定时器,不定期的去估算这个内存结构的大小,当内存结构中的数据超过 5M 时,比如现在内存结构中的数据为 5.01M,那么他会申请 5.01*2 - 5 = 5.02M内存数据结构。
    3. 如果申请内存成功,不会进行溢写磁盘。如果申请不成功,这时候会发送溢写磁盘。
    4. 在溢写磁盘之前,内存结构中的数据会进行排序分区。
    5. 然后开始溢写磁盘,写磁盘是以 batch 的形式去写,一个 batch 是 1 万条数据。
    6. map task 执行完成后,会将这些磁盘小文件合并成一个大的磁盘文件,同时生成一个索引文件。
    7. reduc task 去 map 端拉取数据的时候,首先解析索引文件,根据索引文件再去拉取对应的数据。
  • 总结

    产生磁盘小文件的个数:

    2 * M (map task 的个数)
    

3.2 bypass 机制

  • bypass 机制示意图

    在这里插入图片描述

  • 执行流程

    执行流程和普通模式基本没有什么区别,只是少了一步排序操作

  • 总结

    1. bypass 运行机制的触发条件如下:

      shuffle reduc task 的数量小于 spark.shuffle.sort.bypassMergeThreahold的阐述值。默认值 200。

    2. 产生的磁盘小文件个数: 2 * M (map task 的个数)

4. Shuffle 文件寻址

4.1 MapOutputTracker

MapOutputTracker是 Spark 架构中的一个模块,是一个主从架构。管理磁盘小文件的地址。

  • MapOutputTrackerMaster是主对象,存在于 Driver 中。
  • MapOutputTrackerWorker是从对象,存在于 Excutor 中。

4.2 BlockManager

BlockManager 块管理者,是 Spark 架构中的一个模块,也是一个主从架构。

  • BlockManagerMaster,主对象,存在于 Driver 中。

    BlockManagerMaster会在集群中用到广播变量和缓存数据或者删除缓存数据的时候,通知 BlockManagerSlave传输或者删除数据。

  • BlockManagerSlave,从对象,存在于 Executor 中。

    BlockManagerSlave会与BlockManagerMaster之间通信。

无论在 Driver 端的 BlockManagerMaster还是在 Executor 端的 BlockManagerSlave都含有三个对象:

  1. DiskStore:负责磁盘的管理。
  2. MemoryStore:负责内存的管理。
  3. BlockTransferService:负责数据的传输。

4.3 Shuffle 文件寻址

如图所示,shuffle 文件寻址流程:

在这里插入图片描述

  1. 当 map task 执行完成后,会将 task 的执行情况和磁盘小文件的地址封装到 MapStatus对象中,通过 MapOutputTrackerWorker对象向 Driver 中的 MapOutputTrackerMaster汇报。
  2. 在所有的 map task 执行完毕后,Driver 就掌握了所有的磁盘小文件的地址。
  3. 在 reduce task 执行之前,会通过 Executor 中 MapOutputTrackerWorker向 Driver 端的 MapOutputTrackerMaster获取磁盘小文件的地址。
  4. 获取到磁盘小文件地址后,会通过 BlockManager连接数据所在节点,然后通过 BlockTrasferService进行数据的传输。
  5. BlockTransferService默认启动 5 个 task 去节点拉取数据。默认情况下,5 个 task 拉取数据量不能超过 48M。

5. Shuffle 优化

5.1 调优参数

  • spark.shuffle.file.buffer

    默认值:32k

    参数说明:该参数用于设置 shuffle write taskBufferedOutputStream的 buffer 缓冲大小。将数据写到磁盘之前,会先写入 buffer 缓冲中。待缓冲区写满之后,才会溢写到磁盘。

    调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的值(比如 64k),从而减少 shuffle write 过程中溢写磁盘文件的次数。也就可以减少磁盘 IO 次数,进而提升性能。在实践中发现,合理调节该参数,性能会有 1%~5% 的性能提升。

  • spark.reducer.maxSizeInFlight

    默认值:48M

    参数说明:该参数用于设置 shuffle read task 的 buffer 缓冲区大小,这个 buffer 决定每次能够拉取多少数据。

    调优建议:如果作业可用的内存资源较为充足,可以适当增加该值(比如 96M),从而减少拉取数据次数。也就减少网络传输的次数,进而提升性能。在实践中发现,合理调节该参数,性能会有 1%~5% 的性能提升。

  • spark.shuffle.io.maxRetries

    默认值:3

    参数说明:shuffle read task 从 shuffle write task 所在节点拉取属于自己的数据时,如果因为网络异常导致拉取失败,是会自动进行重试的。该参数就代表了可以重试的最大次数。如果在指定次数之内拉取还是没有成功,就可能会导致作业执行失败。

    调优建议:对于那些包含了特别耗时的 shuffle 操作的作业,建议增加重试最大次数(比如 5次),以避免由于JVM的full gc或者网络不稳定等因素导致的数据拉取失败。在实践中发现,对于针对超大数据量(数十亿~上百亿)的 shuffle 过程,调节该参数可以大幅度提升稳定性。

    shuffle file not find taskScheduler不负责重试task,由DAGScheduler负责重试stage

  • spark.shuffle.io.retryWait

    默认值:5s
    参数说明:具体解释同上,该参数代表了每次重试拉取数据的等待间隔,默认是5s。
    调优建议:建议加大间隔时长(比如60s),以增加shuffle操作的稳定性。

  • spark.shuffle.memoryFraction

    默认值:0.2

    参数说明:该参数代表了Executor内存中,分配给shuffle read task进行聚合操作的内存比例,默认是20%。

    调优建议:如果内存充足,而且很少使用持久化操作,建议调高这个比例,给shuffle read的聚合操作更多内存,以避免由于内存不足导致聚合过程中频繁读写磁盘。在实践中发现,合理调节该参数可以将性能提升10%左右。

  • spark.shuffle.manager

    默认值:sort|hash

    参数说明:该参数用于设置ShuffleManager的类型。Spark 1.5以后,有三个可选项:hash、sort和tungsten-sort。HashShuffleManager是Spark 1.2以前的默认选项,但是Spark 1.2以及之后的版本默认都是SortShuffleManager了。tungsten-sort与sort类似,但是使用了tungsten计划中的堆外内存管理机制,内存使用效率更高。

    调优建议:由于SortShuffleManager默认会对数据进行排序,因此如果你的业务逻辑中需要该排序机制的话,则使用默认的SortShuffleManager就可以;而如果你的业务逻辑不需要对数据进行排序,那么建议参考后面的几个参数调优,通过bypass机制或优化的HashShuffleManager来避免排序操作,同时提供较好的磁盘读写性能。这里要注意的是,tungsten-sort要慎用,因为之前发现了一些相应的bug。

  • spark.shuffle.sort.bypassMergeThreshold----针对SortShuffle

    默认值:200

    参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作,而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。

    调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuffle read task的数量。那么此时就会自动启用bypass机制,map-side就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此shuffle write性能有待提高。

  • spark.shuffle.consolidateFiles----针对HashShuffle

    默认值:false

    参数说明:如果使用HashShuffleManager,该参数有效。如果设置为true,那么就会开启consolidate机制,会大幅度合并shuffle write的输出文件,对于shuffle read task数量特别多的情况下,这种方法可以极大地减少磁盘IO开销,提升性能。

    调优建议:如果的确不需要SortShuffleManager的排序机制,那么除了使用bypass机制,还可以尝试将spark.shffle.manager参数手动指定为hash,使用HashShuffleManager,同时开启consolidate机制。在实践中尝试过,发现其性能比开启了bypass机制的SortShuffleManager要高出10%~30%。

5.2 调优方式

  • 代码调优

    sparkConf.set("spark.shuffle.file.buffer","64K")
    

    这种方式优先级最高,但是由于硬编码,调优方式不灵活,需要修改代码才生效,不推荐。

  • 配置文件调优

    修改 conf/spark-defaults.conf文件

    spark.shuffle.file.buffer=64k
    

    这种方式优先级第三,用于所有的 spark 程序。

  • 启动命令调优

    ./spark-submit --conf spark.shuffle.file.buffer=64 --conf spark.reducer.maxSizeInFlight=96
    

    这种方式优先级第二,可以随程序运行修改调优参数,比较灵活,建议使用这种方式。

猜你喜欢

转载自blog.csdn.net/dwjf321/article/details/109048143