1、阶段划分
MapReduce执行的4个阶段,分别为Split阶段—> Map阶段 —> Shuffle阶段 —> Reduce阶段。
第一阶段:Split输入分片阶段
1、输入分片
输入分片,是逻辑分片,所谓逻辑分片就是根据文件的字节索引进行分割,比如0-1MB位置定义为第一个分片,1MB-2MB定义为为第二个分片,依次类推……而原来的大文件还是原来的大文件,不会受到影响,因此,输入分片(input split)存储的并非数据本身,而是一个分片长度和一个记录数据的位置的数组。
2、分片数量与Map Task数量的关系
Map Task的个数等于split的个数。mapreduce在处理大文件的时候,会根据一定的规则,把大文件划分成多个分片,这样能够提高map的并行度。 划分出来的就是InputSplit,每个map处理一个InputSplit,因此,有多少个InputSplit,就有多少个map task。
3、由谁来划分分片
主要是InputFormat类来负责划分Split。InputFormat类有2个重要的作用:
1)将输入的数据切分为多个逻辑上的InputSplit,其中每一个InputSplit作为一个map的输入。
2)提供一个RecordReader,用于将InputSplit的内容转换为可以作为map输入的k,v键值对。
4、分片的大小由谁来决定?
每个输入分片的大小是固定的,默认情况下,输入片(InputSplit)的大小与数据块(Block)的大小是相同的。Hadoop 2.x默认的block大小是128MB,Hadoop 1.x默认的block大小是64MB,可以在hdfs-site.xml中设置dfs.block.size,注意单位是byte。
5、默认分片大小与Block分块大小是相同的原因
hadoop在存储有输入数据(HDFS中的数据)的节点上运行map任务,可以获得高性能,这就是所谓的数据本地化。所以最佳分片的大小应该与HDFS上的块大小一样,因为如果分片跨越2个数据块,对于任何一个HDFS节点(Hadoop系统保证一个块存储在一个datanode上,基本不可能同时存储这2个数据块),分片中的另外一块数据就需要通过网络传输到map任务节点,与使用本地数据运行map任务相比,效率则更低!优点就是可以实现分块优化,减少网络传输数据,使用本地数据运行map任务。
第二阶段:Map阶段
每个Mapper任务是一个java进程,它会读取HDFS文件中自己对应的输入切片,将切片中记录按照一定的规则解析成很多的键值对,有个默认规则是把每一行文本内容解析成键值对,这里的“键”是每一行的起始位置(单位是字节),“值”是本行的文本内容,也就是我们图中定义为<k1,v1>。然后经过我们重写的map方法处理,针对解析出来的每一个键值对<k1,v1>,都分别调用一次map方法,如果有1000个键值对,就会调用1000次map方法,每一次调用map方法会输出零个或者多个键值对,这里输出的键值对,也就是我们图中定义的,比如我们单词统计案例中其中的一个<k1,v1>为<0,‘Dear Bear River’>,经一次map处理后变为3个<k2,v2>,即<‘Dear’,1>、<‘Bear’,1>和<‘River’,1>。
第三阶段:Shuffle阶段
Shuffle阶段也称为洗牌阶段,该阶段是将输出的<k2,v2>传给Shuffle(洗牌),Shuffle完成对数据的分区、排序和合并等操作。Shuffle过程包含在Map和Reduce两端,即Map shuffle和Reduce shuffle, Shuffle描述着数据从map task流向reduce task的这段过程,具体过程如图:
1、Map端的Shuffle
1)每个输入分片会让一个 Map 任务来处理,默认情况下,以 HDFS 的一个块的大小(默认为 128MB)为一个分片。Map函数开始产生输出时,并不是简单地把数据写到磁盘中,因为频繁的磁盘操作会导致性能严重下降。它的处理过程是把数据首先写到内存中的一个缓冲区, 并做一些预排序,以提升效率。
2)每个 Map 任务都有一个用来写入输出数据的循环内存缓冲区(默认大小为 128MB),当缓冲区中的数据量达到一个特定阈值(默认是 80%)时,系统将会启动一个后台线程,把缓冲 区中的内容写到磁盘中(即 Spill 阶段)。在写磁盘过程中,Map 输出继续被写到缓冲区中,但如果在此期间缓冲区被填满,那么 Map 任务就会阻塞直到写磁盘过程完成。
3)在写入磁盘之前,线程首先根据reduce任务的数目将数据划分为相同数目的分区(Partition),也就是一个reduce任务对应一个分区的数据。这样做是为了避免有些reduce任务分配到大量数据,而有些reduce任务却分到很少数据,甚至没有分到数据的尴尬局面。其实分区就是对数据进行hash的过程。然后对每个分区中的数据进行排序,如果此时设置了Combiner,将排序后的结果进行Combine操作,这样做的目的是让尽可能少的数据写入到磁盘。
4)当map任务输出最后一个记录时,可能会有很多的溢出文件,这时需要将这些文件合并。合并的过程中会不断地进行排序和Combine操作,目的有两个:①尽量减少每次写入磁盘的数据量。②尽量减少下一复制阶段网络传输的数据量。最后合并成了一个已分区且已排序的文件。为了减少网络传输的数据量,这里可以将数据压缩,只要将mapred.compress.map.out设置为true就可以了。
5)溢出写文件归并完毕后,Map 任务将删除所有的临时溢出写文件,并告知 TaskTracker 任务已完成,只要其中一个 Map 任务完成,Reduce 任务就会开始复制它的输出(Copy 阶段)。
6)Map 任务的输出文件放置在运行 Map 任务的 TaskTracker 的本地磁盘上,它是运行 Reduce 任务的 TaskTracker 所需要的输入数据。
2、Reduce 端的 Shuffle 阶段
1)Reduce 进程启动一些数据复制线程,请求 Map 任务所在的 TaskTracker 以获取输出文件,并且每个map传来的数据都是有序的(Copy 阶段)。
2)如果reduce端接受的数据量相当小,则直接存储在内存中(缓冲区大小由mapred.job.shuffle.input.buffer.percent属性控制,表示用作此用途的堆空间的百分比),如果数据量超过了该缓冲区大小的一定比例(由mapred.job.shuffle.merge.percent决定),则对数据合并后溢写到磁盘中。
3)随着溢写文件的增多,后台线程会将它们合并成一个更大的有序的文件,这样做是为了给后面的合并节省时间。其实不管在map端还是reduce端,MapReduce都是反复地执行排序,合并操作,可以说:排序是hadoop的灵魂。
4)合并的过程中会产生许多的中间文件(写入磁盘了),但MapReduce会让写入磁盘的数据尽可能地少,并且最后一次合并的结果并没有写入磁盘,而是直接输入到reduce函数。
5)在输入到reduce函数之前,reducer接收到所有映射到这个reducer的map输出后,也是会对key排序,然后开始构造一个key对应的value迭代器。这时就要用到分组,存在默认分组,key相同则分为一组,也可以使用jobjob.setGroupingComparatorClass设置的自定义分组函数类分组,只要这个比较器比较的两个key相同,他们就属于同一个组,它们的value放在一个value迭代器,而这个迭代器的key使用属于同一个组的所有key的第一个key。
第四阶段:Reduce阶段
Reduce阶段就将分组好的<k2,{v2集合}>,进行聚合统计,和map函数一样也是程序员编写的,然后将结果输出到HDFS,每个reduce进程会对应一个输出文件,名称以part-开头。