一、why shuffle,shuffle的异常情况
1.以下是shuffle过程中必须明确的几个前置问题:
{1}shuffle就是数据重分布,也就是数据分门别类。
既然要分类,那肯定就要有个key,所以无论在mr还是spark,只有kv的数据才能shuffle
{2}磁盘IO和网络传输很耗费性能。
所以,为了减少这2种操作,要用批处理来替代频繁读写,所以我们会看到,无论spark还是mr,在溢写、网络传输的时候,都是批处理,也就是积攒一批处理一起操作
spark的宽依赖中,为啥后面stage要等前面stage完全执行完才开始执行/为啥要溢写啊,直接发到对应的下游分区不就行了
- 设计理念:理论上,这样完全可以实现,自己实现一个taskschedulerImple即可,但spark面对的数据源是有界的,spark是批处理的思想,都是以文件为单位读取,这样其实批量处理比一条条处理效率更高,也容易优化、减少网络和磁盘IO。
flink已经这样实现。上一个stage完成,才计算下一个stage。这样工程实现最简单,你也可以参考flink来实现,代码会复杂,而且调度更复杂,不一定有现在的设计计算的快 - 资源,spark On yarn的Executor是用到了才启动,上游在跑,下游在数据没有完全准备好就跑起来,占用资源,也没提高处理的速度
{3}分布式+并发
大数据使用分而治之的方法论,的确能加快速度,但也会提高复杂度。
如果是单节点,那简单了,直接执行一个groupBy的操作,不会有啥问题。但如果是分布式场景,那复杂程度会上升很多。
最简单的思路,每个map端,把数据分成reduce份,然后再发送到reduce端聚合。
因为并发,所以每个节点都会有多个进程,假如一个进程用一个core,所以单节点文件个数cm
因为分布式,所以cm*r
由此又产生了数据倾斜问题、热点问题、计算位置、等待时间、重试次数、血缘、容错等等一系列分布式独有的问题。
2.为啥分布式计算需要shuffle
业务逻辑(换维度)或并行度改变导致分区规则发生变化,数据需要重新分布,重新分区。
- 业务上需要更换维度,比如wordcount中,一开始是按hdfs上块为维度分的区,之后按单词为维度分区。如果不更换维度,即使发生了数据的分布,比如3个分区变为2个(意味着一个task要计算2个分区的数据),或者3个分区变为4个(一个task没事干),也不会shuffle。
- 开发上计算需要更改并行度,比如repartition,底层也是修改了分区的逻辑(hash的因子),也相当于改了维度。
3.shuffle的异常情况
如果是单机对单机,那就没有shuffle问题。分布式场景下,多分区对多分区。才会有shuffle问题,就意味着:
- 1.要按一定的规则,也就是需要用partitioner分区器进行计算,而且是每一条都要计算,效率受影响
- 2.子RDD的每个分区,都部分依赖父RDD的所有分区,如果父RDD的分区不落盘或者准确的说不先计算完,就会有重复计算的问题。如果shuffle write的数据都放在内存中:数据量就太大,因为需要全部计算完毕,相当于所有的数据都放在内存中,而不是算一点清一点,内存有可能放不下,可能会oom,毕竟大数据场景都是pb、tb级别的。而落盘就意味着IO,所以shuffle是app的性能瓶颈
- 3.一些场景下shuffle往往还伴随着排序(mr是必排序的,因为mr是通过排序来把发往各个reducer的数据分离)
- 4.shuffle是数据倾斜的直接原因,出现数据倾斜,那一定是shuffle引起的
- 6.shuffle会有网络IO的问题,有等待时间的概念,如果超过等待时间,就要执行容错机制。
- 7.因为需要在mapper端先分好,所以会有小文件的问题
一个分区对应一个task,假如mapper端有A个分区,reducer端有B个分区,则mapper端有A * B个文件。对于动不动就几千个分区的spark计算,显然不合适。 - 8.预处理,某些场景在mapper端进行排序、预聚合,会提高效率
- 9.文件寻址,其实就是shuffle的底层实现
如何解决:优化
- 减少shuffle
- 减少小文件数量
- 增大缓存和shuffle内存
总结
shuffle目前得到的设计原则:
上下游分开,上游计算完再计算下游
要落盘,磁盘IO和网络IO的过程使用批处理
二、spark的shuffle解决方案
Spark
中有两种Shuffle
类型,HashShuffle
和SortShuffle
,Spark1.2
之前是HashShuffle
,Spark1.2
引入SortShuffle
。spark2.0
就只有sortshuffle
1.HashShuffle
有2种机制,合并机制主要是为了减少小文件数量
普通机制
执行流程:
1)每一个map task
将不同结果写到不同的buffer
中,每个buffer
的大小为32K
。buffer
起到数据缓存的作用,实现边写边溢。
2)每个buffer
文件最后对应一个磁盘小文件。
3)reduce task
来拉取对应的磁盘小文件。
总结:
1)map task
的计算结果会根据分区器(默认是hashPartitioner
)来决定写入到哪一个磁盘小文件中去。ReduceTask
会去Map
端拉取相应的磁盘小文件。
2)产生的磁盘小文件的个数:
M(map task的个数)*R(reduce task的个数)
问题:
产生的磁盘小文件过多,会导致以下问题:
-
在
Shuffle Write
过程中会产生很多写磁盘小文件的对象。 -
在
Shuffle Read
过程中会产生很多读取磁盘小文件的对象。 -
在
JVM
堆内存中对象过多会造成频繁的gc
,gc
还无法解决运行所需要的内存的话,就会OOM
。 -
在数据传输过程中会有频繁的网络通信,频繁的网络通信出现通信故障的可能性大大增加,一旦网络通信出现了故障会导致
shuffle file cannot find
由于这个错误导致的task
失败,TaskScheduler
不负责重试,由DAGScheduler
负责重试Stage
。
合并机制
总结
产生磁盘小文件的个数:C(core的个数)*R(reduce的个数)
2.SortShuffle
bypass相比普通机制,就是少了排序,也就是预聚合,当reducer比较少的时候,网络io压力不大,就取消预聚合。
普通机制
执行流程
-
map task
的计算结果会写入到一个内存数据结构里面,内存数据结构默认是5M
。 -
在
shuffle
的时候会有一个定时器,不定期的去估算这个内存结构的大小,当内存结构中的数据超过5M
时,比如现在内存结构中的数据为5.01M
,那么他会申请5.01*2-5=5.02M
内存给内存数据结构。 -
如果申请成功不会进行溢写,如果申请不成功,这时候会发生溢写磁盘。
-
在溢写之前内存结构中的数据会进行排序分区
-
然后开始溢写磁盘,写磁盘是以
batch
的形式去写(批量),一个batch
是1万条数据。 -
map task
执行完成后,会将这些磁盘小文件
合并成一个大的磁盘文件,同时生成一个索引文件
。 -
reduce task
去map
端拉取数据的时候,首先解析索引文件,根据索引文件再去拉取对应的数据。
总结
一个task只有一个一个索引文件和数据文件
产生磁盘小文件的个数: 2*M(map task的个数)
bypass机制
总结
bypass
运行机制的触发条件如下:
shuffle reduce task
的数量小于spark.shuffle.sort.bypassMergeThreshold
的参数值。这个值默认是200
。
不需要进行map
端的预聚合,比如groupBykey
,join
。
产生的磁盘小文件为:2*M(map task的个数)
三、文件寻址
shuffle需要文件传输,那么mapper端的文件溢写到磁盘后,reducer端怎么知道呢,这需要进行管理,spark是怎么解决这个问题的呢?
MapOutputTracker和BlockManager都是Executor对象的属性。
文件的位置,MapOutputTracker
MapOutputTracker
是Spark
架构中的一个模块,用来管理shuffle过程中需要传输的数据文件。是一个主从架构。
MapOutputTrackerMaster
是主对象,存在于Driver
中。MapOutputTrackerWorker
是从对象,存在于Excutor
中。
Executor的mapper生成文件后,MapOutputTrackerWorker存在mapstatus对象中,然后告诉driver的MapOutputTrackerMaster,然后reducer再到driver的MapOutputTrackerMaster中获取数据文件地址,然后拉取。如果是单机环境,此时mapper的Executor和reducer的Executor在一个节点上,此时就不需要driver中介了,直接从本地的MapOutputTrackerWorker中拿。
Shuffle
文件寻址流程
- 当
map task
执行完成后,会将task
的执行情况和磁盘小文件的地址封装到MapStatus
对象中,通过MapOutputTrackerWorker
对象向Driver
中的MapOutputTrackerMaster
汇报。在所有的map task
执行完毕后,Driver
中就掌握了所有的磁盘小文件的地址。 - 在
reduce task
执行之前,会通过Excutor
中MapOutPutTrackerWorker
向Driver
端的MapOutputTrackerMaster
获取磁盘小文件的地址。 - 获取到磁盘小文件的地址后,会通过
BlockManager
中的ConnectionManager
连接数据所在节点上的ConnectionManager
,然后通过BlockTransferService
进行数据的传输。 BlockTransferService
默认启动5
个task
去节点拉取数据。一次拉取的数据量根据配置,默认,5
个task
一次拉取数据量不能超过48M
,防止reducer端内存不够。这里的48M相当于buffer,写满就清。
文件的存储,BlockManager
四、内存管理
内存管理和shuffle息息相关,是shuffle调优的重要内容
Spark
执行应用程序时,Spark
集群会启动Driver
和Executor
两种JVM
进程,Driver
负责创建SparkContext
上下文,提交任务,task
的分发等。Executor
负责task
的计算任务,并将结果返回给Driver
。同时需要为需要持久化的RDD
提供储存。Driver
端的内存管理比较简单,这里所说的Spark
内存管理针对Executor
端的内存管理。
Spark
内存管理分为静态内存管理和统一内存管理,Spark1.6
之前(不包含)使用的是静态内存管理,Spark1.6
之后引入了统一内存管理。
静态内存管理中存储内存、执行内存和其他内存的大小在 Spark
应用程序运行期间均为固定的,但用户可以应用程序启动前进行配置。
统一内存管理与静态内存管理的区别在于储存内存和执行内存共享同一块空间,可以互相借用对方的空间。
Spark1.6
及1.6
版本之后的版本默认使用的是统一内存管理。要想使用静态内存可以通过参数spark.memory.useLegacyMode
,设置为true
(默认为false
)使用静态内存管理。
内存分四部分,存、shuffle、算、预留。2.0以后,将默认预留设置为300M,存 + shuffle为60,算为40,然后存和shuffle动态借用。
五、调优
SparkShuffle调优配置项如何使用?
- 在代码中,不推荐使用,硬编码。
new SparkConf().set(“spark.shuffle.file.buffer”,”64”)
- 在提交spark任务的时候,推荐使用。
spark-submit --conf spark.shuffle.file.buffer=64 –-conf ….
- 在
conf
下的spark-default.conf
配置文件中,不推荐,因为是写死后所有应用程序都要用。
调优
参数
结合流程图,看看哪个阶段可以调优
-
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|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
默认值:200
参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作,而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。
调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuffle read task的数量。那么此时就会自动启用bypass机制,map-side就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此shuffle write性能有待提高。 -
spark.shuffle.consolidateFiles
默认值:false
参数说明:如果使用HashShuffleManager,该参数有效。如果设置为true,那么就会开启consolidate机制,会大幅度合并shuffle write的输出文件,对于shuffle read task数量特别多的情况下,这种方法可以极大地减少磁盘IO开销,提升性能。
调优建议:如果的确不需要SortShuffleManager的排序机制,那么除了使用bypass机制,还可以尝试将spark.shffle.manager参数手动指定为hash,使用HashShuffleManager,同时开启consolidate机制。在实践中尝试过,发现其性能比开启了bypass机制的SortShuffleManager要高出10%~30%。
https://www.iteblog.com/archives/1671.html
https://www.jianshu.com/p/391d42665a30