1. Shuffle:
MapReduce的计算模型主要分为三个阶段,Map, shuffle, Reduce。 Map负责数据的过滤,将文件中的数据转化为键值对,Reduce负责合并将具有相同的键的值进行处理合并然后输出到HDFS。 为了让Reduce可以并行处理map的结果,必须对Map的输出进行一定的排序和分割,然后交个Reduce,这个过程就是Shuffle。
官方给的图如下:
在这里插入图片描述
上图Map和Reduce之间的就是shuffle,但是猛地一看就是云里雾里的,倒不如下面这个图清楚:
在这里插入图片描述
Map端的shuffle简单来说就是对map的结果进行分区缓存,当缓存不够的时候进行溢写,在溢写的过程中,排序写入到文件,每一次溢写是一个文件,最后将这些文件合并成一个文件。 分区排序的意思是相同partition的键值对存储在一起,partition之间是有序有的,每一个partition中的键值对也是有序的,默认是升序。
(1) 缓冲区
Map的输出结果不是直接写到文件的,是先写到缓存区中,缓存区是一个环形结构,是用环形缓存区的目的是尽可能高效的利用内存空间,默认大小是100M,可以通过参数调整缓冲区的大小。如下图:
在这里插入图片描述
这个缓冲区其实是一个字节数组叫做kvbuffer,kvbuffer不只有数据键值对,还有数据的索引叫做kvmeta。 1byte[] kvbuffer; // main output buffer2private static final int VALSTART = 0; // val offset in acct3private static final int KEYSTART = 1; // key offset in acct4private static final int PARTITION = 2; // partition offset in acct5private static final int VALLEN = 3; // length of value
索引和数据的放在不同的两个区域,用一个分界点来划分,这个分界点不是一层不变的,会随着每次的溢写而改变,初始的位置为0,数据向上增长,索引向下增长。上图中的buindex是数据的位置索引,一直向上增长,比如初始值为0,写入一个int的key之后变为4,再写入一个int的Value之后变为8.
索引的区记录的是数据键值对的位置,是一个四元组占用4个Int长度:
value的起始位置,key的起始位置,partition的值以及Value的长度。 索引的写入是kvindex每次向下跳4个字节,然后再向上填充数据,比如Kvindex初始位置是-4,当第一个键值对写完之后,(Kvindex+0)的位置存放value的起始位置、(Kvindex+1)的位置存放key的起始位置、(Kvindex+2)的位置存放partition的值、(Kvindex+3)的位置存放value的长度,然后Kvindex跳到-8位置,等第二个键值对和索引写完之后,Kvindex跳到-12位置。
kvbuffer 的默认大小为100M,当然可以自己设置:
1public static final String IO_SORT_MB = "mapreduce.task.io.sort.mb";2final int sortmb = job.getInt(JobContext.IO_SORT_MB, 100);
当缓存区达到一定的比例之后,一个后台线程开始把缓存区的数据写入到磁盘,这个写入的过程叫做Spill,即溢写。开始Spill的比例默认是0.8,这个比列可以通过mapreduce.map.sort.spill.percent配置,在后台溢写的同时,map继续向这个剩余的缓存中继续写入数据,写入数据的起始位置是剩余空间的中间,分别向两边写入索引和数据,如果缓存区满了溢写还没与完成的话,map会阻塞直到Spill完成。
spill的比列默认是0.8 也是可以设置的:
1public static final String MAP_SORT_SPILL_PERCENT = "mapreduce.map.sort.spill.percent";2final float spillper = job.getFloat(JobContext.MAP_SORT_SPILL_PERCENT, (float)0.8);
分区是在写入缓存的时候完成的,看了很多博客说是在溢写的时候进行的分区,感觉不是很对。想想也能明白,既然索引中要写入数据了,实在是没必要溢写的时候补上,并且缓存总放到都是byte数组,来回转换不也是麻烦。我看了看源码确实是在写入缓存的时候进行的分区:
1@Override2public void collect(K key, V value) throws IOException {3 try {4 collector.collect(key, value, partitioner.getPartition(key, value, numPartitions));5 } catch (InterruptedException ie) {6 Thread.currentThread().interrupt();7 throw new IOException("interrupt exception