上篇我刚刚学习完。Spilt的过程,还算比較简单的了,接下来学习的就是Map操作的过程了,Map和Reduce一样。是整个MapReduce的重要内容,所以。这一篇,我会好好的讲讲里面的内部实现过程。首先要说,MapTask。分为4种,可能这一点上有人就可能知道了,各自是Job-setup Task,Job-cleanup Task。Task-cleanup和Map Task。前面3个都是辅助性质的任务。不是本文分析的重点,我讲的就是里面的最最重要的MapTask。

MapTask的整个过程分为5个阶段:

Read----->Map------>Collect------->Spill------>Combine

来张时序图。简单明了:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvQW5kcm9pZGx1c2hhbmdkZXJlbg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">

在后面的代码分析中。你会看到各自方法的调用过程。

在分析整个过程之前。得先了解里面的一些内部结构,MapTask类作为Map Task的一个载体。他的类关系例如以下:

我们调用的就是里面的run方法,开启map任务,对应的代码:

/**
* mapTask主要运行流程
*/
@Override
public void run(final JobConf job, final TaskUmbilicalProtocol umbilical)
throws IOException, ClassNotFoundException, InterruptedException {
this.umbilical = umbilical; // start thread that will handle communication with parent
//发送task任务报告。与父进程做交流
TaskReporter reporter = new TaskReporter(getProgress(), umbilical,
jvmContext);
reporter.startCommunicationThread();
//推断用的是新的MapReduceAPI还是旧的API
boolean useNewApi = job.getUseNewMapper();
initialize(job, getJobID(), reporter, useNewApi); // check if it is a cleanupJobTask
//map任务有4种。Job-setup Task, Job-cleanup Task, Task-cleanup Task和MapTask
if (jobCleanup) {
//这里运行的是Job-cleanup Task
runJobCleanupTask(umbilical, reporter);
return;
}
if (jobSetup) {
//这里运行的是Job-setup Task
runJobSetupTask(umbilical, reporter);
return;
}
if (taskCleanup) {
//这里运行的是Task-cleanup Task
runTaskCleanupTask(umbilical, reporter);
return;
} //假设前面3个任务都不是,运行的就是最基本的MapTask,依据新老API调用不同的方法
if (useNewApi) {
runNewMapper(job, splitMetaInfo, umbilical, reporter);
} else {
//我们关注一下老的方法实现splitMetaInfo为Spilt分片的信息。因为上步骤的InputFormat过程传入的
runOldMapper(job, splitMetaInfo, umbilical, reporter);
}
done(umbilical, reporter);
}

在这里我研究的都是旧的API所以往runOldMapper里面跳。

在这里我要插入一句,后面的运行都会环绕着一个叫Mapper的东西,就是用户运行map函数的一个代理称呼一样,他能够全然自己重写map的背后的过程,也能够用系统自带的mapp流程。

系统已经给了MapRunner的详细实现:

public void run(RecordReader<K1, V1> input, OutputCollector<K2, V2> output,
Reporter reporter)
throws IOException {
try {
// allocate key & value instances that are re-used for all entries
K1 key = input.createKey();
V1 value = input.createValue(); //从RecordReader中获取每一个键值对,调用用户写的map函数
while (input.next(key, value)) {
// map pair to output
//调用用户写的map函数
mapper.map(key, value, output, reporter);
if(incrProcCount) {
reporter.incrCounter(SkipBadRecords.COUNTER_GROUP,
SkipBadRecords.COUNTER_MAP_PROCESSED_RECORDS, 1);
}
}
} finally {
//结束了关闭mapper
mapper.close();
}
}

从这里我们能够看出Map的过程就是迭代式的反复的运行用户定义的Map函数操作。好了,有了这些前提,我们能够往里深入的学习了刚刚说到了runOldMapper方法,里面立即要进行的就是Map Task的第一个过程Read。

Read阶段的作业就是从RecordReader中读取出一个个key-value,准备给后面的map过程运行map函数操作。

//获取输入inputSplit信息
InputSplit inputSplit = getSplitDetails(new Path(splitIndex.getSplitLocation()),
splitIndex.getStartOffset()); updateJobWithSplit(job, inputSplit);
reporter.setInputSplit(inputSplit); //是否是跳过错误记录模式,获取RecordReader
RecordReader<INKEY,INVALUE> in = isSkipping() ?
new SkippingRecordReader<INKEY,INVALUE>(inputSplit, umbilical, reporter) :
new TrackedRecordReader<INKEY,INVALUE>(inputSplit, job, reporter);

后面的就是Map阶段。把值取出来之后。就要给Mapper去运行里面的run方法了,run方法里面会调用用户自己实现的map函数。之前也都是分析过了的。

在用户编写的map的尾部,通常会调用collect.collect()方法,把处理后的key-value输出,这个时候,也就来到了collect阶段。

runner.run(in, new OldOutputCollector(collector, conf), reporter);

之后进行的是Collect阶段基本的操作时什么呢,就是把一堆堆的key-value进行分区输出到环形缓冲区中。这是的数据只放在内存中。还没有写到磁盘中。在collect这个过程中涉及的东西还比較多,看一下结构关系图;

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvQW5kcm9pZGx1c2hhbmdkZXJlbg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">

里面有个partitioner的成员变量,专门用于获取key-value的的分区号。默认是通过key的哈希取模运算。得到分区号的,当然你能够自己定义实现,假设不分区的话partition就是等于-1。

  /**
* Since the mapred and mapreduce Partitioners don't share a common interface
* (JobConfigurable is deprecated and a subtype of mapred.Partitioner), the
* partitioner lives in Old/NewOutputCollector. Note that, for map-only jobs,
* the configured partitioner should not be called. It's common for
* partitioners to compute a result mod numReduces, which causes a div0 error
*/
private static class OldOutputCollector<K,V> implements OutputCollector<K,V> {
private final Partitioner<K,V> partitioner;
private final MapOutputCollector<K,V> collector;
private final int numPartitions; @SuppressWarnings("unchecked")
OldOutputCollector(MapOutputCollector<K,V> collector, JobConf conf) {
numPartitions = conf.getNumReduceTasks();
if (numPartitions > 0) {
//假设分区数大于0,则反射获取系统配置方法,默认哈希去模。用户能够自己实现字节的分区方法
//由于是RPC传来的,所以採用反射
partitioner = (Partitioner<K,V>)
ReflectionUtils.newInstance(conf.getPartitionerClass(), conf);
} else {
//假设分区数为0。说明不进行分区
partitioner = new Partitioner<K,V>() {
@Override
public void configure(JobConf job) { }
@Override
public int getPartition(K key, V value, int numPartitions) {
//分区号直接返回-1代表不分区处理
return -1;
}
};
}
this.collector = collector;
}
.....

collect的代理调用实现方法例如以下,注意此时还不是真正调用:

.....
@Override
public void collect(K key, V value) throws IOException {
try {
//详细通过collect方法分区写入内存。调用partitioner.getPartition获取分区号
//缓冲区为环形缓冲区
collector.collect(key, value,
partitioner.getPartition(key, value, numPartitions));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new IOException("interrupt exception", ie);
}
}

这里的collector指的是上面代码中的MapOutputCollector对象。开放给用调用的是OldOutputCollector,可是我们看看代码:

interface MapOutputCollector<K, V> {

    public void collect(K key, V value, int partition
) throws IOException, InterruptedException;
public void close() throws IOException, InterruptedException; public void flush() throws IOException, InterruptedException,
ClassNotFoundException; }

他仅仅是一个接口,真正的实现是谁呢?这个时候应该回头看一下代码:

private <INKEY,INVALUE,OUTKEY,OUTVALUE>
void runOldMapper(final JobConf job,
final TaskSplitIndex splitIndex,
final TaskUmbilicalProtocol umbilical,
TaskReporter reporter
) throws IOException, InterruptedException,
ClassNotFoundException {
...
int numReduceTasks = conf.getNumReduceTasks();
LOG.info("numReduceTasks: " + numReduceTasks);
MapOutputCollector collector = null;
if (numReduceTasks > 0) {
//假设存在ReduceTask,则将数据存入MapOutputBuffer环形缓冲
collector = new MapOutputBuffer(umbilical, job, reporter);
} else {
//假设没有ReduceTask任务的存在,直接写入把操作结果写入HDFS作为终于结果
collector = new DirectMapOutputCollector(umbilical, job, reporter);
}
MapRunnable<INKEY,INVALUE,OUTKEY,OUTVALUE> runner =
ReflectionUtils.newInstance(job.getMapRunnerClass(), job); try {
runner.run(in, new OldOutputCollector(collector, conf), reporter);
.....

分为2种情况当有Reduce任务时。collector为MapOutputBuffer,没有Reduce任务时为DirectMapOutputCollector。从这里也能明确。作者考虑的非常周全呢,没有Reduce直接写入HDFS,效率会高非常多。

也就是说。终于的collect方法就是MapOutputBuffer的方法了。

由于collect的操作时将数据存入环形缓冲区,这意味着。用户对数据的读写都是在同个缓冲区上的,所以为了避免出现脏数据的现象,一定会做额外处理。这里作者用了和BlockingQueue类似的操作,用一个ReetrantLocj,获取2个锁控制条件,一个为spillDone

,一个为spillReady。同个condition的await,signal方法实现丢缓冲区的读写控制。

.....
private final ReentrantLock spillLock = new ReentrantLock();
private final Condition spillDone = spillLock.newCondition();
private final Condition spillReady = spillLock.newCondition();
.....

然后看collect的方法:

public synchronized void collect(K key, V value, int partition
) throws IOException {
.....
try {
// serialize key bytes into buffer
int keystart = bufindex;
keySerializer.serialize(key);
if (bufindex < keystart) {
// wrapped the key; reset required
bb.reset();
keystart = 0;
}
// serialize value bytes into buffer
final int valstart = bufindex;
valSerializer.serialize(value);
int valend = bb.markRecord(); if (partition < 0 || partition >= partitions) {
throw new IOException("Illegal partition for " + key + " (" +
partition + ")");
}
....

至于环形缓冲区的结构。不是本文的重点,结构设计还是比較复杂的。大家能够自行学习。当环形缓冲区内的数据渐渐地被填满之后,会出现"溢写"操作,就是把缓冲中的数据写到磁盘DISK中。这个过程就是后面的Spill阶段了。

Spill的阶段会时不时的穿插在collect的运行过程中。

...
if (kvstart == kvend && kvsoftlimit) {
LOG.info("Spilling map output: record full = " + kvsoftlimit);
startSpill();
}

假设开头kvstart的位置等kvend的位置,说明转了一圈有到头了。数据已经满了的状态,開始spill溢写操作。

private synchronized void startSpill() {
LOG.info("bufstart = " + bufstart + "; bufend = " + bufmark +
"; bufvoid = " + bufvoid);
LOG.info("kvstart = " + kvstart + "; kvend = " + kvindex +
"; length = " + kvoffsets.length);
kvend = kvindex;
bufend = bufmark;
spillReady.signal();
}

会触发condition的信号量操作:

private synchronized void startSpill() {
LOG.info("bufstart = " + bufstart + "; bufend = " + bufmark +
"; bufvoid = " + bufvoid);
LOG.info("kvstart = " + kvstart + "; kvend = " + kvindex +
"; length = " + kvoffsets.length);
kvend = kvindex;
bufend = bufmark;
spillReady.signal();
}

就会跑到了SpillThead这个地方运行sortAndSpill方法:

spillThreadRunning = true;
try {
while (true) {
spillDone.signal();
while (kvstart == kvend) {
spillReady.await();
}
try {
spillLock.unlock();
//当缓冲区溢出时,写到磁盘中
sortAndSpill();

sortAndSpill里面会对数据做写入文件操作写入之前还会有sort排序操作。数据多了还会进行一定的combine合并操作。

private void sortAndSpill() throws IOException, ClassNotFoundException,
InterruptedException {
......
try {
// create spill file
final SpillRecord spillRec = new SpillRecord(partitions);
final Path filename =
mapOutputFile.getSpillFileForWrite(numSpills, size);
out = rfs.create(filename); final int endPosition = (kvend > kvstart)
? kvend
: kvoffsets.length + kvend;
//在写入操作前进行排序操作
sorter.sort(MapOutputBuffer.this, kvstart, endPosition, reporter);
int spindex = kvstart;
IndexRecord rec = new IndexRecord();
InMemValBytes value = new InMemValBytes();
for (int i = 0; i < partitions; ++i) {
IFile.Writer<K, V> writer = null;
try {
long segmentStart = out.getPos();
writer = new Writer<K, V>(job, out, keyClass, valClass, codec,
spilledRecordsCounter);
if (combinerRunner == null) {
// spill directly
DataInputBuffer key = new DataInputBuffer();
while (spindex < endPosition &&
kvindices[kvoffsets[spindex % kvoffsets.length]
+ PARTITION] == i) {
final int kvoff = kvoffsets[spindex % kvoffsets.length];
getVBytesForOffset(kvoff, value);
key.reset(kvbuffer, kvindices[kvoff + KEYSTART],
(kvindices[kvoff + VALSTART] -
kvindices[kvoff + KEYSTART]));
//writer中写入键值对操作
writer.append(key, value);
++spindex;
}
} else {
int spstart = spindex;
while (spindex < endPosition &&
kvindices[kvoffsets[spindex % kvoffsets.length]
+ PARTITION] == i) {
++spindex;
}
// Note: we would like to avoid the combiner if we've fewer
// than some threshold of records for a partition
//假设分区多的话,运行合并操作
if (spstart != spindex) {
combineCollector.setWriter(writer);
RawKeyValueIterator kvIter =
new MRResultIterator(spstart, spindex);
//运行一次文件合并combine操作
combinerRunner.combine(kvIter, combineCollector);
}
} ......
//写入到文件里
spillRec.writeToFile(indexFilename, job);
} else {
indexCacheList.add(spillRec);
totalIndexCacheMemory +=
spillRec.size() * MAP_OUTPUT_INDEX_RECORD_LENGTH;
}
LOG.info("Finished spill " + numSpills);
++numSpills;
} finally {
if (out != null) out.close();
}
}

每次Spill的过程都会产生一堆堆的文件,在最后的时候就会来到了Combine阶段。也就是Map任务的最后一个阶段了,他的任务就是把全部上一阶段的任务产生的文件进行Merge操作,合并成一个文件,便于后面的Reduce的任务的读取,在代码的相应实现中是collect.flush()方法。

.....
try {
runner.run(in, new OldOutputCollector(collector, conf), reporter);
//将collector中的数据刷新到内存中去
collector.flush();
} finally {
//close
in.close(); // close input
collector.close();
}
}

这里的collector的flush方法调用的就是MapOutputBuffer.flush方法,

public synchronized void flush() throws IOException, ClassNotFoundException,
InterruptedException {
...
// shut down spill thread and wait for it to exit. Since the preceding
// ensures that it is finished with its work (and sortAndSpill did not
// throw), we elect to use an interrupt instead of setting a flag.
// Spilling simultaneously from this thread while the spill thread
// finishes its work might be both a useful way to extend this and also
// sufficient motivation for the latter approach.
try {
spillThread.interrupt();
spillThread.join();
} catch (InterruptedException e) {
throw (IOException)new IOException("Spill failed"
).initCause(e);
}
// release sort buffer before the merge
kvbuffer = null;
//最后进行merge合并成一个文件
mergeParts();
Path outputPath = mapOutputFile.getOutputFile();
fileOutputByteCounter.increment(rfs.getFileStatus(outputPath).getLen());
}

至此,Map任务宣告结束了。总体流程还是真是有点九曲十八弯的感觉。

分析这么一个比較庞杂的过程,我一直在想怎样更好的表达出我的想法。欢迎MapReduce的学习者,提出意见,共同学习

Map Task内部实现分析的更多相关文章

  1. ${mapred.local.dir}选择策略--Map Task存放中间结果

    上篇说了block在DataNode配置有多个${dfs.data.dir}时的存储策略,本文主要介绍TaskTracker在配置有多个${mapred.local.dir}时的选择策略. mapre ...

  2. hadoop输入分片计算(Map Task个数的确定)

    作业从JobClient端的submitJobInternal()方法提交作业的同时,调用InputFormat接口的getSplits()方法来创建split.默认是使用InputFormat的子类 ...

  3. hadoop 分片与分块,map task和reduce task的理解

    分块:Block HDFS存储系统中,引入了文件系统的分块概念(block),块是存储的最小单位,HDFS定义其大小为64MB.与单磁盘文件系统相似,存储在 HDFS上的文件均存储为多个块,不同的是, ...

  4. list 、set 、map 粗浅性能对比分析

    list .set .map 粗浅性能对比分析   不知道有多少同学和我一样,工作五年了还没有仔细看过list.set的源码,一直停留在老师教导的:"LinkedList插入性能比Array ...

  5. C++ map,set内部数据结构

    1)Set是一种关联容器,它用于存储数据,并且能从一个数据集合中取出数据.它的每个元素的值必须唯一,而且系统会根据该值来自动将数据排序.每个元素的值不能直接被改变.[重点]内部结构采用红黑树的平衡二叉 ...

  6. MapReduce作业的map task和reduce task调度参数

    MapReduce作业可以细分为map task和reduce task,而MRAppMaster又将map task和reduce task分为四种状态: 1.pending:刚启动但尚未向reso ...

  7. Spark技术内幕:Shuffle Map Task运算结果的处理

    Shuffle Map Task运算结果的处理 这个结果的处理,分为两部分,一个是在Executor端是如何直接处理Task的结果的:还有就是Driver端,如果在接到Task运行结束的消息时,如何对 ...

  8. java集合框架07——Map架构与源代码分析

    前几节我们对Collection以及Collection中的List部分进行了分析,Collection中还有个Set,因为Set是基于Map实现的,所以这里我们先分析Map,后面章节再继续学习Set ...

  9. 线程池ThreadPool及Task调度死锁分析

    近1年,偶尔发生应用系统启动时某些操作超时的问题,特别在使用4核心Surface以后.笔记本和台式机比较少遇到,服务器则基本上没有遇到过. 这些年,我写的应用都有一个习惯,就是启动时异步做很多准备工作 ...

随机推荐

  1. 分酒问题(DFS解法)

    题目大概是这样: 已知有三个容量分别为3千克.5千克和8千克的并且是没有刻度的酒瓶,3千克和5千克的瓶子均装满了酒,而8千克的瓶子为空.现要求仅用这三个酒瓶将这些酒均分为两个4千克并分别装入5千克和8 ...

  2. AngularJS学习篇(二十三)

    AngularJS 路由 AngularJS 路由允许我们通过不同的 URL 访问不同的内容. 通过 AngularJS 可以实现多视图的单页Web应用(single page web applica ...

  3. Problem E: 可变长数组

    Problem E: 可变长数组 Time Limit: 1 Sec  Memory Limit: 128 MBSubmit: 472  Solved: 368[Submit][Status][Web ...

  4. java注解(Annotation)

    本文转载自http://www.cnblogs.com/xdp-gacl/p/3622275.html 一.认识注解 注解(Annotation)很重要,未来的开发模式都是基于注解的,JPA是基于注解 ...

  5. 06.十分钟学会表达式语言EL

    一. 概念:MVC设计模式一个主要好处就是让jsp中的代码越来越来少,而且规定只能出现三种代码:接收属性,判断语句,迭代输出.但是,在开发中,jsp输出至少还是需要接受VO对象的,这时候为了避免导入V ...

  6. Flex布局(引用阮一峰大神)

    Flex 布局教程:语法篇 http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html Flex 布局教程:实例篇 http://www.ruan ...

  7. C#执行PowserShell 脚本

    Windows PowserShell能够很简洁 快速通过Script脚本方式获得我们想要执行效果. 如何在C#中任意执行PowerShell脚本.?类似目前我要在做一个进程管理工具. 通过Power ...

  8. [转载] Redis系统性介绍

    转载自http://blog.nosqlfan.com/html/3139.html?ref=rediszt 虽然Redis已经很火了,相信还是有很多同学对Redis只是有所听闻或者了解并不全面,下面 ...

  9. 【转】WCF传输大数据的设置

    在从客户端向WCF服务端传送较大数据(>65535B)的时候,发现程序直接从Reference的BeginInvoke跳到EndInvoke,没有进入服务端的Service实际逻辑中,怀疑是由于 ...

  10. iOS11、iPhone X、Xcode9 适配

    更新iOS11后,发现有些地方需要做适配,整理后按照优先级分为以下三类: 1.单纯升级iOS11后造成的变化: 2.Xcode9 打包后造成的变化: 3.iPhoneX的适配 一.单纯升级iOS11后 ...