Hadoop中MapReduce的Shuffle过程不仅会产生大量的网络传输开销,也会带来大量的磁盘I/O开销,但Spark在Shuffle过程有时也会如此。
一、MapReduce的Shuffle操作
在Hadoop MapReduce框架中,Shuffle是连接Map和Reduce之间的桥梁。所谓Shuffle,是指对Map输出结果进行分区、排序、合并等处理并交给Reduce的过程。
(1)在Map端的Shuffle过程:
Map的输出结果首先被写入缓存,当缓存满时,就启动溢写操作,把缓存中的数据写入磁盘文件,并清空缓存。当启动溢写操作时,首先需要把缓存中的数据进行分区,不同分区的数据发送给不同的Reduce任务进行处理,然后对每个分区的数据进行排序(Sort)和合并(Combine),之后再写入磁盘文件。每次溢写操作会生成一个新的磁盘文件,随着Map任务的执行,磁盘中就会生成多个溢写文件,然后通知相应的Reduce任务来领取属于自己处理的那个分区数据。
(2)在Reduce端的Shuffle过程。
Reduce任务从Map端的不同Map机器领回属于自己处理的那部分数据,然后,对数据进行归并(Merge)后交给Reduce处理。
二、Spark的Shuffle操作
![](https://i-blog.csdnimg.cn/blog_migrate/aaed720e461bad777d4aef2771f69b05.png)
Spark作为MapReduce框架的一种改进,自然也实现了Shuffle的逻辑。
(1)在Map端的shuffle写入方面:
每一个Map任务会根据Reduce任务的数量创建出相应的桶,因此桶的数量是m x r,其中m是Map任务的个数,r是Reduce任务的个数。Map任务产生的结果会根据设置的分区算法填充到每个桶中去。分区算法可以自定义,也可以采用系统默认的算法;默认的算法是根据每个键值对(key,value)的key,把键值对哈希到不同的桶中去。
当Reduce任务启动时,它会根据自己任务的id和所依赖的Map任务的id,从远端或是本地取得相应的桶,作为Reduce任务的输入进行处理。
注意:
在最新的Spark版本中,采用了多个桶写入一个文件的方式。每个Map任务不会为每个Reduce任务单独生成一个文件,而是把每个Map任务所有的输出数据只写到一个文件中。
Shuffle过程中每个Map任务会产生两个文件,即数据文件和索引文件,其中,数据文件是存储当前Map任务的输出结果,而索引文件中则存储了数据文件中的数据的分区信息。下一个阶段的Reduce任务就是根据索引文件来获取属于自己处理的那个分区的数据。
(2)Reduce端的shuffle读取方面:
与MapReduce不同的是,Spark并不在Reduce端做归并和排序,而是采用了成为Aggregator的机制。Aggregator本质上是一个HashMap,里面每个元素是<K,V>形式。以词频统计为例,它将从Map端拉取到的每一个(key,value),更新或是插入到HashMap中,若在HashMap中没有查到这个key,则把这个(key,value)插入其中,若查到这个key,则把Value的值累加到V上去。这样就不需要预先把所有的(key,value)进行归并和排序,而是来一个处理一个,避免了外部排序这一步骤。但同时需要注意的是,Reduce任务所拥有的内存,必须足以存放属于自己处理的所有key和value值吗,否则就会产生内存溢出问题。
因此, Spark文档中建议用户涉及到这类操作的时候尽量增加分区的数量,也就是增加Map和 Reduce任务的数量。增加Map和 Reduce任务的数量虽然可以减小分区的大小,使得内存可以容纳这个分区,但是,在 Shuffe写入环节,桶的数量是由Map和 Reduce任务的数量决定的,任务越多,桶的数量就越多,就需要更多的缓冲区(Buffer),带来更多的内存消耗。因此,在内存使用方面,我们会陷入一个两难的境地,一方面,为了减少内存的使用,需要采取增加Map和 Reduce任务数量的策略,另一方面,Map和 Reduce任务数量的增多,又会带来内存开销更大的问题。最终,为了减少内存的使用,只能将 Aggregator操作从内存移到磁盘上进行。也就是说,尽管 Spark经常被称为“基于内存的分布式计算框架”,但是,它的 Shuffle过程依然需要把数据写入磁盘。