Spark Shuffle及其调优

本文详细介绍了Spark中的Shuffle机制,包括HashShuffle和SortShuffle的优化,以及Shuffle过程中的写入和读取操作。着重讨论了SortShuffleManager的bypass机制,分析了不同Shuffle策略对性能的影响。还提到了与MapReduce Shuffle的对比,并提供了Spark Shuffle的相关参数调优建议。
摘要由CSDN通过智能技术生成

Shuffle概述

  在MapReduce和Spark中都有Shuffle。对于MapReduce框架,Shuffle是连接Map和Reduce之间的桥梁,将Map阶段读取的数据进行Shuffle操作并输出到对应的Reduce,Reduce进行计算。MapReduce的Shuffle过程如图所示。
在这里插入图片描述
Shuffle实质上是对数据进行重组,在整个Shuffle过程中,往往伴随着大量的磁盘和网络I/O,因此Shuffle性能的高低也直接决定了整个程序的性能高低。Spark的Shuffle实现过程如图所示。
在这里插入图片描述
  Spark在DAG调度的过程中,会根据Shuffle过程来划分Stage阶段,即存在ShuffleDependency(宽依赖)的时候,会将作业job划分成多个Stage。划分Stage时,构建ShuffleDependency的时候会进行Shuffle注册,获取后续数据读取所需要的ShuffleHandle,最终每一个job提交后都会生成一个ResultStage和若干个ShuffleMapStage,其中ResultStage表示生成作业的最终结果所在的Stage。ResultStage与ShuffleMapStage中的task分别对应着ResultTask与ShuffleMapTask。一个job,除了最终的ResultStage外,其他若干ShuffleMapStage中各个ShuffleMapTask都需要将最终的数据根据相应的Partitioner对数据进行分组,然后持久化分区的数据。进行数据持久化目的在于1)容错;2)降低内存数据存储压力。

Spark的四种Shuffle机制

  负责Shuffle过程的执行、计算和处理的组件主要就是ShuffleManager,也即Shuffle管理器。ShuffleManager随着Spark的发展有两种实现的方式,分别为HashShuffleManager(spark1.2之前使用)和SortShuffleManager,因此Spark的Shuffle有Hash Shuffle和Sort Shuffle两种。
  在Spark 1.2以前,默认的shuffle计算引擎是HashShuffleManager。而HashShuffleManager有着一个非常严重的弊端,就是会产生大量的中间磁盘文件,进而由大量的磁盘IO操作影响了性能。因此在Spark 1.2以后的版本中,默认的ShuffleManager改成了SortShuffleManager。SortShuffleManager相较于HashShuffleManager来说,有了一定的改进。主要就在于,每个Task在进行Shuffle操作时,虽然也会产生较多的临时磁盘文件,但是最后会将所有的临时文件合并(merge)成一个磁盘文件,因此每个Task就只有一个磁盘文件。在下一个stage的shuffle read task拉取自己的数据时,只要根据索引读取每个磁盘文件中的部分数据即可。

spark1.2版本以前:hashShuffleManager
	未经优化的hashShuffleManager
	经过优化的hashShuffleManager

spark1.2版本以后:SortShuffleManager
	普通机制
	ByPass机制

HashShuffle机制

  在1.2版本之前,Spark使用HashShuffle,1.2版本之后使用Sort-Base Shuffle。Spark的运行主要分为两部分:一部分是以SparkContext为核心的驱动程序,另一部分是Worker节点上Task,它是运行实际任务的。程序运行的时候,Driver和Executor进程相互交互:运行什么任务,即Driver会通过网络传输为Executor分配Task;任务数据从哪儿获取,即Task要从 Driver 抓取其他上游Task的数据结果,所以这个过程中就会不断地产生网络结果。其中,下游Stage 向上游Stage申请数据的过程就称之为 Shuffle。

未优化的HashShuffle机制

  对相同的key执行hash算法,从而将相同的key都写入到一个磁盘文件中,而每一个磁盘文件都只属于下游stage的一个task。在将数据写入磁盘之前,会先将数据写入到内存缓冲,当内存缓冲填满之后,才会溢写到磁盘文件中。但是这种策略的不足在于,下游有几个task,上游的每一个task都就都需要创建几个临时文件,每个文件中只存储key取hash之后相同的数据,导致了当下游的task任务过多的时候,上游会堆积大量的小文件。
在这里插入图片描述

经过优化的HashShuffle机制

  在shuffle write过程中,task就不是为下游stage的每个task创建一个磁盘文件了。此时会出现shuffleFileGroup的概念,每个shuffleFileGroup会对应一批磁盘文件,磁盘文件的数量与下游stage的task数量是相同的。一个Executor上有多少个CPU core,就可以并行执行多少个task。而第一批并行执行的每个task都会创建一个shuffleFileGroup,并将数据写入对应的磁盘文件内。当Executor的CPU core执行完一批task,接着执行下一批task时,下一批task就会复用之前已有的shuffleFileGroup,包括其中的磁盘文件。也就是说,此时task会将数据写入已有的磁盘文件中,而不会写入新的磁盘文件中。因此,consolidate机制允许不同的task复用同一批磁盘文件,这样就可以有效将多个task的磁盘文件进行一定程度上的合并,从而大幅度减少磁盘文件的数量,进而提升shuffle write的性能。
  如果想使用优化之后的ShuffleManager,需要设置:spark.shuffle.consolidateFiles=true
在这里插入图片描述

未经优化:
上游的task数量:m
下游的task数量:n
上游的executor数量:k  (m>=k)
总共的磁盘文件:m*n

优化之后的:
上游的task数量:m
下游的task数量:n
上游的executor数量:k  (m>=k)
总共的磁盘文件:k*n

SortShuffleManager

  SortShuffleManager的运行机制主要分成两种,一种是普通运行机制,另一种是bypass运行机制。当ShuffleReadTask的数量小于等于spark.shuffle.sort.bypassMergeThreshold参数的值时(默认为200),就会启用bypass机制。

普通运行机制

  在普通模式下,数据会先写入一个内存数据结构中,此时根据不同的shuffle算子,可以选用不同的数据结构。如果是由聚合操作的shuffle算子,就是用map的数据结构(边聚合边写入内存),如果是join的算子,就使用array的数据结构(直接写入内存)。接着,每写一条数据进入内存数据结构之后,就会判断是否达到了某个临界值,如果达到了临界值的话,就会尝试的将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。
  在溢写到磁盘文件之前,会先根据key对内存数据结构中已有的数据进行排序,排序之后,会分批将数据写入磁盘文件。默认的batch数量是10000条,也就是说,排序好的数据,会以每批次1万条数据的形式分批写入磁盘文件,写入磁盘文件是通过Java的BufferedOutputStream实现的。BufferedOutputStream是Java的缓冲输出流,首先会将数据缓冲在内存中,当内存缓冲满溢之后再一次写入磁盘文件中,这样可以减少磁盘IO次数,提升性能。
  此时task将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写,会产生多个临时文件,最后会将之前所有的临时文件都进行合并,最后会合并成为一个大文件。最终只剩下两个文件,一个是合并之后的数据文件,一个是索引文件(标识了下游各个task的数据在文件中的start offset与end offset)。最终再由下游的task根据索引文件读取相应的数据文件。
在这里插入图片描述

shuffle write:mapper阶段,上一个stage得到最后的结果写出
shuffle read :reduce阶段,下一个stage拉取上一个stage进行合并

SortShuffleManager-bypass机制

下图说明了bypass SortShuffleManager的原理。
在这里插入图片描述
bypass运行机制的触发条件如下:
1)ShuffleMap Task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值(默认为200)
2)不是聚合类的Shuffle算子(比如reduceByKey)

  此时Task会为每个下游Task都创建一个临时磁盘文件,并将数据按key进行hash,然后根据key的hash值,将key写入对应的磁盘文件之中。当然,写入磁盘文件时也是先写入内存缓冲,缓冲写满之后再溢写到磁盘文件的。最后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件。
  该过程的磁盘写机制其实跟未经优化的HashShuffleManager是一模一样的,因为都要创建数量惊人的磁盘文件,只是在最后会做一个磁盘文件的合并而已。因此少量的最终磁盘文件,也让该机制相对未经优化的HashShuffleManager来说,ShuffleRead的性能会更好。
  而该机制与普通SortShuffleManager运行机制的不同在于:
1)磁盘写机制不同
2)不会进行排序
也就是说,启用该机制的最大好处在于,ShuffleWrite过程中,不需要进行数据的排序操作,也就节省掉了这部分的性能开销。

总结
  Map端的Shuffle一般为Shuffle的Write阶段,Reduce端的Shuffle一般为Shuffle的Read阶段。Spark的Shuffle分为两种实现:HashShuffle和SortShuffle。
  HashShuffle又分为普通机制和合并机制,普通机制因为其会产生MR个数的巨量磁盘小文件而产生大量性能低下的Io操作,从而性能较低,因为其巨量的磁盘小文件还可能导致OOM,HashShuffle的合并机制通过重复利用磁盘文件从而从而大幅度减少磁盘文件的数量,但是当Reducer 端的并行任务或者是数据分片过多的时候,依然会产生大量的磁盘小文件。
  SortShuffle也分为普通机制和bypass机制,普通机制在内存数据结构(默认为5M)完成排序,会产生2M个磁盘小文件。而当shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值或者算子不是聚合类的shuffle算子(比如reduceByKey)的时候会触发SortShuffle的bypass机制,SortShuffle的bypass机制不会进行排序,极大的提高了其性能。
  SortShuffleManager相较于HashShuffleManager来说,有了一定的改进。主要就在于,每个Task在进行shuffle操作时,虽然也会产生较多的临时磁盘文件,但是最后会将所有的临时文件合并(merge)成一个磁盘文件,因此每个Task就只有一个磁盘文件。在下一个stage的shuffle read task拉取自己的数据时,只要根据索引读取每个磁盘文件中的部分数据即可。

Spark Shuffle源码分析

  在Spark 1.0版本之前,Spark只支持Hash Shuffle,原理很简单:每个Task根据key的哈希值计算出每个key将要写入的partition,然后将数据写入一个文件供下游的Task来拉取。但是这种Hash Shuffle模式有其缺点:当并行度很高时,会产生很多中间落地的文件,对系统的内存和磁盘IO会造成很大压力。
  为了解决这个问题,在Spark 0.8.1中加入了Shuffle Consolidate File机制,在1.6版本之前,需要通过设置spark.shuffle.consolidateFiles设置为true(默认为false)来使用这个功能,1.6版本之后成为默认项。其实现原理为:对于同一个core的不同Task在写中间文件的时候可以共享追加同一个文件,这样就显著的减小了文件的数量。
  Shuffle Consolidate File机制虽然缓解了Shuffle过程产生文件过多的问题,但是并没有彻底解决内存和IO的问题,所以在Spark 1.1中实现了Sort Based Shuffle,通过spark.shuffle.manager选项可以设置,默认为Hash,而在Spark 1.2中Sort Based Shuffle取代Hash Based Shuffle成为默认选项,在Spark 2.0版本之后,Hash Based Shuffle已经不见踪影,Sort Based Shuffle成为唯一选项。
  Sort Based Shuffle的实现:首先,每个ShuffleMapTask不会为每个Reducer生成单独的一个文件,它会将所有的结果写到一个文件中,同时生成一个Index文件,Reducer可以通过这个Index文件取得它需要处理的数据,这样就避免了产生大量的中间文件,也就节省同时打开大量文件使用的内存和随机写带来的IO。过程是这样的:

  1. 每个Map Task会为下游的每一个Reducer,或者说每一个partition生成一个Array,将key-value数据写入到这个Array中,每一个partition中的数据并不会排序(避免不必要的排序)
  2. 每个Array中的数据如果超过某个阈值将会写到外部存储,这个文件会记录相应的partitionId以及保存了多少了数据条目等信息
  3. 最后用归并排
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值