Spark的Shuffle过程

1. Spark Shuffle概述

Shuffle就是对数据进行重组,由于分布式计算的特性和要求,在实现细节上更加繁琐和复杂。

在MapReduce框架中,Shuffle是连接Map和Reduce之间的桥梁,Map阶段通过shuffle读取数据,并输出到对应的Reduce端;而Reduce阶段负责从Map端拉取数据并进行计算。在整个shuffle过程中,往往伴随着大量的磁盘和网络IO,所以shuffle性能的高低也直接决定了整个应用程序的性能高低。

在Spark中也有自己的shuffle过程,RDD之间的关系包含宽窄依赖,在宽依赖之间是存在shuffle过程的,因此在spark程序的每个job中,都是根据是否有shuffle操作进行阶段(stage)划分,每个stage都是一系列的RDD map操作。

2. Shuffle的作用

shuffle的中文解释是“洗牌”,可以理解为将集群中各节点上的数据重新打乱整合的过程,下面以问题的形式来阐述Spark shuffle的作用。

问题:每一个key对应的value不一定都是在一个partition中,也不太可能在同一个节点上,因为RDD是分布式的弹性的数据集,他的partition极有可能分布在各个节点上,那么他们怎么聚合呢?

Shuffle Write:上一个stage的每个map task就必须保证将自己处理的当前分区中的数据相同的key写入一个分区文件中,可能会写入多个不同的分区文件中

Shuffle Read:reduce task就会从上一个stage的所有task所在的机器上寻找属于自己的那些分区文件,这样就可以保证每一个key所对应的value都会汇聚到同一个节点上去处理和聚合

3. Spark Shuffle的运行时机

先拿Spark shuffle与MR shuffle 做个类比,spark的shuffle过程是在stage与stage之间进行的,那么两个stage可以一个看做是Map task,一个是Reduce task。

产生shuffle的算子

算子作用
distinct去重
reduceBykey聚合
groupBykey分组聚合
combinerBykey聚合
sortBykey排序
sortBy排序(按某个字段)
coalesce重分区
repartition重分区
join表关联

4. Spark Shuffle的运行机理及图解

MR Shuffle过程

在这里插入图片描述

4.1 HashShuffle

4.1.1 HashShuffle概述

在spark-1.6版本之前,一直使用HashShuffle,在spark-1.6版本之后使用Sort-Base Shuffle,因为HashShuffle存在的不足所以就替换了HashShuffle.

我们知道,Spark的运行主要分为2部分:一部分是驱动程序,其核心是SparkContext;另一部分是Worker节点上Task,它是运行实际任务的。程序运行的时候,Driver和Executor进程相互交互:运行什么任务,即Driver会分配Task到Executor,Driver 跟 Executor 进行网络传输; 任务数据从哪儿获取,即Task要从 Driver 抓取其他上游的 Task 的数据结果,所以有这个过程中就不断的产生网络结果。其中,下一个 Stage 向上一个 Stage 要数据这个过程,我们就称之为 Shuffle。

4.1.2 优化前的Hash shuffle机制

在这里插入图片描述
shuffle write阶段

shuffle read阶段

缺点:
在Hash shuffle没有优化前,每一个shuffle MapTask 会为每一个ReduceTask创建一个buffer缓存,并且会为每一个buffer创建一个block文件,这样就会导致shuffle过程中产生大量磁盘小文件。

磁盘小文件过多带来的问题?

  1. Write阶段创建大量的写文件对象
  2. read阶段,来拉去数据时就要产生大量的网络IO
  3. read阶段创建大量的读文件对象

注意:创建对象过多的话,会导致JVM内存不足,JVM内存不足就会导致GC(OOM)

4.1.3 优化后的Hash shuffle机制

在这里插入图片描述
每一个Executor进程根据核数,决定Task的并发数量,比如executor核数是2,就是可以并发运行两个task,如果是一个则只能运行一个task。

假设executor核数是1,ShuffleMapTask数量是M,那么它依然会根据ResultTask的数量R,创建R个buffer缓存,然后对key进行hash,数据进入不同的buffer中,每一个bucket对应着一个block file,用于刷新buffer缓存里的数据。

然后下一个task运行的时候,那么不会再创建新的buffer和block file,而是复用之前的task已经创建好的buffer和block file。即所谓同一个Executor进程里所有Task都会把相同的key放入相同的buffer缓冲区中。

这样的话,生成文件的数量就是(本地worker的executor数量executor的coresResultTask数量)如上图所示,即2 * 1* 3 = 6个文件,每一个Executor的shuffleMapTask数量100,ReduceTask数量为100,那么

未优化的HashShuffle的文件数是2 1 100100 =20000,优化之后的数量是21*100 = 200文件,相当于少了100倍

缺点:如果 Reducer 端的并行任务或者是数据分片过多的话则 Core * Reducer Task 依旧过大,也会产生很多小文件。

磁盘小文件个数 = core * reduce Task

4.2 SortShuffle

SortShuffle介绍

为了缓解Shuffle过程产生文件数过多和Writer缓存开销过大的问题,spark引入了类似于hadoop Map-Reduce的shuffle机制。该机制每一个ShuffleMapTask不会为后续的任务创建单独的文件,而是会将所有的Task结果写入同一个文件,并且对应生成一个索引文件。以前的数据是放在内存缓存中,等到数据完了再刷到磁盘,现在为了减少内存的使用,在内存不够用的时候,可以将输出溢写到磁盘,结束的时候,再将这些不同的文件联合内存的数据一起进行归并,从而减少内存的使用量。一方面文件数量显著减少,另一方面减少Writer缓存所占用的内存大小,而且同时避免GC的风险和频率。

SortShuffleManager的运行机制主要分成两种。

  1. 普通运行机制
  2. bypass运行机制。

当shuffle read task(Reduce Task)的数量小于等于spark.shuffle.sort.bypassMergeThreshold参数的值时(默认为200),就会启用bypass机制。当shuffle read task(Reduce Task)的数量小于等于spark.shuffle.sort.bypassMergeThreshold参数的值时(默认为200),就会启用bypass机制。

4.2.1 普通运行机制

在这里插入图片描述
写入内存数据结构

该图说明了普通的SortShuffleManager的原理。在该模式下,数据会先写入一个内存数据结构中(默认5M),此时根据不同的shuffle算子,可能选用不同的数据结构。如果是reduceByKey这种聚合类的shuffle算子,那么会选用Map数据结构,一边通过Map进行聚合,一边写入内存;如果是join这种普通的shuffle算子,那么会选用Array数据结构,直接写入内存。接着,每写一条数据进入内存数据结构之后,就会判断一下,是否达到了某个临界阈值。如果达到临界阈值的话,那么就会尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。

注意:

shuffle中的定时器:定时器会检查内存数据结构的大小,如果内存数据结构空间不够,那么会申请额外的内存,申请的大小满足如下公式:

applyMemory=nowMenory*2-oldMemory

申请的内存=当前的内存情况*2-上一次的内嵌情况

意思就是说内存数据结构的大小的动态变化,如果存储的数据超出内存数据结构的大小,将申请内存数据结构存储的数据*2-内存数据结构的设定值的内存大小空间。申请到了,内存数据结构的大小变大,内存不够,申请不到,则发生溢写

排序

在溢写到磁盘文件之前,会先根据key对内存数据结构中已有的数据进行排序。

溢写

排序过后,会分批将数据写入磁盘文件。默认的batch数量是10000条,也就是说,排序好的数据,会以每批1万条数据的形式分批写入磁盘文件。写入磁盘文件是通过Java的BufferedOutputStream实现的。BufferedOutputStream是Java的缓冲输出流,首先会将数据缓冲在内存中,当内存缓冲满溢之后再一次写入磁盘文件中,这样可以减少磁盘IO次数,提升性能。

merge

一个task将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作,也就会产生多个临时文件。最后会将之前所有的临时磁盘文件都进行合并,这就是merge过程,此时会将之前所有临时磁盘文件中的数据读取出来,然后依次写入最终的磁盘文件之中。此外,由于一个task就只对应一个磁盘文件,也就意味着该task为Reduce端的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个磁盘文件。

注意:

①block file= 2M

一个map task会产生一个索引文件和一个数据大文件

② m*r>2m(r>2):

SortShuffle会使得磁盘小文件的个数再次的减少

4.2.2 bypass运行机制

在这里插入图片描述
bypass运行机制的触发条件如下:

  1. shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值。

  2. 不是聚合类的shuffle算子(比如reduceByKey)。

此时task会为每个reduce端的task都创建一个临时磁盘文件,并将数据按key进行hash然后根据key的hash值,将key写入对应的磁盘文件之中。当然,写入磁盘文件时也是先写入内存缓冲,缓冲写满之后再溢写到磁盘文件的。最后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件。

该过程的磁盘写机制其实跟未经优化的HashShuffleManager是一模一样的,因为都要创建数量惊人的磁盘文件,只是在最后会做一个磁盘文件的合并而已。因此少量的最终磁盘文件,也让该机制相对未经优化的HashShuffleManager来说,shuffle read的性能会更好。

而该机制与普通SortShuffleManager运行机制的不同在于:

第一,磁盘写机制不同;

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

5. Spark的内存管理及参数调优

5.1 spark的内存管理

Spark的内存管理分为静态内存管理和统一内存管理。

静态内存管理:内存存储、执行内存和其他内存的大小在运行期间是固定的。

统一内存管理:Spark1.6之后引入的,与静态内存管理的不同在于储存内存和执行内存共享同一块空间,可以互相借用对方的空间。

5.1.1 静态内存管理图解

在这里插入图片描述

5.1.2 统一内存管理图解

在这里插入图片描述

5.2 shuffle调优

spark.shuffle.file.buffer
默认值:32k
参数说明:该参数用于设置shuffle write task的BufferedOutputStream的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操作的作业,建议增加重试最大次数(比如60次),以避免由于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
参数说明:该参数用于设置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
默认值:200
参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作,而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。
调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuffle read task的数量。那么此时就会自动启用bypass机制,map-side就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此shuffle write性能有待提高。

如果你的Application在执行的过程中,出现了类似reduce OOM的错误,错误原因会有哪一些?
1、代码不规范。。。
三种解决方案:
1、提高Executor的内存
2、提高shuffle聚合的内存比例
3、减少每次拉去的数据量

6. 磁盘小文件寻址

未完待续。。。。

7. MR的shuffle过程与Spark shuffle的不同之处

比较不同点
spark shuffle与MR shufflespark中HashShuffle的shuffle write中没有分组和排序
SortShuffle普通机制与MR shuffleSortShuffle的内存是约等于5M且动态变化的,而MR的内存是固定100M
SortShuffle bypass机制与MR shuffleSpark中SortShuffle的bypass运行机制中没有排序,Spark shuffle默认是SortShuffle的bypass运行机制,因为它没有排序和分组,所以这也是比MR快的原因之一
  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SparkShuffle过程是指在数据处理过程中,将数据重新分区和排序的过程。它是Spark中非常重要的一个操作,用于将数据从一个RDD的分区传输到另一个RDD的分区。 SparkShuffle过程包括两个阶段:Map阶段和Reduce阶段。 在Map阶段,每个Executor上的任务(Task)会将输入数据根据指定的分区函数进行分区,并将分区后的数据写入磁盘上的.data文件中。同时,还会生成一个.index文件,用于记录每个分区的数据在.data文件中的位置信息。 在Reduce阶段,Spark会根据分区函数将数据重新分配到不同的Executor上的任务中。每个任务会读取自己负责的分区数据,并进行合并、排序等操作,最终生成最终结果。 SparkShuffle过程可以使用不同的策略来实现,其中包括BypassMergeSortShuffleWriter、SortShuffleWriter和UnsafeSortShuffleWriter等。 BypassMergeSortShuffleWriter是一种优化策略,它会尽量减少数据的复制和排序操作,提高Shuffle的性能。 SortShuffleWriter是一种常用的策略,它会将数据写入磁盘,并使用外部排序算法对数据进行排序。 UnsafeSortShuffleWriter是一种更高效的策略,它使用了内存进行排序,减少了磁盘IO的开销。 下面是一个示例代码,演示了SparkShuffle过程: ```scala val inputRDD = sc.parallelize(List(("apple", 1), ("banana", 2), ("apple", 3), ("banana", 4))) val shuffledRDD = inputRDD.groupByKey() val resultRDD = shuffledRDD.mapValues(_.sum()) resultRDD.collect().foreach(println) ``` 这段代码首先创建了一个输入RDD,其中包含了一些键值对数据。然后使用groupByKey()函数对数据进行分组,生成一个ShuffledRDD。最后使用mapValues()函数对每个分组进行求和操作,得到最终结果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值