Hadoop整个MR的过程源码解析(二)-map端任务的执行

map端相对于client端就会复杂很多,里面包含了map的读入,重新赋值,输出等过程,

入口为;mapTask的run方法,因为我自己使用的是2.6.5的版本所以是使用的newapi,此处需要注意的地方是,如果reduce的个数为0的话,就不会设置排序的过程,之后还会有根据reduce的个数进行双向选择的地方,此处需要注意

  @Override
    public void run(final JobConf job, final TaskUmbilicalProtocol umbilical)
            throws IOException, ClassNotFoundException, InterruptedException {
        this.umbilical = umbilical;

        if (isMapTask()) {
            // If there are no reducers then there won't be any sort. Hence the map
            // phase will govern the entire attempt's progress.
            //判断reduce的个数。默认reduce的个数为1个
            if (conf.getNumReduceTasks() == 0) {
                mapPhase = getProgress().addPhase("map", 1.0f);
            } else {
                // If there are reducers then the entire attempt's progress will be
                // split between the map phase (67%) and the sort phase (33%).
                mapPhase = getProgress().addPhase("map", 0.667f);
                //增加排序阶段-》排序结果-》内部有序外部无序--》reduce性能的提升
                sortPhase = getProgress().addPhase("sort", 0.333f);
            }
        }
        TaskReporter reporter = startReporter(umbilical);

        boolean useNewApi = job.getUseNewMapper();
        //初始化job
        initialize(job, getJobID(), reporter, useNewApi);

        // check if it is a cleanupJobTask
        if (jobCleanup) {
            runJobCleanupTask(umbilical, reporter);
            return;
        }
        if (jobSetup) {
            runJobSetupTask(umbilical, reporter);
            return;
        }
        if (taskCleanup) {
            runTaskCleanupTask(umbilical, reporter);
            return;
        }

        if (useNewApi) {
            runNewMapper(job, splitMetaInfo, umbilical, reporter);
        } else {
            runOldMapper(job, splitMetaInfo, umbilical, reporter);
        }
        done(umbilical, reporter);
    }

接下来的这个方法中将进行map相关的基本上所有的重要流程;

  /**
     * 这个方法的作用;
     *  初始化任务的上下文-》包含job相关的信息
     *  初始化切片-》该map所要处理的切片是哪一个-》切片决定map的个数,有多少个切片就会有多少个map去执行
     *  初始化读取器-》切片中的内容是以什么样格式被读到内存中去-》里面涉及的有;
     *          获取切片所归属的位置信息
     *          根据该map所持有的切片的位置信息和偏移量,决定归属的文件,打开文件,seek到偏移量的位置开始读取
     *              进而完成计算像数据移动
     *              此处需要注意的是;
     *                  如果该切片不是第一个切片,切片相关信息中有切片的索引值,就会抛弃首行,从第二行开始读取,并把下一个切片的首行读进来
     * 初始化mapper;
     *      此处是需要我们去重新定义的,定义map输出的key,value的格式和类型,如果不重写的话,会导致沿用默认的mapper,输出从文件重获取的k,v的值,没有实际意义
     *      此处是必须重写的地方
     * 初始化output;
     *      两个方式
     *          如果没有reduce过程-》初始化直接输出的output
     *          如果存在reduce过程-》初始化一个NewOutputCollector-》
     *                              初始化一个可排序的环形缓冲区-》100m,0.8时发生溢写
     *                                  当发生溢写的时候同时会发生排序,排序时先按分区排序,再按key排序
     *                                      如果没有自定义排序比较器的话,会按照key值所属的类型的比较器去进行比较
     *
     *
     */
    @SuppressWarnings("unchecked")
    private 
    void runNewMapper(final JobConf job,
                      final TaskSplitIndex splitIndex,
                      final TaskUmbilicalProtocol umbilical,
                      TaskReporter reporter
    ) throws IOException, ClassNotFoundException,
            InterruptedException {
        // make a task context so we can get the classes
        //任务执行的上下文实例
        org.apache.hadoop.mapreduce.TaskAttemptContext taskContext =
                new org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl(job,
                        getTaskID(),
                        reporter);
        // make a mapper
        //此处说明我们必须重写mapper,不然会默认将从文件中获取的key,value直接输出。
        org.apache.hadoop.mapreduce.Mapper mapper =
                (org.apache.hadoop.mapreduce.Mapper)
                        ReflectionUtils.newInstance(taskContext.getMapperClass(), job);
        // make the input format
        //输入格式化类
        //mapreduce.job.inputformat.class 如果没有设置这个参数的话
        //->默认的格式化类为TextInputFormat
        org.apache.hadoop.mapreduce.InputFormat inputFormat =
                (org.apache.hadoop.mapreduce.InputFormat)
                        ReflectionUtils.newInstance(taskContext.getInputFormatClass(), job);
        // rebuild the input split
        //重建输入的 切片信息-》获取到,,,重构切片清单
        org.apache.hadoop.mapreduce.InputSplit split = null;
        //获取当前map需要处理的切片信息
        //根据切片的索引获取当前切片的位置信息和偏移量,
        split = getSplitDetails(new Path(splitIndex.getSplitLocation()),
                splitIndex.getStartOffset());
        LOG.info("Processing split: " + split);
        //初始化文件读取器-》默认linerecordReader->按行读取
        org.apache.hadoop.mapreduce.RecordReader input =
                new NewTrackingRecordReader
                        (split, inputFormat, reporter, taskContext);

        job.setBoolean(JobContext.SKIP_RECORDS, isSkipping());
        org.apache.hadoop.mapreduce.RecordWriter output = null;

        // get an output object
        //设置输出模式-》根据reduce的个数来判断
        if (job.getNumReduceTasks() == 0) {
            output =
                    new NewDirectOutputCollector(taskContext, job, umbilical, reporter);
        } else {
            output = new NewOutputCollector(taskContext, job, umbilical, reporter);
        }

        org.apache.hadoop.mapreduce.MapContext
                mapContext =
                new MapContextImpl(job, getTaskID(),
                        input, output,
                        committer,
                        reporter, split);

        org.apache.hadoop.mapreduce.Mapper.Context
                mapperContext =
                new WrappedMapper().getMapContext(
                        mapContext);

        try {
            //输入的初始化-》将首行扔弃,将给上一个map去获取
            /**
             * 输入格式化器的初始化;
             *  获取切片所属的文件,位置,以及将切片内容放到mappe人的上下文中
             */
            input.initialize(split, mapperContext);
            //mapper程序开始执行
            mapper.run(mapperContext);
            mapPhase.complete();//程序计数器
            setPhase(TaskStatus.Phase.SORT);
            statusUpdate(umbilical);
            input.close();
            input = null;
            output.close(mapperContext);
            output = null;
        } finally {
            closeQuietly(input);
            closeQuietly(output, mapperContext);
        }
    }

当初始化过程都进行完成之后,就该读取器的初始化了,首先说一下读取器的作用,内存中存储的切片的信息,也就是从开始位置到结束为止之间是一些列的字节数组,需要指定以什么样的形式去读取,我们这里默认按行读取也就是如果不指定读取器的话,系统默认使用行读取器,下面是相关选择读取器,以及格式化类的源码;

  public static final String INPUT_FORMAT_CLASS_ATTR ="mapreduce.job.inputformat.class";


/**
   * Get the {@link InputFormat} class for the job.
   * 
   * @return the {@link InputFormat} class for the job.
   */
  @SuppressWarnings("unchecked")
  public Class> getInputFormatClass() 
     throws ClassNotFoundException {
    return (Class>) 
      conf.getClass(INPUT_FORMAT_CLASS_ATTR, TextInputFormat.class);
  }

/** 
   * Get the value of the name property as a Class.  
   * If no such property is specified, then defaultValue is 
   * returned.
   * 
   * @param name the class name.
   * @param defaultValue default value.
   * @return property value as a Class, 
   *         or defaultValue. 
   */
  public Class getClass(String name, Class defaultValue) {
    String valueString = getTrimmed(name);
    if (valueString == null)
      return defaultValue;
    try {
        //如果没有指定,则会选择TextInputFormat作为默认格式化类
      return getClassByName(valueString);
    } catch (ClassNotFoundException e) {
      throw new RuntimeException(e);
    }
  }

读取器的选择;

Hadoop整个MR的过程源码解析(二)-map端任务的执行_第1张图片

   NewTrackingRecordReader(org.apache.hadoop.mapreduce.InputSplit split,
                                org.apache.hadoop.mapreduce.InputFormat 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 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);
            //文件读取器
            this.real = inputFormat.createRecordReader(split, taskContext);
            long bytesInCurr = getInputBytes(fsStats);
            fileInputByteCounter.increment(bytesInCurr - bytesInPrev);
        }

Hadoop整个MR的过程源码解析(二)-map端任务的执行_第2张图片

接下来是读取器的初始化,主要完成的任务有;

根据job的信息,获取切片的位置信息;

读取文件,并seek到切片的偏移量的位置

避免hdfs存储层产生将一行数据分割开的情况;

如果切片的偏移量为0,也就是第一个切片的话,会向下多读一行数据,加到自己的流中

如果切片的偏移量不为0,也就是他不是第一个切片的话,会将自己的首行让出,此处的做法是,用一个匿名函数存储第一行的数据,并把偏移量加到切片的start上去,但是并不会对该行数据做任何处理,然后将下一个切片的首行数据读入

最终完成切片的重构,该处所谓的重构就是让首行,向下多读一行

 

/**
   * 获取切片的位置信息,获取job的配置信息,
   * 设置行读取器所读取的行的最大长度,如果没有设置的话,会默认为int的最大值
   * 重要的是接下来的几点
   *    1;获取切片的偏移量,开始,结束的位置,以及所在的文件block的信息
   *    2;读取文件,将文件加载到内存的streaming中,通过seek操作到该切片的起始位置
   *    3;如果该切片不是第一个切片的话也就是切片的开始位置不为0,
   *          会将首行抛弃,从第二行开始读取,然后会将下一个切片的首行读进来
   *    4;最终完成对输入数据的格式化
   *
   *
   * @param genericSplit
   * @param context
   * @throws IOException
   */
  public void initialize(InputSplit genericSplit,
                         TaskAttemptContext context) throws IOException {
    //获取切片的文件信息
    FileSplit split = (FileSplit) genericSplit;
    //加载job的配置信息
    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 {
      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.
    if (start != 0) {
      start += in.readLine(new Text(), 0, maxBytesToConsume(start));
    }
    this.pos = start;
  }

按照方法入口的执行顺序,接下来执行的就是mapper中的run方法;

此处最主要的就是context.nextKeyValue(),底层调用的就是读取器的nextKeyValue()方法

 /**
   * Expert users can override this method for more complete control over the
   * execution of the Mapper.
   *context.nextkeyvalue实际调用的是输入格式化类的nextKeyValue()方法
   *    这个方法有两个作用
   *      1;判断是否还有下一条数据
   *      3;提供getCurrentKey(),getCurrentValue()用来获取被重新赋值之后的k。v
   * @param context
   * @throws IOException
   */
  public void run(Context context) throws IOException, InterruptedException {
    setup(context);
    try {
      while (context.nextKeyValue()) {
        map(context.getCurrentKey(), context.getCurrentValue(), context);
      }
    } finally {
      cleanup(context);
    }
  }

接下来我们需要看一下读取器中的.nextKeyValue()到底进行了什么操作,话不多说,直接上源码;

这段代码的主要内容为以下几点;

判断当前切片是否存在下一条数据,如果存在下一条数据就会对key,value进行赋值,并且将这段数据的长度加到偏移量上,这样下一次读取的时候就会直接从当前偏移量上去获取数据

此处的key,value,就可以使用 context.getCurrentKey(),context.getCurrentValue()来获取了

如果不存在下一条数据,就返回false,key,value=null。

/**
   * 判断是否还有下一条数据
   * 判断当前pos是不是0.也就是判断当前切片是不是第一个切片,如果不是,让首行,读下一个切片的首行
   *  如果是的话,就多读取下一个切片的首行
   *
   * 对key,value进行赋值
   * @return
   * @throws IOException
   */
  public boolean nextKeyValue() throws IOException {
    if (key == null) {
      key = new LongWritable();
    }
    key.set(pos);//这一行相对于切片的偏移量加上切片相对于块的偏移量
    if (value == null) {
      value = new Text();
    }
    int newSize = 0;//行读取器读取的字节数
    // We always read one extra line, which lies outside the upper
    // split limit i.e. (end - 1)
    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));
    }
    if (newSize == 0) {
      key = null;
      value = null;
      return false;
    } else {
      return true;
    }
  }

到此为止map的输入过程所涉及到的代码基本上算是讲完了,下一篇开始将map的输出过程,涉及到需要根据reduce的个数判断选择输出的格式化类,以及环形缓冲区,分区,排序,排序比较器,归并,commbiner等相关概念

你可能感兴趣的:(个人心得)