目录
- Spark的shuffle分析
- Shuffle简介
- 宽窄依赖
- Stage
- stage切割规则
- stage计算模式
- stage并行度
- Spark Shuffle
- shuffle Write
- shuffle Read
- HashShuffle
- 普通机制
- 合并机制
- SortShuffle
- 普通机制
- bypass机制
- shuffle 寻址
- Spark内存管理
- 静态内存管理
- 统一内存管理
- reduce中OOM如何处理?
- shuffle调优
- shuffle调优配置如何使用?
- shuffle调优
- spark.shuffle.file.buffer
- spark.reducer.maxSizeInFlight
- spark.shuffle.io.maxRetries
- spark.shuffle.io.retryWait
- spark.shuffle.memoryFraction
- spark.shuffle.manager
- spark.shuffle.sort.bypassMergeThreshold
- spark.shuffle.consolidateFiles
Spark的shuffle分析
Shuffle简介
Shuffle描述着数据从map task输出到reduce task输入的这段过程。shuffle是连接Map和Reduce之间的桥梁,Map的输出要用到Reduce中必须经过shuffle这个环节,shuffle的性能高低直接影响了整个程序的性能和吞吐量。因为在分布式情况下,reduce task需要跨节点去拉取其它节点上的map task结果。这一过程将会产生网络资源消耗和内存,磁盘IO的消耗。通常shuffle分为两部分:Map阶段的数据准备和Reduce阶段的数据拷贝处理。一般将在map端的Shuffle称之为Shuffle Write,在Reduce端的Shuffle称之为Shuffle Read
宽窄依赖
RDD之间有一系列的依赖关系,依赖关系又分为窄依赖和宽依赖
窄依赖(不会有shuffle):父RDD与子RDD之间关系是一对一的。或者父RDD与子RDD之间的关系是多对一的。
宽依赖(会有shuffle):父RDD与子RDD之间关系是一对多
其中rdd3--->rdd4是多对一的关系,就是一个宽依赖,会产生shuffle。如果没有shuffle过程,如何能将多个分区中相同的key进行统计呢?
Stage
Spark任务会根据RDD之间的依赖关系,形成一个DAG有向无环图,DAG会提交给DAGScheduler,DAGScheduler会把DAG划分相互依赖的多个stage,划分stage的依据就是RDD之间的宽窄依赖。遇到宽依赖就划分stage,每个stage包含一个或多个task任务。然后将这些task以taskSet的形式提交给TaskScheduler运行。
stage是由一组并行的task组成
stage切割规则
从后往前,遇到宽依赖就切割stage
stage计算模式
pipeline管道计算模式,pipeline只是一种计算思想。也就是数据是一条一条往下执行的
测试pipeline计算模式
val rdd1 = sc.parallelize(Array("zhangsan","lisi","wangwu"))
/**
* 我们发现其输出的顺序为
* map========= zhangsan
* filter===== zhangsan~
* map========= lisi
* filter===== lisi~
* map========= wangwu
* filter===== wangwu~
* 所以是执行一条map然后执行一条filter
*/
val rdd2 = rdd1.map(name => {
println("map========= " + name)
name + "~"
})
val rdd3 = rdd2.filter(name => {
println("filter===== " +name)
true
})
rdd3.collect()
数据一致在管道里面什么时候会落地磁盘?
1)对RDD进行持久化
2)shuffle write的时候
stage并行度
stage的task并行度是由stage的最后一个RDD的分区数来决定的。
在spark中对于由shuffle的RDD算子,都提供了第二个参数来指定分区数
Spark Shuffle
如:reduceByKey会将上一个RDD中的每一个key对应的所有value聚合成一个value,然后生成一个新的value,元素类型是<key,value>对的形式,这样每一个key对应一个聚合起来的value。
问题:聚合之前,每一个key对应的value不一定都是在一个partition中,也不可能在同一个节点上,RDD的partition极有可能分布在各个节点上。
shuffle Write
上一个stage的每个map task就必须保证将自己处理的当前分区的数据相同的key写入一个分区文件中。
shuffle Read
reduce task就会从上一个stage的所有task所在的机器上寻找属于己的那些分区文件,这样就可以保证每一个key所对应的value都会汇聚到同一个节点上去处理和聚合。
Spark由两种Shuffle类型,HashShuffle和SortShuffle
Spark1.2之前是HashShuffle默认的分区器是HashPartitioner,
Spark1.2引入SortShuffle默认的分区器是RangePartitioner
HashShuffle
普通机制
执行流程
1)每一个map task将不同结果写入到不同的buffer中,每个buffer的大小为32K,buffer起到数据缓存的作用
2)每个buffer文件最后对应一个磁盘小文件
3)reduce task来拉取对应的磁盘小文件
总结
map task的计算结果会根据分区器(默认是hashPartitioner)来决定写入到哪一个磁盘小文件中取,Reduce task会去Map端拉取相应的磁盘小文件
产生的磁盘小文件个数
M (map task的个数)* R(reduce task的个数)
存在的问题
产生的磁盘文件过多,会导致的问题
a)在Shuffle Write过程中会产生很多磁盘小文件的对象
b)在Shuffle Read过程中会产生很多读取磁盘小文件的对象。
c)在JVM堆内存中对象过多会造成频繁的gc,gc还无法解决运行所需要的内存 的话,就会OOM。
d)在数据传输过程中会有频繁的网络通信,频繁的网络通信出现通信故障的可能性大大增加,一旦网络通信出现了故障会导致shuffle file cannot find 由于这个错误导致的task失败,TaskScheduler不负责重试,由DAGScheduler负责重试Stage。
合并机制
产生的磁盘小文件个数 C(core的个数)* R (reduce task的个数)
SortShuffle
普通机制
执行流程
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)reduce task去map端拉取数据的时候,首先解析索引文件,根据索引文件再去拉取对应的数据。
bypass机制
总结
1)bypass运行条件
shuffle reduce task的数量小于spark.shuffle.sort.bypassMergeThreshold的参数值。这个值默认是200。
shuffle 寻址
shuffle文件寻址liuc
1)当map task执行完成后,会将task的执行情况和磁盘小文件的地址封装到MapStatus对象中,通过MapOutputTrackerWorker对象向Driver中MapOutputTrackerMaster汇报
2)在所有的map task执行完毕后,Driver中就掌握了所有的磁盘小文件的地址
3)在reduce task执行之前,会通过Executor中MapOutputTrackerWorker向Driver端的MapOutputTrackerMaster获取磁盘小文件的地址
4)获取到磁盘小文件的地址后,会通过BlockManager中ConnectionManager连接数据所在节点上的ConnectionManager,然后通过BlockTransferService进行数据传输
5)BlockTransferService默认启动5个task去节点拉取数据。默认情况下,5个task拉取数据量不能超过48M。
Spark内存管理
Spark执行应用程序时,Spark集群会启动Driver和Executor两种JVM进程,Driver负责创建SparkContext上下文,提交任务,task的分发等。Executro负责task的计算任务,并将结果返回给Driver。同时需要为持久化的RDD提供存储。Driver的内存管理比较简单,而这里所讨论的spark内存管理是指Executor端的内存管理。
Spark内存管理分区静态内存管理和统一内存管理,spark1.6之前使用的是静态内存管理,spark1.6之后引入了统一内存管理。
静态内存管理中存储内存、执行内存和其他内存的大小在spark应用程序运行期间均为固定的,但用户可以在应用程序启动前进行配置
统一内存管理与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以互相借用对方的空间。
Spark1.6以上版本默认使用的是统一内存管理,可以通过参数spark.memory.useLegacyMode 设置为true(默认为false)使用静态内存管理。
静态内存管理
统一内存管理
reduce中OOM如何处理?
1)减少每次拉取的数量
2)提高shuffle聚合的内存比例
3)提高Executor的总内存
shuffle调优
shuffle调优配置如何使用?
1)在代码中,不推荐使用,硬编码。
new SparkConf().set(“spark.shuffle.file.buffer”,”64”)
2)在提交spark任务的时候,推荐使用。
spark-submit --conf spark.shuffle.file.buffer=64 –conf ….
3)在conf下的spark-default.conf配置文件中,不推荐,因为是写死后所有应用程序都要用。
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|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%。