MapReduceshuffle过程详解

一、shuffle概念

	shuffle,洗牌的意思。在MapReduce中,shuffle将map端的无规则输出按指定的规则处理为
具有一定规则的数据后,reduce端再接收处理。
	shuffle的工作阶段在map和reduce两端,即Map Shuffle和Reduce shuffle。
	shuffle之前,MapReduce会对要处理的数据进行分片(split)操作,为每一个分片分配一个
MapTask任务。接下来map()函数会对每一个分片中的每一行数据进行处理得到键值对(key,value),
其中key为偏移量,value为一行的内容。此时得到的键值对又叫做“中间结果”。  
	然后便进入shuffle阶段,可以看出shuffle阶段的作用是处理“中间结果”。
补充Partition

MapReduce提供默认的分区类(HashPartitioner),其核心代码如下:

public class HashPartitioner<K, V> extends Partitioner<K, V> {

  /** Use {@link Object#hashCode()} to partition. */
  public int getPartition(K key, V value,
                          int numReduceTasks) {
    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
  }

}

	getPartition()方法有三个参数,前两个指的是mapper任务输出的键值对,而第三个参数指的是  
设置的reduce任务的数量,默认值为1。因为任何整数与1相除的余数肯定是0。也就是说默认的
getPartition()方法的返回值总是0,即Mapper任务的输出默认总是给同一个Reducer任务,最终只能  
输出到一个文件中。如果想要让mapper输出的结果给多个reducer处理,那么需要写一个类,让其继承  
Partitioner类,并重写getPartition()方法,让其针对不同情况返回不同数值即可。并在最后通过
job设置指定分区类和reducer任务数量即可。

下面具体解释shuffle。

先放图参考:
21
22


二、 Map Shuffle具体过程

  • 输出到环形内存缓冲区
  • 执行溢写 过程:分区partition —>排序sort—>合并combiner—>溢写
  • 归并merge

具体流程及解释如下:


1. Collect阶段

输出到环形缓冲区

	每个Map任务不断地以<key, value>对的形式把数据输出到在内存中构造的一个环形数据结构中。  
使用环形数据结构是为了更有效地使用内存空间,在内存中放置尽可能多的数据。这个数据结构其实 
就是个字节数组,叫Kvbuffer,名如其义,但是这里面不光放置了<key, value>数据,还放置了一些
索引数据,给放置索引数据的区域起了一个Kvmeta的别名,在Kvbuffer的一块区域上穿了一个
IntBuffer(字节序采用的是平台自身的字节序)的马甲。<key, value>数据区域和索引数据区域在
Kvbuffer中是相邻不重叠的两个区域,用一个分界点来划分两者,分界点不是亘古不变的,而是每次
Spill之后都会更新一次。初始的分界点是0,<key, value>数据的存储方向是向上增长,索引数据的
存储方向是向下增长,如图所示:

23


	Kvbuffer的存放指针bufindex是一直闷着头地向上增长,比如bufindex初始值为0,一个  
Int型的key写完之后,bufindex增长为4,一个Int型的value写完之后,bufindex增长为8。

	索引是对<key, value>在kvbuffer中的索引,是个四元组,包括:value的起始位置、key
的起始位置、partition值、value的长度,占用四个Int长度,Kvmeta的存放指针Kvindex每次
都是向下跳四个“格子”,然后再向上一个格子一个格子地填充四元组的数据。比如Kvindex初始位
置是-4,当第一个<key, value>写完之后,(Kvindex+0)的位置存放value的起始位置、
(Kvindex+1)的位置存放key的起始位置、(Kvindex+2)的位置存放partition的值、(Kvindex+3)
的位置存放value的长度,然后Kvindex跳到-8位置,等第二个<key, value>和索引写完之后,
Kvindex跳到-32位置。

	Kvbuffer的大小虽然可以通过参数设置,但是总共就那么大,<key, value>和索引不断地增加,
加着加着,Kvbuffer总有不够用的那天,那怎么办?把数据从内存刷到磁盘上再接着往内存写数据,
把Kvbuffer中的数据刷到磁盘上的过程就叫Spill,多么明了的叫法,内存中的数据满了就自动地
spill到具有更大空间的磁盘。

	关于Spill触发的条件,也就是Kvbuffer用到什么程度开始Spill,还是要讲究一下的。如果把
Kvbuffer用得死死得,一点缝都不剩的时候再开始Spill,那Map任务就需要等Spill完成腾出空间
之后才能继续写数据;如果Kvbuffer只是满到一定程度,比如80%的时候就开始Spill,那在Spill
的同时,Map任务还能继续写数据,如果Spill够快,Map可能都不需要为空闲空间而发愁。两利相衡
取其大,一般选择后者。

	Spill这个重要的过程是由Spill线程承担,Spill线程从Map任务接到“命令”之后就开始正式干活,
干的活叫SortAndSpill,原来不仅仅是Spill,在Spill之前还有个颇具争议性的Sort。

	在Map Task任务的业务处理方法map()中,最后一步通过context.write(key,value)输出Map 
Task的中间处理结果,在相关的方法中,会调用
Partitioner.getPartition(K2 key, V2 value, int numPartitions)方法获得输出的
key/value对应的分区号(分区号可以认为对应着一个要执行Reduce Task的节点),
然后将<key,value,partition>暂时保存在内存中的MapOutputBuffe内部的环形数据缓冲区,
该缓冲区的默认大小是100MB,可以通过参数io.sort.mb来调整其大小。

	当缓冲区中的数据使用率达到一定阀值后,触发一次Spill操作,将环形缓冲区中的部分数据
写到磁盘上,生成一个临时的Linux本地数据的spill文件;然后在缓冲区的使用率再次达到阀值后,
再次生成一个spill文件。直到数据处理完毕,在磁盘上会生成很多的临时文件。

2. Sort阶段
	当Spill触发后,SortAndSpill先把Kvbuffer中的数据按照partition值和key两个关键字  
升序排序,移动的只是索引数据,排序结果是Kvmeta中数据按照partition为单位聚集在一起,  
同一partition内的数据按照key有序。

24


3. Spill阶段

执行溢写 过程:分区partition —>排序sort—>合并combiner—>溢写

	当缓冲区的使用率达到一定阀值后,触发一次“溢写”操作,将环形缓冲区中的部分数据写到  
Linux的本地磁盘。需要特别注意的是,在将数据写磁盘之前,先要对要写磁盘的数据进行一次
排序操作,先按<key,value,partition>中的partition分区号排序,然后再按key排序,在必要  
的时候,比如说配置了Combiner并且当前系统的负载不是很高的情况下会将有相同partition分区
号和key的数据做聚合操作,还有如果设置过对中间数据做压缩的配置则还会做压缩操作。

	话分两端,在Spill线程如火如荼的进行SortAndSpill工作的同时,Map任务不会因此而停歇,  
而是一无既往地进行着数据输出。Map还是把数据写到kvbuffer中,那问题就来了:<key, value>  
只顾着闷头按照bufindex指针向上增长,kvmeta只顾着按照Kvindex向下增长,是保持指针起始位  
置不变继续跑呢,还是另谋它路?如果保持指针起始位置不变,很快bufindex和Kvindex就碰头了,
碰头之后再重新开始或者移动内存都比较麻烦,不可取。Map取kvbuffer中剩余空间的中间位置,用
这个位置设置为新的分界点,bufindex指针移动到这个分界点,Kvindex移动到这个分界点的-16位
置,然后两者就可以和谐地按照自己既定的轨迹放置数据了,当Spill完成,空间腾出之后,不需要  
做任何改动继续前进。分界点的转换如下图所示:

26

	Map任务总要把输出的数据写到磁盘上,即使输出数据量很小在内存中全部能装得下,  
在最后也会把数据刷到磁盘上。

4. 归并merge阶段

归并merge

	待Map Task任务的所有数据都处理完后,会对任务产生的所有中间数据文件做一次合并操作,  
以确保一个Map Task最终只生成一个中间数据文件。

	合并(Combiner)和归并(Merge)的区别: 
	两个键值对<“a”,1>和<“a”,1>,如果合并,会得到<“a”,2>,如果归并,会得到<“a”,<1,1>>

Reduce Shuffle具体过程

  • 复制copy
  • 归并merge
  • reduce

在Reduce端,shuffle主要分为复制Map输出、排序合并两个阶段。

1. copy阶段
	Reduce进程启动一些数据copy线程,通过HTTP方式请求MapTask所在的NodeManager
以获取输出文件。 
	NodeManager需要为分区文件运行reduce任务。并且reduce任务需要集群上若干个map
任务的map输出作为其特殊的分区文件。而每个map任务的完成时间可能不同,因此只要有一个
任务完成,reduce任务就开始复制其输出。

	reduce任务有少量复制线程,因此能够并行取得map输出。默认线程数为5,但这个默认值  
可以通过mapreduce.reduce.shuffle.parallelcopies属性进行设置。

1.1 Reducer如何知道自己应该处理哪些数据呢?

	因为Map端进行partition的时候,实际上就相当于指定了每个Reducer要处理的数据
(partition就对应了Reducer),所以Reducer在拷贝数据的时候只需拷贝与自己对应的  
partition中的数据即可。每个Reducer会处理一个或者多个partition。

1.2 reducer如何知道要从哪台机器上获取map输出呢?

	map任务完成后,它们会使用心跳机制通知它们的application master、因此对于指定作业,  
application master知道map输出和主机位置之间的映射关系。reducer中的一个线程定期询问  
master以便获取map输出主机的位置。知道获得所有输出位置。

2. 归并merge
	Copy 过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比 map 端的更为灵活,  
它基于 JVM 的 heap size 设置,因为 Shuffle 阶段 Reducer 不运行,所以应该把绝大  
部分的内存都给 Shuffle 用。

	Copy过来的数据会先放入内存缓冲区中,如果内存缓冲区中能放得下这次数据的话就直  
接把数据写到内存中,即内存到内存merge。Reduce要向每个Map去拖取数据,在内存中每个
Map对应一块数据,当内存缓存区中存储的Map数据占用空间达到一定程度的时候,开始启动  
内存中merge,把内存中的数据merge输出到磁盘上一个文件中,即内存到磁盘merge。与map  
端的溢写类似,在将buffer中多个map输出合并写入磁盘之前,如果设置了Combiner,则会  
化简压缩合并的map输出。Reduce的内存缓冲区可通过  
mapred.job.shuffle.input.buffer.percent配置,默认是JVM的heap size的70%。内存  
到磁盘merge的启动门限可以通过mapred.job.shuffle.merge.percent配置,默认是66%。

	当属于该reducer的map输出全部拷贝完成,则会在reducer上生成多个文件(如果拖取  
的所有map数据总量都没有内存缓冲区,则数据就只存在于内存中),这时开始执行合并操作,  
即磁盘到磁盘merge,Map的输出数据已经是有序的,Merge进行一次合并排序,所谓Reduce  
端的sort过程就是这个合并的过程,采取的排序方法跟map阶段不同,因为每个map端传过来  
的数据是排好序的,因此众多排好序的map输出文件在reduce端进行合并时采用的是归并排序,  
针对键进行归并排序。一般Reduce是一边copy一边sort,即copy和sort两个阶段是重叠而不  
是完全分开的。最终Reduce shuffle过程会输出一个整体有序的数据块。

3. reduce
	当一个reduce任务完成全部的复制和排序后,就会针对已根据键排好序的Key构造对应的  
Value迭代器。这时就要用到分组,默认的根据键分组,自定义的可是使用 
job.setGroupingComparatorClass()方法设置分组函数类。对于默认分组来说,只要这个  
比较器比较的两个Key相同,它们就属于同一组,它们的 Value就会放在一个Value迭代器,而  
这个迭代器的Key使用属于同一个组的所有Key的第一个Key。

	在reduce阶段,reduce()方法的输入是所有的Key和它的Value迭代器。此阶段的输出直  
接写到输出文件系统,一般为HDFS。如果采用HDFS,由于NodeManager也运行数据节点,所以  
第一个块副本将被写到本地磁盘。

1、	当reduce将所有的map上对应自己partition的数据下载完成后,reducetask真正进入  
reduce函数的计算阶段。由于reduce计算时同样是需要内存作为buffer,可以用
mapreduce.reduce.input.buffer.percent(default 0.0)  
(源代码MergeManagerImpl.java:674行)来设置reduce的缓存。

	这个参数默认情况下为0,也就是说,reduce是全部从磁盘开始读处理数据。如果这个参数  
大于0,那么就会有一定量的数据被缓存在内存并输送给reduce,当reduce计算逻辑消耗内存很  
小时,可以分一部分内存用来缓存数据,可以提升计算的速度。所以默认情况下都是从磁盘读取  
数据,如果内存足够大的话,务必设置该参数让reduce直接从缓存读数据,这样做就有点Spark  
Cache的感觉。

2、	Reduce在这个阶段,框架为已分组的输入数据中的每个键值对对调用一次  
reduce(WritableComparable,Iterator, OutputCollector, Reporter)方法。Reduce任务  
的输出通常是通过调用 OutputCollector.collect(WritableComparable,Writable)写入文
件系统的。
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值