Mapreduce的shuffle过程详解
mapreduce框架内部核心工作机制详解示意图
- maptask之前,输入切片的划分
如图,以一个wordcount的job为例,在文件输入目录下有a.txt,b.txt,c.txt 三个文件(/wordcount/input/a.txt,b.txt,c.txt),a.txt为200M,b.txt为180M,c.txt为100M。然后划分输入切片,job客户端负责划分:扫描输入目录中的所有文件,遍历每一个文件,按照128M划分。
切片用FileSplit对象描述(存放文件的路径,偏移量),划分成以下
FileSplit0:/word/input/a.txt 0-128M
FileSplit1:/word/input/a.txt 128-200M
FileSplit2:/word/input/a.txt 0-128M
FileSplit3:/word/input/a.txt 128-180M
等几个切片。之后将这几个对象放在ArrayList中,最后将ArrayList序列化到叫做job.split的文件中。
(2)maptask
1.MRAppMaster读取job.split文件,根据job.split文件里的切片数,启动相应个数的maptask(maptask的进程名是yarn child)。这里以两个切片为例,相应地启动了两个maptask:maptask0处理FileSplit0切片,maptask1处理FileSplit1切片。
2.启动了maptask之后,就开始读取数据,maptask调用TextInputFormat类里拿到一个LineRecordReader对象,调用LineRecordReader的next()方法,并且反复调用next()方法,调用一次next()方法,读取一行数据,并将读取的一行的起始偏移量作为key,该行的内容作为value。将(k,v)传入Mapper类的map方法中,next()反复调用,map()就反复调用,将这些(k,v)处理完之后,传给Context对象。
3.MapOutCollector从Context对象中读取数据之后,将这些(k,v)写入一个大小为100M的环形缓冲区内(环形缓冲区的大小可以自己设置),数据一直往环形缓冲区写,当达到环形缓冲区的80%(也可以自己设置)时,maptask会启动一个Spiller线程,这个线程对这个80%的数据进行分区(按Partitioner的getPartitioner方法分区),排序(按key排序),Combiner。
剩下的20%内存继续写入数据。分区,排序,Combiner完成之后,会将内存中的数据读出来,写入本地磁盘(maptask运行的本地)(这一过程就是溢写,溢出之后,继续往环形缓冲区写入数据,新的数据将之前的数据覆盖)。溢出的文件存放了多个分区的数据(如图黑色kv是0号分区数据,蓝色kv是1号分区数据),一个溢出文件中,区号小的数据在前面,并且同区中按key排序。溢出文件可能会有很多个(图中有两个溢出文件)。以上过程循环往复,直到LineRecordReader调用next()方法,发现没有下一个数据时(读到最后的偏移量时),环形缓冲区的数据都写入磁盘中。
4.调用Merge这个工具类将溢出到磁盘的多个文件合并(如果数据小于80M,只溢出一个文件,就不用合并),这些文件中,0号区的数据合并在一起,1号区的数据合并在一起(如图),产生一个文件(文件中的数据分区且有序),产生这个文件的同时还会生成一个相应的分区索引文件(指明了各个分区的偏移量是从哪开始从哪结束)。生成这个文件之后maptask的工作完全结束。
注:Combiner的作用是为了减轻IO负担,例如分区中有(a,1),(a,1)(b,1),(c,1),(d,1),(d,1)这样一组数据,Combiner之后就变成了(a,2),(b,1),(c,1),(d,2),减小了数据量。
(3)reduce
1.MRAppMaster启动reduce,并告诉哪个reduce处理哪个分区的数据。如图,这里reduce0处理0号分区的数据,reduce1处理1号分区的数据。Maptask的结果文件会被纳入NodeManager的web程序document目录中,根据这个目录,reduce就知道maptask结果文件的位置,就可以从各个maptask运行的节点下载数据了(http协议下载)。Reduce0就下载两个结果文件中的0号分区的数据,在reduce中就有两个0号分区文件,再次使用merge将这两个文件合并(这个过程还会排序)成一个文件(都是0号区数据)
2.Reducer类里的reduce(k,迭代器,context)方法处理数据,迭代器每迭代一次(相应的数据就放入key对象,迭代器中,从磁盘反序列化),reduce方法中的迭代器从文件中获取数据,用分组比较器(GroupingComparator)判断每一次迭代的key是否与上一次迭代的key相同,相同就继续迭代。不同,一次reduce就结束了。下一次再调用迭代器就从下一个key开始,相同的key是一组。将数据交给context.write(k,v),它会将数据传递给TextOutPutFormat类,TextOutPutFormat调用getRecordWrite方法获取RecordWriter类,最后用RecordWriter类的write(k,v)将数据写出。