InputSplit—>RecordReder—>map(key,value,context)的过程解析

上图首先描述了在TaskTracker端Task(MapTask、ReduceTask)的执行过程,MapTask(org.apache.hadoop.mapred)首先被TaskRunner调用,然后在MapTask内部首先进行一些初始化工作,然后调用run()方法,判断如果使用了新版API就调用RunNewMapper()开始执行Map操作。

1)runNewMapper()分析

1.首先创建一个Mapper对象

// make a mapper
    org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE> mapper =
      (org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE>)
        ReflectionUtils.newInstance(taskContext.getMapperClass(), job);

2.创建一个InputFormat对象

1  org.apache.hadoop.mapreduce.InputFormat<INKEY,INVALUE> inputFormat =
2       (org.apache.hadoop.mapreduce.InputFormat<INKEY,INVALUE>)
3         ReflectionUtils.newInstance(taskContext.getInputFormatClass(), job);

3.创建InputSplit对象

  org.apache.hadoop.mapreduce.InputSplit split = null;
    split = getSplitDetails(new Path(splitIndex.getSplitLocation()),
        splitIndex.getStartOffset());//获得分片的详细信息

其中,splitIndex是TaskSplitIndex类型(用于指示此mapTask处理的分片),TaskSplitIndex有两个字段:

String splitLocation  //job.split在HDFS上的路径

long startOffset    //此次处理的分片在job.split中的位置。

利用上述两个字段首先找到job.split,然后就可以在startOffset的位置处找到这次处理的分片的详细信息。

4.利用InputFormat和InputSplit创建RecordReader对象input,这里应该是已经确定了input具体是那种记录读取器,例如LineRecordReader

1  org.apache.hadoop.mapreduce.RecordReader<INKEY,INVALUE> input =
2       new NewTrackingRecordReader<INKEY,INVALUE>
3           (split, inputFormat, reporter, job, taskContext);

5.创建输出收集器OutputCollector对象output(如果reduce=0创建NewDirectOutputCollector类对象,否则创建NewOutputCollector类对象)

     // get an output object
      if (job.getNumReduceTasks() == 0) {//reduce数量如果是0
         output =
           new NewDirectOutputCollector(taskContext, job, umbilical, reporter);
      } else {
        output = new NewOutputCollector(taskContext, job, umbilical, reporter);
      }

6.利用上述对象创建MapContext类的对象mapperContext

mapperContext = contextConstructor.newInstance(mapper, job, getTaskID(),
                                                     input, output, committer,
                                                     reporter, split);

其中reader和split是数据成员,getCurrentKey()是获得当前的key,同样getCurrentValue().如果还有下条记录,nextKeyValue()返回true,否则返回false,这三个方法均由reader调用。由于RecordReader是抽象类,并未实现相关方法,其子类实现了这些方法。

 1  @Override
 2   public KEYIN getCurrentKey() throws IOException, InterruptedException {
 3     return reader.getCurrentKey();
 4   }
 5 
 6   @Override
 7   public VALUEIN getCurrentValue() throws IOException, InterruptedException {
 8     return reader.getCurrentValue();
 9   }
10 
11   @Override
12   public boolean nextKeyValue() throws IOException, InterruptedException {
13     return reader.nextKeyValue();
14   }

在MapContext的构造函数中,字段reader就是由input初始化的,所以reader的具体类型也是已经确定了的,所以会调用具体实现了的这些方法,例如LineRecorReader的方法

(在org.apache.hadoop.mapreduce.lib.input中找到,因为新版API重写了LineRecordReader),以下是LineRecordReader部分源码():

 1 private CompressionCodecFactory compressionCodecs = null;
 2   private long start;
 3   private long pos;
 4   private long end;
 5   private LineReader in;
 6   private int maxLineLength;
 7   private LongWritable key = null;
 8   private Text value = null;
 9   private Seekable filePosition;
10   private CompressionCodec codec;
11 public void initialize(InputSplit genericSplit,
12                          TaskAttemptContext context) throws IOException {
13     FileSplit split = (FileSplit) genericSplit;
14     Configuration job = context.getConfiguration();
15     this.maxLineLength = job.getInt("mapred.linerecordreader.maxlength",
16                                     Integer.MAX_VALUE);
17     start = split.getStart();
18     end = start + split.getLength();
19     final Path file = split.getPath();
20     compressionCodecs = new CompressionCodecFactory(job);
21     codec = compressionCodecs.getCodec(file);
22 
23     // open the file and seek to the start of the split
24     FileSystem fs = file.getFileSystem(job);
25     FSDataInputStream fileIn = fs.open(split.getPath());
26 
27     if (isCompressedInput()) {
28       decompressor = CodecPool.getDecompressor(codec);
29       if (codec instanceof SplittableCompressionCodec) {
30         final SplitCompressionInputStream cIn =
31           ((SplittableCompressionCodec)codec).createInputStream(
32             fileIn, decompressor, start, end,
33             SplittableCompressionCodec.READ_MODE.BYBLOCK);
34         in = new LineReader(cIn, job);
35         start = cIn.getAdjustedStart();
36         end = cIn.getAdjustedEnd();
37         filePosition = cIn;
38       } else {
39         in = new LineReader(codec.createInputStream(fileIn, decompressor),
40             job);
41         filePosition = fileIn;
42       }
43     } else {
44       fileIn.seek(start);
45       in = new LineReader(fileIn, job);
46       filePosition = fileIn;
47     }
48     // If this is not the first split, we always throw away first record
49     // because we always (except the last split) read one extra line in
50     // next() method.
51     if (start != 0) {
52       start += in.readLine(new Text(), 0, maxBytesToConsume(start));
53     }
54     this.pos = start;
55   }
56  @Override
57   public LongWritable getCurrentKey() {
58     return key;
59   }
60 
61   @Override
62   public Text getCurrentValue() {
63     return value;
64   }
65

7-8行可看到类的字段key和value。使用initialize()方法初始化,读取分片中的数据到key/value由nextKeyValue()方法完成: 

 1 public boolean nextKeyValue() throws IOException {
 2     if (key == null) {
 3       key = new LongWritable();
 4     }
 5     key.set(pos);//以记录的偏移量为key
 6     if (value == null) {
 7       value = new Text();
 8     }
 9     int newSize = 0;
10     // We always read one extra line, which lies outside the upper
11     // split limit i.e. (end - 1)
12     while (getFilePosition() <= end) {
13      //获取value值,调用了很多的函数
14       newSize = in.readLine(value, maxLineLength,
15           Math.max(maxBytesToConsume(pos), maxLineLength));
16       if (newSize == 0) {
17         break;
18       }
19       pos += newSize;//更新pos
20       if (newSize < maxLineLength) {
21         break;
22       }
23 
24       // line too long. try again
25       LOG.info("Skipped line of size " + newSize + " at pos " + 
26                (pos - newSize));
27     }
28     if (newSize == 0) {
29       key = null;
30       value = null;
31       return false;
32     } else {
33       return true;
34     }
35   }

7.初始化记录读取器input(例如LineRecordReader.initialize())

 input.initialize(split, mapperContext);

8.调用Mapper类的run()方法:

 mapper.run(mapperContext);

Mapper类结果如下所示:

Mapper有一个内部类Context。通过run()方法调用这几个方法,run()的实现如下所示:

  public void run(Context context) throws IOException, InterruptedException {
    setup(context);
    try {
      while (context.nextKeyValue()) {
        map(context.getCurrentKey(), context.getCurrentValue(), context);
      }
    } finally {
      cleanup(context);
    }
  }

 从MapTask的角度分析下Mapper中的run()方法内的context.nextkeyValue(),流程图如下所示:

上面已经给出了LineRecordRead的源码,以下做简要分析:

LineRecordRead有3个核心字段,分别是pos,key,value。pos就是读取的字段在文件中的偏移量,每次通过nextKeyValue()方法中读取分片中一个记录,并将pos设置为此记录的key,然后再将此记录存储在value中,最后更新pos的值,作为下个字段的偏移量。最后,nextKeyValue方法返回一个布尔值,true表示成功读取到一条记录,否则,表示此分片中已没有记录。

     然后执行map(context.getCurrentKey(), context.getCurrentValue(), context),其中context.getCurrentKey()调用了LineRecordRead的方法getCurrentKey()直接返回当前key,context.getCurrentValue()也是同样。

2)基于以上的分析,MapTask的任务逻辑图如下所示:

    其中输入分片就是由上述的第2、3完成的。RecordReader就对应了第4步的记录读取器input对象。OutputCollector对应第5步中的输出收集器对象output。第8步就对应了上图中的Mapper,接下来就分析Mapper之后发生了什么,这就要进入到Mapper类的map()方法内部:

用户要重写Mapper的map方法,这里以WordCount为例进行分析。重写的map方法如下所示;

 public void map(Object key, Text value, Context context
                    ) throws IOException, InterruptedException {
      StringTokenizer itr = new StringTokenizer(value.toString());
      while (itr.hasMoreTokens()) {
        word.set(itr.nextToken());
        context.write(word, one);
      }
    }

     关注最后一行代码:context.write(word,one),一直Context是Mapper的内部类,继承自MapContext类,那么这个write方法究竟做了什么呢?以下是整个调用过程:

context.write(word, one);

Context类只是简单的继承了MapContext类,并没有write方法,查看MapContext有没有write方法,结果MapContext也没有write方法,继续查看MapContext的父类TaskInputOutputContext,其中write方法源码为:

public void write(KEYOUT key, VALUEOUT value
                    ) throws IOException, InterruptedException {
    output.write(key, value);
  }

output是此类的一个字段,定义如下:

private RecordWriter<KEYOUT,VALUEOUT> output;

而RecordWriter是一个抽象类,没有字段,只有未实现的抽象方法write和close,Context通过继承机制,获得了output字段,这个字段肯定是RecordWriter的某个具体实现类,到底是哪个类呢?转了一圈,我们看看context对象的来源:就是在RunNewMapper中(对应第8步)

 mapper.run(mapperContext);

mapper就是一个Mapper对象,调用其run方法:

 public void run(Context context) throws IOException, InterruptedException {
    setup(context);
    try {
      while (context.nextKeyValue()) {
        map(context.getCurrentKey(), context.getCurrentValue(), context);
      }
    } finally {
      cleanup(context);
    }
  }

将mapperContext对象赋值给了context对象,也就是context的来源是mapperContext对象,那我们就需要看看mapperContext是怎么来的:

org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE>.Context mapperContext = null;

mapperContext = contextConstructor.newInstance(mapper, job, getTaskID(),input, output, committer,reporter, split);

首先mapperContext对象是Context类型,然后就是第二行代码的作用就相当于使用new Context(....)创建新对象。是时候上图了:

super就是调用父类的构造函数。再次贴上mapperContext创建的代码:

mapperContext = contextConstructor.newInstance(mapper, job, getTaskID(),input, output, committer,reporter, split);

     job就对应于conf,getTaskID()就对应于taskid,input对应reader,output对应writer,...。通过观察这三个类的构造函数,不能看出最终output对象传值给了TaskInputOutputContext类中的RecordReader output对象。再回到这个output的定义:

// get an output object
      if (job.getNumReduceTasks() == 0) {//reduce数量如果是0
         output =
           new NewDirectOutputCollector(taskContext, job, umbilical, reporter);
      } else {
        output = new NewOutputCollector(taskContext, job, umbilical, reporter);
      }

这样我们就可以确定TaskInputOutputContext中字段output的类型是NewOutputCollector类型(RecordWriter抽象类的一个实现)。

当然,context继承了TaskInputOutputContext这个output字段,更重要的还有其write方法。对Context类做个小结,到目前为止所知道的它的字段和方法如下:

再回到map方法:

 public void map(Object key, Text value, Context context
                    ) throws IOException, InterruptedException {
      StringTokenizer itr = new StringTokenizer(value.toString());
      while (itr.hasMoreTokens()) {
        word.set(itr.nextToken());
        context.write(word, one);
      }
    }

可以看到context.write()方法其实就是调用了NewOutputCollector类的write方法,这个类部分声明:

private final MapOutputCollector<K,V> collector;//map的输出内存缓冲区。
    private final org.apache.hadoop.mapreduce.Partitioner<K,V> partitioner;//作业所使用的分区(Partitioner)类型(默认的Partitioner就是HashPartitioner)
    private final int partitions;//reduce的数量 
    NewOutputCollector(org.apache.hadoop.mapreduce.JobContext jobContext,
                       JobConf job,
                       TaskUmbilicalProtocol umbilical,
                       TaskReporter reporter
                       ) throws IOException, ClassNotFoundException {
      collector = new MapOutputBuffer<K,V>(umbilical, job, reporter);//创建collector对象
      partitions = jobContext.getNumReduceTasks();//获得reduce的数量。
      if (partitions > 0) {
        partitioner = (org.apache.hadoop.mapreduce.Partitioner<K,V>)
          ReflectionUtils.newInstance(jobContext.getPartitionerClass(), job);//获得作业所使用的分区(Partitioner)类型(默认的Partitioner就是HashPartitioner)
      } else {
        partitioner = new org.apache.hadoop.mapreduce.Partitioner<K,V>() {
          @Override
          public int getPartition(K key, V value, int numPartitions) {
            return -1;
          }
        };
      }
    }   
    public void write(K key, V value) throws IOException, InterruptedException {
      collector.collect(key, value,
                        partitioner.getPartition(key, value, partitions));
    }

     NewOutputCollector有三个数据成员:collector、partitioner和partitions,这三个字段都在构造函数内完成初始化,collector是MapOutputBuffer类的对象,是本类的核心字段,partitioner是Partitioner类的对象,用于指示本次map所使用的分区类型,所谓的对key/value分区的过程其实也就是调用getPartition(key,value,reduceNums)方法返回一个整数作为此键值对的分区号,用户可以自定义分区类,其实也就是自定义getPartition(key,value,reduceNums)方法。不过分区只是根据key将map的输出分成不同的区(以0,1,2,3等数字作为分区号),每个区用一个reduce处理。默认的分区方法是HashPartitioner,首先将key的哈希值和Integer类型最大值进行与运算,然后将结果对作业的reduce数量取模值,将这个模值作为此key/value对应的分区号,可见键值对的分区号只是与key有关,其原型如下:

public class HashPartitioner<K, V> extends Partitioner<K, V> {

  /** Use {@link Object#hashCode()} to partition. */
  public int getPartition(K key, V value,
                          int numReduceTasks) {
    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
  }

接下来查看到NewOutputCollector的write方法又调用了collector.collect(key,value)方法:

public void write(K key, V value) throws IOException, InterruptedException {
      collector.collect(key, value,
                        partitioner.getPartition(key, value, partitions));
    }

不深入collect方法内部看的话,看到此方法的第一印象就是colllector将key、value、partition(对应的分区号)一起存入内存缓冲区。接下来分析map阶段的spill过程。

 参考:

http://zheming.wang/hadoop-mapreduce-zhi-xing-liu-cheng-xiang-jie.html

原文地址:https://www.cnblogs.com/lz3018/p/4927667.html