MapReduce源码解读之MapTask-input

写在前面

前面分析了MapReduce客户端作业提交、计算MapTask数量,现在开始分析作业提交之后,MapTask是如何运行的。ResourceManager收到客户端提交的作业后,会启动MrAppMaster,MrAppmaster运行程序时向ResouceManager 请求maptask/reduceTask。这个请求最终会在NodeManager上产生一个Yarn child进程,Yarn child会反射出MapTask类,这个MapTask类就是Map任务的主类,这个类里有一个run()方法,我们从MapTask类的run()方法开始分析。

:本人使用的版本为Hadoop2.6.5,所以源码分析也是基于Hadoop2.6.5,如果对源码感兴趣的话可以点击这里下载源码

1. 入口:MapTask类的run()方法

 @Override
  public void run(final JobConf job, final TaskUmbilicalProtocol umbilical)
    throws IOException, ClassNotFoundException, InterruptedException {
    this.umbilical = umbilical;
    
	//这里判断它是否为MapTask
    if (isMapTask()) {
    
      // 从conf中获取ReduceTask的数量,如果没有ReduceTask,那么map阶段占100%
      if (conf.getNumReduceTasks() == 0) {
        mapPhase = getProgress().addPhase("map", 1.0f);
      } else {
      
        // 如果有ReduceTask,Map会多一个排序阶段,map阶段占67%,排序阶段占33%
        mapPhase = getProgress().addPhase("map", 0.667f);
        sortPhase  = getProgress().addPhase("sort", 0.333f);
      }
    }
    TaskReporter reporter = startReporter(umbilical);
 
    boolean useNewApi = job.getUseNewMapper();
    initialize(job, getJobID(), reporter, useNewApi);

    // 检查和清理JobTask
    if (jobCleanup) {
      runJobCleanupTask(umbilical, reporter);
      return;
    }
    if (jobSetup) {
      runJobSetupTask(umbilical, reporter);
      return;
    }
    if (taskCleanup) {
      runTaskCleanupTask(umbilical, reporter);
      return;
    }

	//新旧API的判断
    if (useNewApi) {

	  //我们使用的是新的API,所以进入runNewMapper()方法
      runNewMapper(job, splitMetaInfo, umbilical, reporter);
    } else {
      runOldMapper(job, splitMetaInfo, umbilical, reporter);
    }
    done(umbilical, reporter);
  }

2. 进入runNewMapper(job, splitMetaInfo, umbilical, reporter)方法

private <INKEY,INVALUE,OUTKEY,OUTVALUE>
  void runNewMapper(final JobConf job,
                    final TaskSplitIndex splitIndex,
                    final TaskUmbilicalProtocol umbilical,
                    TaskReporter reporter
                    ) throws IOException, ClassNotFoundException,
                             InterruptedException {
    // 创建MapTask的上下文,注意,job里面已经有从HDFS中下载的各种配置信息
    org.apache.hadoop.mapreduce.TaskAttemptContext taskContext =
      new org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl(job, 
                                                                  getTaskID(),
                                                                  reporter);
    // 创建一个mapper,这个mapper就是用户手写的类的实例,taskContext会从job中获取用户设定的MapperClass,然后传给mapper
    org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE> mapper =
      (org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE>)
      
      	//这里通过反射来创建类,反射的类就是用户手写的Mapper类
        ReflectionUtils.newInstance(taskContext.getMapperClass(), job);
        
    // 创建一个输入格式化类实例inputformat
    org.apache.hadoop.mapreduce.InputFormat<INKEY,INVALUE> inputFormat =
      (org.apache.hadoop.mapreduce.InputFormat<INKEY,INVALUE>)
      
//这里通过反射来创建一个InputFormatClass实例,
//注意,这个getInputFormatClass()方法在客户端提交作业时就出现过,默认是TextInputFormat   
ReflectionUtils.newInstance(taskContext.getInputFormatClass(), job);

    /** 重新构建Inputsplit实例split,为什么要重新构建呢?因为原split中包含了所有MapTask应该要执行的文件路径,起始位置,偏移量。
    而单个MapTask只需要关心自己执行哪一部分就行,所以需要重新构建应该split来存放自己要执行的split信息**/
    org.apache.hadoop.mapreduce.InputSplit split = null;
    split = getSplitDetails(new Path(splitIndex.getSplitLocation()),
        splitIndex.getStartOffset());
    LOG.info("Processing split: " + split);

//这里使用split,inputFormat, reporter, taskContext构造出了一个RecordReader实例input
//进入NewTrackingRecordReader类去看这个input里面封装了哪些东西
org.apache.hadoop.mapreduce.RecordReader<INKEY,INVALUE> input =
      new NewTrackingRecordReader<INKEY,INVALUE>
        (split, inputFormat, reporter, taskContext);

3. 进入 NewTrackingRecordReader类

    NewTrackingRecordReader(org.apache.hadoop.mapreduce.InputSplit split,
        org.apache.hadoop.mapreduce.InputFormat<K, V> inputFormat,
        TaskReporter reporter,
        org.apache.hadoop.mapreduce.TaskAttemptContext taskContext)
        throws InterruptedException, IOException {
        
        //跳过一些配置信息
      this.reporter = reporter;
      this.inputRecordCounter = reporter
          .getCounter(TaskCounter.MAP_INPUT_RECORDS);
      this.fileInputByteCounter = reporter
          .getCounter(FileInputFormatCounter.BYTES_READ);

      List <Statistics> matchedStats = null;
      if (split instanceof org.apache.hadoop.mapreduce.lib.input.FileSplit) {
        matchedStats = getFsStatistics(((org.apache.hadoop.mapreduce.lib.input.FileSplit) split)
            .getPath(), taskContext.getConfiguration());
      }
      fsStats = matchedStats;

      long bytesInPrev = getInputBytes(fsStats);
      
      //real就是一个记录读取器,我们进入这个createRecordReader()看它是如何被创建出来的
      this.real = inputFormat.createRecordReader(split, taskContext);
      long bytesInCurr = getInputBytes(fsStats);
      fileInputByteCounter.increment(bytesInCurr - bytesInPrev);
    }

5. 进入inputFormat.createRecordReader(split, taskContext)方法

/**
* 这个方法由TextInputFormat类实现
**/
 @Override
  public RecordReader<LongWritable, Text> 
    createRecordReader(InputSplit split,
                       TaskAttemptContext context) {
    String delimiter = context.getConfiguration().get(
        "textinputformat.record.delimiter");
    byte[] recordDelimiterBytes = null;
    if (null != delimiter)
      recordDelimiterBytes = delimiter.getBytes(Charsets.UTF_8);
      
      //这个方法最终返回了一个行记录读取器LineRecordReader()
      //也就是说经过层层包装,其实我们的实例input包含了一个LinerecordReader
      //然后我们回到NewTrackingRecordReader类
    return new LineRecordReader(recordDelimiterBytes);
  }

6.回到NewTrackingRecordReader类

	//new 了一个NewTrackingRecordReader,将split,inputFormat传进去,这样input才能读出一条记录
	//此时input已经包含了一个行记录读取器
    org.apache.hadoop.mapreduce.RecordReader<INKEY,INVALUE> input =
      new NewTrackingRecordReader<INKEY,INVALUE>
        (split, inputFormat, reporter, taskContext);
    
    job.setBoolean(JobContext.SKIP_RECORDS, isSkipping());
    org.apache.hadoop.mapreduce.RecordWriter output = null;
    
    // 创建一个输出对象output,暂时先跳过map的输出
    if (job.getNumReduceTasks() == 0) {
      output = 
        new NewDirectOutputCollector(taskContext, job, umbilical, reporter);
    } else {
      output = new NewOutputCollector(taskContext, job, umbilical, reporter);
    }

	//创建mapContext并将参数封装进去
    org.apache.hadoop.mapreduce.MapContext<INKEY, INVALUE, OUTKEY, OUTVALUE> 
    mapContext = 
      new MapContextImpl<INKEY, INVALUE, OUTKEY, OUTVALUE>(job, getTaskID(), 
          input, output, 
          committer, 
          reporter, split);

    //创建mapperContext并将mapContext传进去	
    	org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE>.Context 
        mapperContext = 
          new WrappedMapper<INKEY, INVALUE, OUTKEY, OUTVALUE>().getMapContext(
              mapContext);

    try {
    
      //input初始化,它完成了一个很重要的工作,进入initialize()方法
      input.initialize(split, mapperContext);
      

7. 进入input.initialize(split, mapperContext)方法

 @Override
 /**
 *这个方法的实现类是NewTrackingRecordReader
 */
    public void initialize(org.apache.hadoop.mapreduce.InputSplit split,
                           org.apache.hadoop.mapreduce.TaskAttemptContext context
                           ) throws IOException, InterruptedException {
      long bytesInPrev = getInputBytes(fsStats);
		
	  //这个real是之前创建出来的记录读取器,它又有一个initialize()方法,我们进入这个方法
      real.initialize(split, context);
      long bytesInCurr = getInputBytes(fsStats);
      fileInputByteCounter.increment(bytesInCurr - bytesInPrev);
    }

8. 进入real.initialize(split, context)方法

/**
*这个方法的实现类是LineRecordReader
*首先这里提出一个问题:split是按块的大小切分的,那么就有可能把一个单词切分成2部分,
*比如'hello'这个单词,有可能在做切分的时候,offset刚好到'he',这样一个完整的单词就被切成了'he'和'llo'两部分,这个问题该怎么解决呢?
*这个initialize()方法就是用来解决这个问题的
*/
public void initialize(InputSplit genericSplit,
                         TaskAttemptContext context) throws IOException {
                         
    //拿到切片信息
    FileSplit split = (FileSplit) genericSplit;
    Configuration job = context.getConfiguration();
    this.maxLineLength = job.getInt(MAX_LINE_LENGTH, Integer.MAX_VALUE);
    
    //从切片信息中拿到起始位置
    start = split.getStart();
    //从切片信息中拿到长度(其实就是偏移量),加上起始位置就是结束位置
    end = start + split.getLength();
    //从切片信息中拿到文件路径
    final Path file = split.getPath();

    // open the file and seek to the start of the split
    final FileSystem fs = file.getFileSystem(job);
    //打开这个文件,获取这个文件输入流
    fileIn = fs.open(file);
    
    CompressionCodec codec = new CompressionCodecFactory(job).getCodec(file);
    if (null!=codec) {
      isCompressedInput = true;	
      decompressor = CodecPool.getDecompressor(codec);
      if (codec instanceof SplittableCompressionCodec) {
        final SplitCompressionInputStream cIn =
          ((SplittableCompressionCodec)codec).createInputStream(
            fileIn, decompressor, start, end,
            SplittableCompressionCodec.READ_MODE.BYBLOCK);
        in = new CompressedSplitLineReader(cIn, job,
            this.recordDelimiterBytes);
        start = cIn.getAdjustedStart();
        end = cIn.getAdjustedEnd();
        filePosition = cIn;
      } else {
        in = new SplitLineReader(codec.createInputStream(fileIn,
            decompressor), job, this.recordDelimiterBytes);
        filePosition = fileIn;
      }
    } else {
    
      //每一个map都会seek到自己的切片的偏移量的起始位置去执行任务
      fileIn.seek(start);
      in = new UncompressedSplitLineReader(
          fileIn, job, this.recordDelimiterBytes, split.getLength());
      filePosition = fileIn;
    }
    // If this is not the first split, we always throw away first record
    // because we always (except the last split) read one extra line in
    // next() method.
    //除了第一个MapTask,其他MapTask都会执行这一步
    if (start != 0) {
    
    //每次起始位置将读取到的第一条记录丢弃掉,然后在末尾多读取一条记录
    //比如读取到'he',会多读取一条记录(比如'llo')并将它们拼接起来('hello'),然后下一个MapTask会丢弃'llo',这样就避免了重复读取
    //下面回到NewTrackingRecordReader类
      start += in.readLine(new Text(), 0, maxBytesToConsume(start));
    }
    //pos是第二条记录的偏移量
    this.pos = start;
  }

9.回到NewTrackingRecordReader类

      input.initialize(split, mapperContext);
      
      //到这里我们发现,MapTask的run()方法调用了Mapper的run()方法,mapper的run()方法又调用了用户重写的run()方法,
      //这个mapperContext里面包含了input,output,split,lineRecorder等信息
      //现在再来看这个run()方法是怎么读取记录的,进入run()方法
      mapper.run(mapperContext);
      mapPhase.complete();
      
      //用户的run()方法结束,开始走排序阶段
      setPhase(TaskStatus.Phase.SORT);
      statusUpdate(umbilical);
      input.close();
      input = null;
      output.close(mapperContext);
      output = null;
    } finally {
      closeQuietly(input);
      closeQuietly(output, mapperContext);
    }
  }

10. 进入mapper.run(mapperContext)方法

  public void run(Context context) throws IOException, InterruptedException {
    setup(context);
    try {
    
    //context里面包含了行读取器,map()方法为用户手写的map方法,通过context.nextKeyValue()来获取一条记录,我们进入context.nextKeyValue()方法
      while (context.nextKeyValue()) {
        map(context.getCurrentKey(), context.getCurrentValue(), context);
      }
    } finally {
      cleanup(context);
    }
  }

11. 进入context.nextKeyValue()方法

 @Override
/**
*这个方法的实现类是LineRecordReader
*/
public boolean nextKeyValue() throws IOException {
    if (key == null) {
      key = new LongWritable();
    }
    
    //将第二条记录的偏移量赋值给Key,也就是说从第二条记录开始读
    key.set(pos);
    if (value == null) {
      value = new Text();
    }
    int newSize = 0;
    
    // 在这里可以看到,在读取完end的偏移量之后,还会再读取一条记录
    while (getFilePosition() <= end || in.needAdditionalRecordAfterSplit()) {
      if (pos == 0) {
        newSize = skipUtfByteOrderMark();
      } else {
        newSize = in.readLine(value, maxLineLength, maxBytesToConsume(pos));
        pos += newSize;
      }

      if ((newSize == 0) || (newSize < maxLineLength)) {
        break;
      }

      // line too long. try again
      LOG.info("Skipped line of size " + newSize + " at pos " + 
               (pos - newSize));
    }
    
    //如果newSize==0,说明没有key-value了,也就没有记录了,返回false
    //回到mapper.run(mapperContext)方法
    if (newSize == 0) {
      key = null;
      value = null;
      return false;
    } else {
      return true;
    }
  }

12. 回到mapper.run(mapperContext)方法

 public void run(Context context) throws IOException, InterruptedException {
    setup(context);
    try {
    
    //context里面包含了行读取器,map()方法为用户手写的map方法,通过context.nextKeyValue()来获取一条记录,我们进入context.nextKeyValue()方法
      while (context.nextKeyValue()) {
      //假设有记录,执行map方法,我们进入map(context.getCurrentKey(), context.getCurrentValue(), context)方法
        map(context.getCurrentKey(), context.getCurrentValue(), context);
      }
    } finally {
      cleanup(context);
    }
  }

13.进入map(context.getCurrentKey(), context.getCurrentValue(), context)方法

/**
*这个map方法就是用户要实现的map方法
*/
  protected void map(KEYIN key, VALUEIN value, 
                     Context context) throws IOException, InterruptedException {
    context.write((KEYOUT) key, (VALUEOUT) value);
  }

14.总结

从对上面的源码的分析过程中我们发现,其实大部分工作都由LineRecordReader这个类来完成。input使用行记录读取器来实现读取文件数据。这段代码解释了每一个MapTask是如何去读取自己要执行的数据,如何避免完整的记录被切分成两部分,以及用户实现的map方法是如何被加载的。

发布了17 篇原创文章 · 获赞 0 · 访问量 344

猜你喜欢

转载自blog.csdn.net/qq_37163925/article/details/105627064