spark虽然是基于内存计算的,但是它也会产生shuffle
首先我们需要知道,Spark任务会根据RDD之间的依赖关系,形成一个DAG有向无环图,DAG会提交给DAGScheduler,DAGScheduler会把DAG划分相互依赖的多个stage,划分stage的依据就是RDD之间的宽窄依赖。遇到宽依赖就划分stage,每个stage包含一个或多个task任务。那我们可以想一下,如果上一个RDD上的patition不在同一个节点上,那么它是如何进行聚合的呢?这时候就产生了shuffle。
spark shuffle
shuffle write:主要就是在一个stage结束计算之后,为了下一个stage可以执行shuffle类的算子(比如reduceByKey,groupByKey),而将每个task处理的数据按key进行“分区”。所谓“分区”,就是对相同的key执行hash算法,从而将相同key都写入同一个磁盘文件中,而每一个磁盘文件都只属于reduce端的stage的一个task。在将数据写入磁盘之前,会先将数据写入内存缓冲中,当内存缓冲填满之后,才会溢写到磁盘文件中去。
shuffle read:通常就是一个stage刚开始时要做的事情。此时该stage的每一个task就需要将上一个stage的计算结果中的所有相同key,从各个节点上通过网络都拉取到自己所在的节点上,然后进行key的聚合或连接等操作。由于shuffle write的过程中,task给Reduce端的stage的每个task都创建了一个磁盘文件,因此shuffle read的过程中,每个task只要从上游stage的所有task所在节点上,拉取属于自己的那一个磁盘文件即可。
spark shuffle的发展史,spark1.2之前没有SortShuffle,spark1.2-spark1.6之间是有HashShuffle和SortShuffle的,spark2.0之后就只有SortShuffle了,HashShuffle被移除了
hashshuffle
1.普通机制
执行流程:
1)map task阶段每个task根据分区器对key取哈希值,然后对reduce task取模,将不同的结果写到不同的buffle中
2)buffle的默认大小是32kb,buffer起到的是缓存作用,缓存能够加速写磁盘,提高计算的效率
3)每个buffle中的数据最后会映射为一个个的磁盘小文件
4)reduce task根据不同的结果拉取对应的磁盘小文件,拉取的时候也是先拉取到buffer中,每次拉取的大小不能超过buffer缓冲大小(默认48kb),然后通过内存中的一个map来进行聚合,直到拉取完所有数据
缺点:
产生的磁盘小文件过多,会产生num(map)*num(reduce)个小文件
2.合并机制
执行流程:
1)map task阶段每个task根据分区器对key取哈希值,然后对reduce task取模,将不同的结果写到不同的buffer中。一个Executor上有多少个CPU core,就可以并行执行多少个task。不同的task,会把相同key的数据放到同一个buffer中,并将数据写入对应的磁盘文件内。
2)buffle的默认大小是32kb,buffer起到的是缓存作用,缓存能够加速写磁盘,提高计算的效率
3)每个buffle中的数据最后会映射为一个个的磁盘小文件
4)reduce task根据不同的结果拉取对应的磁盘小文件,拉取的时候也是先拉取到buffer中,每次拉取的大小不能buffer缓冲大小,然后通过内存中的一个map来进行聚合,知道拉取完所有数据
注意:
1)产生的磁盘小文件的数量=num(core)*num(recude)
2)启动HashShuffle的合并机制ConsolidatedShuffle的配置:
spark.shuffle.consolidateFiles=true
3)如果 Reducer 端的并行任务或者是数据分片过多的话则 Core * Reducer Task 依旧过大,也会产生很多小文件。
sort shuffle
1.普通机制
执行流程:
1)map task会将计算结果根据不同的算子写入到不同的内存数据结构中,reduceByKey这种聚合类的shuffle算子,那么会选用Map数据结构,一边通过Map进行聚合,一边写入内存;如果是join这种普通的shuffle算子,那么会选用Array数据结构,直接写入内存。内存数据结构大小默认为5MB
2)每写一条数据进入内存数据结构之后,就会判断一下,是否达到了某个临界阈值。如果达到临界阈值的话,那么就会尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。在shuffle的时候会有一个定时器,定时器会检查内存数据结构的大小,如果内存数据结构空间不够,那么会申请额外的内存,申请的大小满足如下公式:applyMemory=nowMenory*2-oldMemory。申请不成功就会溢写,申请成功则继续存放数据。
3)溢写到磁盘之前会。根据key进行排序分区
4)然后开始溢写,溢写是以batch的形式分批进行,每次10000条数据。先将数据放到bufferOutPutStream中,BufferedOutputStream是Java的缓冲输出流,首先会将数据缓冲在内存中,当内存缓冲满溢之后再一次写入磁盘文件中,这样可以减少磁盘IO次数,提升性能
5)溢写到磁盘之后是一个个的磁盘小文件,然后对这些小文件进行marge合并成一个大文件。同时生成一个索引文件,其中标识了下游各个task的数据在文件中的start offset与end offset
6)reduce task拉取数据的时候,会先去解析索引文件,然后去拉取对应的数据
注意:
1)产生的文件数量=2*num(map task)
bypass机制
流程与不同机制类似,只是少了排序操作,启用该机制的最大好处在于,shuffle write过程中,不需要进行数据的排序操作,也就节省掉了这部分的性能开销。
触发条件:
.产生的磁盘小文件为:2*M(map task的个数)
shuffle 寻址
1)maptask 执行完毕后会把task执行结果和磁盘的小文件地址封装到MapStatus对象中,通过Executor中的MapOutputTrackerWorker对象向Driver中的MapOutputTrackerMaster汇报。MapOutputTracker使用来管理小文件地址的,。
2)reduce task拉取数据的时候会先在本地的MapOutputTackerWorker中的获取,如果没有则向Driver的MapOutputTackerWorker请求小文件地址
3)Driver会返回请求的磁盘小文件地址
4)获取到磁盘小文件地址后,会通过BlockManagerWorker中的ConnectionManager连接到对应的BlockManager
5)然后通过BlockManagerWorker中的BlockTransferService传输数据,默认启动5个task拉取数据,拉取数据量不可以超过48MB
reduce 中OOM如何处理?