Spark的Shuffle(一) - SortShuffleWriter

上一篇“shuffle的一些概念”中提到了三种shuffle的方式,此处先来分析下SortShuffleWriter,结合代码一起调试下看看它内部到底是如何运行的。

选择带有聚合的算子调试就行了,例如对一个pairRDD进行reduceByKey操作,然后就可以跳到对应的源码里面了,可以看出reduceByKey算子使用的是确实是SortShuffleWriter:

直接跑到运行Task的代码中看它到低是如何进行Shuffle的。

Ps:这里我有个疑问,我代码里面并没有选择Kryo的序列化方式,为什么这里显示的是kryo?默认的不应该是JavaSerializer么?

 

SortShuffleWriter流程如下所示

 

 

Shuffle相关的一些配置项:

这两个用于控制shuffle时落盘的频率:

spark.shuffle.file.buffer: 默认32K

spark.shuffle.spill.batchSize: 默认10000

 

shuffle时临时文件相关配置:

spark.shuffle.spill.initialMemoryThreshold: shuffle时初始的缓冲区大小,扩容时会扩容一辈,申请不到一倍的内存就会生成临时shuffle文件,默认5*1024*1024(5M)

spark.shuffle.spill.numElementsForceSpillThreshold: shuffle时,缓冲区的数量达到这量就会生成临时shuffle文件,默认Long.MaxValue

 

Shuffle主要过程个人总结为如下5步

接下来对上面这些流程进行一步步的详细分析。

 

1.创建用于对数据进行排序、聚合、写缓存等操作ExternalSort

比较重要的参数是下面4个:

private val fileBufferSize = conf.getSizeAsKb("spark.shuffle.file.buffer", "32k").toInt * 1024

private val serializerBatchSize = conf.getLong("spark.shuffle.spill.batchSize", 10000)

@volatile private var map = new PartitionedAppendOnlyMap[K, C]

@volatile private var buffer = new PartitionedPairBuffer[K, C]

由于Shuffle将数据写到磁盘之前,会先写到缓冲区中,满了之后才会溢出写到磁盘。写磁盘的时候默认32K或者10000条数据写一次磁盘。

后面两个数据结构就是所谓的缓冲区,map适合有Aggregator场景,buffer则不是。

 

2.接着调用ExternalSort的insertAll()方法将数据写入到缓冲区。如果数据量太多,会flush()多个临时的shuffle文件

map.changeValue(…)是根据用于定义的函数,更新缓冲区中的值,缓冲区里面长这样:

接下来的maySpillCollection()方法内部会调用mayBeSpill()判断是否需要生成临时的Shuffle文件(所以干嘛要分成两个方法呢?一个方法不就好了么?!):

Sparkenv.get.conf.getLong("spark.shuffle.spill.initialMemoryThreshold", 5 * 1024 * 1024)

缓冲区初始化大小,可以扩大缓冲区的话,会尝试申请一倍的内存Sparkenv.get.conf.getLong("spark.shuffle.spill.numElementsForceSpillThreshold", Long.MaxValue)

默认达到Long类型的最大长度才会刷新缓冲区,我早就设置成10了,哈哈哈,就是为了看它是如何生成临时文件的。

 

2.1分析下如何生成临时Shuffle文件:

mayBeSpill()就是根据上面定义的两个参数,将内存中的数据刷写到磁盘上生成临时的Shuffle文件:

注意:写临时文件完成之后会释放这个Task所占用的内存空间,到它最初所占有的内存大小,就是那个默认的5M:

写临时文件的方法是spill(collection),它的核心是生成一个inMemoryIterator迭代器,然后使用这个迭代器将数据刷到磁盘上,并记录下这个临时shuffle文件。

 

2.1.1获取内存数据迭代器:

因为这里reduceByKeyy使用的是PartitionedAppendOnlyMap结构,所以这里就看下它的迭代器:

这个迭代器的key是一个tuple(partition ID,K),然后还有个keyComparator,估计是还要按照key排序一把,点进去看下。

首先是生成一个partitionKeyComparator,先比较partition,在比较key

这个comparator用于对AppendOnlyMap中的元素进行排序

最后返回的这个迭代器迭代的时候,会按照partition的顺序返回

 

2.1.2将内存中的数据刷写到磁盘上生成临时Shuffle文件

spillMemoryIteratorToDisk(inMemoryIterator:WritablePartitionedIterator)通过上面inMemoryIterator的将数据刷写到磁盘生成临时文件,临时文件的目录和文件名称命名如下:

 

 

2.1.3可以看到最终会生成很多临时的temp_shuffle文件

至此,写内存的过程就结束了。注意如果数据量少的话,就不会有这些临时的Shuffle文件,而是全部在内存里面。

 

3.接着将map端缓存的文件写入到磁盘中,最终只会生成一个Shuffle文件,临时Shuffle文件会被合并到一起。

writePartitionedFile()方法就是往最终的那一个Shuffle文件中写数据,内部流程如下所示:

this.partitionedItreator这个迭代器是按照下面的方式获取的:

最终会为每个partition生成一个迭代器,并且每个partition中的元素已经完成了agg和order操作(如果定义过这两个操作的话)。

 

4.生成索引文件

这个方法看了下,好像没什么好说的,就是写了一个IndexFile,这个文件记录的是每个partition在那个正式的Shuffle文件的偏移量,以便reduce任务拉取时使用。

 

5.返回MapStatus

MapStatus会返回给Driver,这样Driver就可以知道每个Executor上的Shuffle文件的位置了。

如果MapStatus太大的话会对它进行压缩。

 

 

 

参考:

《Spark内核设计的艺术》

https://www.cnblogs.com/20125126chen/p/4534190.html(写文件时,文件系统如何定位)

http://www.importnew.com/14111.html(写文件时,Java I/O底层是如何工作的)

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值