mapreduce 任务task处理流程

在MapReduce计算框架中,一个应用程序被划分成Map和Reduce两个计算阶段,它们分别由一个或者多个Map Task和Reduce Task组成。其中,每个Map Task处理输入数据集合中的一片数据(InputSplit),并将产生的若干个数据片段写到本地磁盘上,而Reduce Task则从每个Map Task上远程拷贝相应的数据片段,经分组聚集和归约后,将结果写到HDFS上作为最终结果,如下图所示。总体上看,Map Task与Reduce Task之间的数据传输采用了pull模型。为了能够容错,Map Task将中间计算结果存放到本地磁盘上,而Reduce Task则通过HTTP请求从各个Map Task端拖取(pull)相应的输入数据。

对于Map Task而言,它的执行过程可概述为:首先,通过用户提供的InputFormat将对应的InputSplit解析成一系列key/value,并依次交给用户编写的map()函数处理;接着按照指定的Partitioner对数据分片,以确定每个key/value将交给哪个Reduce Task处理;之后将数据交给用户定义的Combiner进行一次本地规约(用户没有定义则直接跳过);最后将处理结果保存到本地磁盘上。

对于Reduce Task而言,由于它的输入数据来自各个Map Task,因此首先需通过HTTP请求从各个已经运行完成的Map Task上拷贝对应的数据分片,待所有数据拷贝完成后,再以key为关键字对所有数据进行排序,通过排序,key相同的记录聚集到一起形成若干分组,然后将每组数据交给用户编写的reduce()函数处理,并将数据结果直接写到HDFS上作为最终输出结果。

接下来我们深入分析Map Task和Reduce Task的执行细节。

Map Task

Map Task的整体计算流程如图所示,共分为5个阶段,分别是:

(1)、Read阶段:Map Task通过用户编写的RecordReader从输入InputSplit中解析处一个个的Key/Value。

(2)、Map阶段:该阶段主要将解析出来的Key/Value交给用户编写的map()函数处理,并产生新的key/value。

(3)、Collect阶段:在用户编写的map()函数中,数据处理完时一般调用OutputCollector.collect()输出结果,在该函数内部,它首先调用Partitioner计算该key/value所属的partition,并将key/value写入到环形缓冲区中。

(4)、Spill阶段:即“溢写”阶段,当环形缓冲区比较满后,MapTask会将缓冲区中的数据写到本地磁盘的临时文件中,需要注意的是,在写入文件之前,MapTask会首先对数据进行一次排序,如果用户指定了压缩和combiner函数,那么还会对数据进行压缩和合并。

(5)、Combine阶段:当该Map Task的所有数据处理结束之后,Map Task对所有生成的临时文件进行合并,确保对当前的Map Task只生成一个数据文件。

接下来我们重点介绍三个部分: 环形缓冲区,key/value排序,和临时文件的合并。

环形缓冲区

当用户在map函数中调用OutputCollector.collect(key,value)时,MR内部调用如下函数,即首先计算该key/value的partition,然后将该数据写入缓冲区中,缓冲区默认大小为100M。缓冲区有单向缓冲区,双线缓冲区和环形缓冲区,之所以MapReduce选择环形缓冲区主要是为了能够并发读写,缓冲区80%空间被占用后就开始启动数据溢写,剩下20%的空间可以继续写入数据。

 collector.collect(key, value,partitioner.getPartition(key, value, numPartitions));

我们知道对一个key/value来讲,实际上缓冲区中要存储<partion, key,value>三个数据。Map Task中的缓冲区采用如下图所示的两级索引结构。其中最后真正存储数据的是kvbuffer字节数组。key/value写入kvbuffer时是首先要经过序列化的,所以我们要记录每一个key和value序列化后在kvbuffer中的起始和终止位置。这就是kvindices[]索引的作用。在这个索引中,我们记录每一个key/value的partition值,key序列化后在kvbuffer中的开始位置,value序列化后在kvbuffer中的开始位置。由于是顺序写入buffer中,所以后一个的开始位置即为前一个的结束位置,所以省去记录keyend和valend值。其实从实现之后的排序以及文件Spill的角度来讲,单独一个kvindicess索引就已经足够了。但是为了方便排序实现,所以引入第二级索引结构:kvoffset, kvoffset中记录每一个key/value在kvindices中得起始位置,也恰好就是每一个key/value得partition位置。

引入kvoffset得作用就是方便排序。因为排序得过程中涉及到swap操作,如果只有kvindices的话,每次swap就得交换三个项,有了kvoffset之后,只需要交换一个值即可,同时有了kvoffset也方便遍历所有得key。接下来我们介绍排序。

排序

我们知道mapper端生成得所有key/value都会被partition到不同得reducer处理,所以mapper端首先要把同一个partition的数据放到一起。reducer拿到所有mapper得相应中间数据之后需要按照key对全部数据进行排序,为了减少排序得消耗,MapReduce会在mapper端就进行一次本地排序。那么最终得方案就是,在mapper端对key/value进行排序,排序比较函数为,首先比较partition得值,再比较key值

排序的算法主要是使用的快排。只不过相比较naive的快排,MapReduce中做了如下的优化:

  1. Pivot的选择:选择首、尾、中间三个元素的中位数
  2. 减少递归深度:对一个长度为n的数组序列,计算最大递归次数为2 * ceil(log(n)),如果递归深度大于该阈值,则对剩下的元素采用堆排序。同时如果当前数组长度小于13时,对当前数组直接采用插入排序
  3. 优化相同元素:常规的快排中,我们把序列分为一般分为两部分,小于pivot的部分,和大于等于pivot的部分。再MR中,分为三部分,小于pivot的部分,等于pivot的部分和大于pivot的部分。等于pivot的部分之后不再参与排序。

前两点优化比较常见,最后一种优化业主要是根据MR的具体场景做出的选择。如果相同key较多的情况下,对快排的优化还是比较有帮助的。

3.3 文件合并

前面提到,当环形缓冲区中数据快满时,MR会把缓冲区中的数据排序后写到磁盘的临时文件中,最终该Map Task结束时,我们就会有一堆的临时文件,最终我们要把这些文件进行合并,生成一个最终的输出文件用于后续的需要。同时我们要保证最终的文件也是有序的,相同partition的key/value集中到一起,同时同一个partition下的key/value按照key值进行排序。即最终的输出文件如下图所示。同时,我们还需要一个元信息文件,记录每个Partition在文件中的开始位置,压缩后的长度(MapReduce支持输出文件压缩),压缩前的原始长度等信息。我们的临时文件的格式和下图所示完全一样,每个临时文件也会有一个对应的元信息文件。所以我们要做的就是,将一堆有序的临时文件合并成一个全局有序的大文件。这个问题和子节跳动面试问的问题本质上应该是同一个问题。

 

MapReduce中采用的方案就是采用堆排序的方式来进行排序。对所有的中间文件,都采用一个Segment类进行封装抽象成一个迭代器,然后用每个Segment的当前元素构建一个小顶堆,整体构成一个Merger,Merger也是一个迭代器。用户不断地从Merger中获取元素写入到最终的输出文件中。Merger每次返回堆顶的元素,返回后将堆顶对应的Segment的下一个元素入堆,重新构建小顶堆,依此类推。所以文件合并过程实际上是一个不断建堆的过程:建堆→取堆顶元素→重新建堆→取堆顶元素……

最终,经过上述的一系列步骤,一个Map Task运行结束之后,我们得到了一个输出结果文件和一个元信息文件。这里面有两个值得讨论的地方:

  1. 为什么不直接每次Spill的时候,把数据Append到最终的目标文件中,然后对目标文件进行一次排序即可?
  2. 为什么最终只生成一个文件,而不是对每个partition都生成一个文件?

问题1主要是放到一个大文件里面排序可能比较困难,如果数据非常大的话,最终仍然是借助归并排序的方式进行排序。所以每次都会排好序后写入一个临时文件,每个临时文件大约为100M,最终归并排序这些临时文件即可。

问题2主要是为了节省inode,因为如果reducer数目很多的话,每个maptask就会生成大量的小文件,而且这些文件只有等到整个Job完成后才能删除,所以可能没运行几个Map Task, 系统的文件inode就被用光了。

以上就是MapTask的执行过程了。接下来我们介绍Reduce Task。

Reduce Task

Reduce Task的整体计算流程如图所示,共分为5个阶段。

(1)、Shuffle阶段:也称为Copy阶段。Reduce Task从各个Map Task上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。

(2)、Merge阶段:在远程拷贝数据的同时,Reduce Task启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。

(3)、Sort阶段:按照MapReduce语义,用户编写的reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个Map Task已经实现对自己的处理结果进行了局部排序,因此,Reduce Task只需对所有数据进行一次归并排序即可。

(4)、Reduce阶段:在该阶段中,Reduce Task将每组数据依次交给用户编写的reduce()函数处理。

(5)、Write阶段:reduce()函数将计算结果写到HDFS上。

Reduce Task会有一个专门的线程GetMapEventsThread不停的询问Task Tracker当前作业中已经完成的Map任务,每当有新的Map任务结束后,Reduce Task就会通过MapOutputCopier线程该Map Task所在的TaskTracker拉取数据。如果拉取的数据过大,则直接将数据写入磁盘的一个临时文件中,否则放入到内存中。同时,磁盘上的文件个数不能过多,如果个数超过 (2 * ioSortFactor - 1)时,就会触发线程进行文件合并。由于Reduce拉取的数据一部分在内存中,一部分在磁盘上,所以会有两个线程分别进行合并。reducer采用的文件合并方式与mapper采用的合并方式相同,所以上文中虽然提到有Merge阶段和Sort阶段,但是我们从Merge的过程会发现,merge的时候就是再排序。所以当最终剩下K个临时文件之后,对这K个文件构建一个Merger,我们迭代Merger的过程,就是在排序。每次我们迭代出相同Key的所有数据,然后输入到用户的reduce函数中进行处理,最终将处理的结果写到HDFS中。这部分涉及到的技术点和Map阶段的技术点有很多相似之处,所以介绍相对简短。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值