MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)

前文已经分析了Map Task的输入,这次我们来分析较为复杂的输出,看看Map Task的输出到底做了哪些事情,分析完之后,将会对我们学习MapReduce有很大的帮助

Map Task OutPut源码分析

这里依旧用的Hadoop的版本为2.7.2 ,工具是IDEA

由于上文我们已经有输入的分析,所以,这里直接找到MatpTask的run方法
MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第1张图片
我们直接往下看 ,

 private <INKEY,INVALUE,OUTKEY,OUTVALUE>
  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
    org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE> mapper =
      (org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE>)
        ReflectionUtils.newInstance(taskContext.getMapperClass(), job);
    // make the input format
    org.apache.hadoop.mapreduce.InputFormat<INKEY,INVALUE> inputFormat =
      (org.apache.hadoop.mapreduce.InputFormat<INKEY,INVALUE>)
        ReflectionUtils.newInstance(taskContext.getInputFormatClass(), job);
    // rebuild the input split
    org.apache.hadoop.mapreduce.InputSplit split = null;
    split = getSplitDetails(new Path(splitIndex.getSplitLocation()),
        splitIndex.getStartOffset());
    LOG.info("Processing split: " + split);
	// 这里map的输入上文已经分析的很透彻了。所以我们直接往下看
    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;
      // get an output object
       // 如果reduce的数量为0,map的输出就直接是hdfs了
    if (job.getNumReduceTasks() == 0) {
      output = 
        new NewDirectOutputCollector(taskContext, job, umbilical, reporter);
    } else {
       //我们来分析有reduce的情况下,map是怎么输出的
      output = new NewOutputCollector(taskContext, job, umbilical, reporter);
    }

    org.apache.hadoop.mapreduce.MapContext<INKEY, INVALUE, OUTKEY, OUTVALUE> 
    mapContext = 
      new MapContextImpl<INKEY, INVALUE, OUTKEY, OUTVALUE>(job, getTaskID(), 
          input, output, 
          committer, 
          reporter, split);

    org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE>.Context 
        mapperContext = 
          new WrappedMapper<INKEY, INVALUE, OUTKEY, OUTVALUE>().getMapContext(
              mapContext);

    try {
      input.initialize(split, mapperContext);
      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);
    }
  }

我们进去至少有一到多个reduce的那个方法里面,看一看

 NewOutputCollector(org.apache.hadoop.mapreduce.JobContext jobContext,
                       JobConf job,
                       TaskUmbilicalProtocol umbilical,
                       TaskReporter reporter
                       ) throws IOException, ClassNotFoundException {
       //这个可排序的集合我们先不看,先把下面的给看了
      collector = createSortingCollector(job, reporter);
      // map输出的时候,其实输入的是KVP,我们可以说reduce Task的别名,就是Partition分区
      // 我们通过这个代码也可以看出来
      partitions = jobContext.getNumReduceTasks();
      //reduce分区数大于1
      if (partitions > 1) {
       // 这里,反射创建,一般都是如果用户设置就取用户的,用户没有设置就取框架的默认值,
       // 我们看一下框架默认的分区器是什么
        partitioner = (org.apache.hadoop.mapreduce.Partitioner<K,V>)
          ReflectionUtils.newInstance(jobContext.getPartitionerClass(), job);
      } else {
      // reduce分区数等于1
      //partitioner 分区器 
        partitioner = new org.apache.hadoop.mapreduce.Partitioner<K,V>() {
          @Override
          //获取分区号
          public int getPartition(K key, V value, int numPartitions) {
           // 这里返回是一定是 0 因为进到这个里面,Partitions一定是1.
           // 这里我们可以得知,当我们只有1个reduce的时候,我们输出的任何key,他们的分区号都是0
           // 他们都会在一个reduce里
            return partitions - 1;
          }
        };
      }
    }

我们进去JobContext的实现类JobContextImpl,看一下默认的分区器是什么

public Class<? extends Partitioner<?,?>> getPartitionerClass() 
     throws ClassNotFoundException {
    return (Class<? extends Partitioner<?,?>>) 
    	//如果用户配置了分区器,就取用户的,如果用户没有设置,就取HashPartitioner,哈希
    	// 我们进去看一下HashPartitioner是怎么分区的
      conf.getClass(PARTITIONER_CLASS_ATTR, HashPartitioner.class);
  }
 public int getPartition(K key, V value,
                          int numReduceTasks) {
        // key的哈希值与上integer的最大值,这一步就是为了得到一个正整数
        // 然后取模 如果是 reduce数量是3 ,那么取模就是 0 1 2                  
    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
  }

我们知道,hash算法属于映射的,如果我们想要我们的数据均匀的散列,那么我们就可以使用hash算法,如果我们要保证我们数据的原有特征,这个时候用hash就不合适了,这个时候我们可以自定义分区器,如果我们自定义分区器,框架就不会取默认的hash,而是我们自己写的。

这里我们看完了,我们先出去看一下这个分区器在哪里被用的,我们的这个 NewOutputCollector方法就是输出的,我们看一看它的方法

MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第2张图片
我们看见了一个write,见名知意,我们context调用的write方法其实是NewOutputCollector的write方法,我们进去看看

 public void write(K key, V value) throws IOException, InterruptedException {
  //调用collect方法,往容器里面放东西,把我们的key,value,通过我们写的分区器获得我们的分区号
  // 都放到里面,最终,map调用write传的是kv,但是在里面会转换成kvp,然后往collector容器里面放
      collector.collect(key, value,
                        partitioner.getPartition(key, value, partitions));
    }

我们现在往前看这个collector容器

  NewOutputCollector(org.apache.hadoop.mapreduce.JobContext jobContext,
                       JobConf job,
                       TaskUmbilicalProtocol umbilical,
                       TaskReporter reporter
                       ) throws IOException, ClassNotFoundException {
       //现在我们进到这个createSortingCollector方法里面看看
      collector = createSortingCollector(job, reporter);
      ......
     } 
private <KEY, VALUE> MapOutputCollector<KEY, VALUE>
          createSortingCollector(JobConf job, TaskReporter reporter)
    throws IOException, ClassNotFoundException {
    MapOutputCollector.Context context =
      new MapOutputCollector.Context(this, job, reporter);
	//我们看到job.getClasses,就是往配置文件里面取,如果我们配置了,就取我们配置的
	// 如果我们没有配置,就是默认的MapOutputBuffer,map输出缓冲区。
    Class<?>[] collectorClasses = job.getClasses(
      JobContext.MAP_OUTPUT_COLLECTOR_CLASS_ATTR, MapOutputBuffer.class);
    int remainingCollectors = collectorClasses.length;
    Exception lastException = null;
    //迭代这个输出缓冲区
    for (Class clazz : collectorClasses) {
      try {
        if (!MapOutputCollector.class.isAssignableFrom(clazz)) {
          throw new IOException("Invalid output collector class: " + clazz.getName() +
            " (does not implement MapOutputCollector)");
        }
        Class<? extends MapOutputCollector> subclazz =
          clazz.asSubclass(MapOutputCollector.class);
        LOG.debug("Trying map output collector class: " + subclazz.getName());
        //输出缓冲区赋到我们的这个容器里
        MapOutputCollector<KEY, VALUE> collector =
          ReflectionUtils.newInstance(subclazz, job);
          // 输出初始化,,,,我们在输入的时候有输入初始化,这里是输出初始化
          // 我们看一下容器的初始化,看collector的初始化就是MapOutputBuffer的初始化
        collector.init(context);
        LOG.info("Map output collector class = " + collector.getClass().getName());
        // 最终返回这个容器
        return collector;
      } catch (Exception e) {
        String msg = "Unable to initialize MapOutputCollector " + clazz.getName();
        if (--remainingCollectors > 0) {
          msg += " (" + remainingCollectors + " more collector(s) to try)";
        }
        lastException = e;
        LOG.warn(msg, e);
      }
    }
    throw new IOException("Initialization of all the collectors failed. " +
      "Error in last collector was :" + lastException.getMessage(), lastException);
  }

我们要进去 collector.init方法,看一下MapOutputBuffer的初始化,选择MapOutputBuffer的init方法

public void init(MapOutputCollector.Context context
                    ) throws IOException, ClassNotFoundException {
      job = context.getJobConf();
      reporter = context.getReporter();
      mapTask = context.getMapTask();
      mapOutputFile = mapTask.getMapOutputFile();
      sortPhase = mapTask.getSortPhase();
      spilledRecordsCounter = reporter.getCounter(TaskCounter.SPILLED_RECORDS);
      partitions = job.getNumReduceTasks();
      rfs = ((LocalFileSystem)FileSystem.getLocal(job)).getRaw();

      //sanity checks
      // 在这里,定义了一个float类型的溢写的阈值百分比
      final float spillper =
      	// 在这里,job从配置文件当中取,
      	//如果用户配置了,就取用户的,如果没有,就是默认的0.8,百分之80的意思
        job.getFloat(JobContext.MAP_SORT_SPILL_PERCENT, (float)0.8);
        //定义了一个sortmb,从配置文件中去取,如果用户配置了就取用户的,如果没有就是默认的100Mb
		// 这里解释一下,map完事之后,会放到一个内存缓冲区里面,这个100Mb就是这个内存缓冲区的大小
		//上边那个0.8的阈值,说的就是只要使用到百分之80了,就可以做溢写,写到磁盘去。
	// 这个100Mb要根据具体情况去分析,如果kv很大,这个100Mb就不合适,用户可以适当的调大
	
	//这里要百分之80的阈值做溢写的原因是:如果没有这百分之80做溢写这个事情
	//  整个100M来做缓冲区,map一直输出把缓冲区给占满,没有空闲了,这个时候map会阻塞,就不能往后写了
	// 会把空间给锁住,锁完之后开始做溢写,写完磁盘把空间腾出来之后,才可以继续往里写,这是个线性的时间
	// 如果我们做了这百分之80的阈值,map输出向这百分之80写,写满了,还有百分之20,
	// 这个时候,map向剩下的百分之20去写的同时,这百分之80做溢写,这个会有一部分时间轴重叠了
	// 会压缩我们的时间,这就是为什么要80做阈值。当然我们可以根据数据来做调整。
      final int sortmb = job.getInt(JobContext.IO_SORT_MB, 100);
      indexCacheMemoryLimit = job.getInt(JobContext.INDEX_CACHE_MEMORY_LIMIT,
                                         INDEX_CACHE_MEMORY_LIMIT_DEFAULT);
      if (spillper > (float)1.0 || spillper <= (float)0.0) {
        throw new IOException("Invalid \"" + JobContext.MAP_SORT_SPILL_PERCENT +
            "\": " + spillper);
      }
      if ((sortmb & 0x7FF) != sortmb) {
        throw new IOException(
            "Invalid \"" + JobContext.IO_SORT_MB + "\": " + sortmb);
      }
      sorter = ReflectionUtils.newInstance(job.getClass("map.sort.class",
            QuickSort.class, IndexedSorter.class), job);
      // buffers and accounting
      int maxMemUsage = sortmb << 20;
      maxMemUsage -= maxMemUsage % METASIZE;
      kvbuffer = new byte[maxMemUsage];
      bufvoid = kvbuffer.length;
      kvmeta = ByteBuffer.wrap(kvbuffer)
         .order(ByteOrder.nativeOrder())
         .asIntBuffer();
      setEquator(0);
      bufstart = bufend = bufindex = equator;
      kvstart = kvend = kvindex;

      maxRec = kvmeta.capacity() / NMETA;
      softLimit = (int)(kvbuffer.length * spillper);
      bufferRemaining = softLimit;
      LOG.info(JobContext.IO_SORT_MB + ": " + sortmb);
      LOG.info("soft limit at " + softLimit);
      LOG.info("bufstart = " + bufstart + "; bufvoid = " + bufvoid);
      LOG.info("kvstart = " + kvstart + "; length = " + maxRec);

      // k/v serialization
      comparator = job.getOutputKeyComparator();
      keyClass = (Class<K>)job.getMapOutputKeyClass();
      valClass = (Class<V>)job.getMapOutputValueClass();
      serializationFactory = new SerializationFactory(job);
      keySerializer = serializationFactory.getSerializer(keyClass);
      keySerializer.open(bb);
      valSerializer = serializationFactory.getSerializer(valClass);
      valSerializer.open(bb);

      // output counters
      mapOutputByteCounter = reporter.getCounter(TaskCounter.MAP_OUTPUT_BYTES);
      mapOutputRecordCounter =
        reporter.getCounter(TaskCounter.MAP_OUTPUT_RECORDS);
      fileOutputByteCounter = reporter
          .getCounter(TaskCounter.MAP_OUTPUT_MATERIALIZED_BYTES);

      // compression
      if (job.getCompressMapOutput()) {
        Class<? extends CompressionCodec> codecClass =
          job.getMapOutputCompressorClass(DefaultCodec.class);
        codec = ReflectionUtils.newInstance(codecClass, job);
      } else {
        codec = null;
      }

      // combiner
      final Counters.Counter combineInputCounter =
        reporter.getCounter(TaskCounter.COMBINE_INPUT_RECORDS);
      combinerRunner = CombinerRunner.create(job, getTaskID(), 
                                             combineInputCounter,
                                             reporter, null);
      if (combinerRunner != null) {
        final Counters.Counter combineOutputCounter =
          reporter.getCounter(TaskCounter.COMBINE_OUTPUT_RECORDS);
        combineCollector= new CombineOutputCollector<K,V>(combineOutputCounter, reporter, job);
      } else {
        combineCollector = null;
      }
      spillInProgress = false;
      minSpillsForCombine = job.getInt(JobContext.MAP_COMBINE_MIN_SPILLS, 3);
      spillThread.setDaemon(true);
      spillThread.setName("SpillThread");
      spillLock.lock();
      try {
        spillThread.start();
        while (!spillThreadRunning) {
          spillDone.await();
        }
      } catch (InterruptedException e) {
        throw new IOException("Spill thread failed to initialize", e);
      } finally {
        spillLock.unlock();
      }
      if (sortSpillException != null) {
        throw new IOException("Spill thread failed to initialize",
            sortSpillException);
      }
    }

在这里,我觉得很有必要来深入了解MRShuffle过程的缓冲区,这里先介绍一下缓冲区,明白了之后,在来做源码的分析

缓冲区介绍(以及环形缓冲区源码分析)

我们通过上面整个源码的分析,也清楚的知道了MapOutputBuffer内部使用了一个环形缓冲区来暂时存储用户输出的数据,当我们的缓冲区达到框架默认的百分之80阈值之后 ,才会将我们缓冲区内的数据写到磁盘,而缓冲区的设计也会直接影响到我们Map Task的输出效率,我们先来了解一下都有什么样的缓冲区

  1. 单向缓冲区,我们的输出向缓冲区中单向的写入,当缓冲区写满后,一次性写到磁盘上,就一直这样,不断的写缓冲区,直到我们输出的数据全部写到磁盘上,但是单向缓冲区最大的缺点就是,性能不高,而且不能支持同时读写数据

MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第3张图片

  1. 双向缓冲区,是对单向缓冲区的一个改进,它使用了两个缓冲区,一个用于写数据,另一个将写满的数据写到磁盘上,这样,两个缓冲区就能交替读写,从而提升了效率,不过,双向缓冲区只能一定程度上让读写并行,仍然会存在读写等待的问题
    MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第4张图片

  2. 环形缓存区,是比双向缓冲区更好的,当缓冲区的使用率达到一定的阈值之后,遍开始像磁盘写入数据,同时,我们还可以继续向不断增加的剩余空间继续写入数据,进而达到了真正的读写并行。
    MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第5张图片

我们MapReduce的MapOutputBuffer正是采用了环形内存缓冲区来保存数据,(其实我们的hadoop也曾经才用过双向缓冲区) 由线程SpillThread将数据写到一个临时文件中,当所有数据处理完毕后,在对所有临时文件进行一次合并以生成一个最终文件。这样使用环形缓冲区就使得Map Task的Collect阶段和Spill阶段可并行进行了(Spill即为溢写操作)

我们深入的介绍一下MapOutputBuffer采用的环形缓冲区,环形缓冲区的内部结构采用了两级索引,涉及了三个环形内存缓冲区,分别是kvoffsets、kvindices和kvbuffer,这三个环形缓冲区总共占的内存空间大小为100M(框架默认的情况,在上面的源码中我们也看了),接下来我们分别介绍一下这三个环形缓冲区的含义
MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第6张图片

  1. kvoffsets
    看见这个名字,我们就知道,它就是我们key/value的偏移量索引数组,用于保存我们的key/value信息在位置索引kvindices中的偏移量,考虑到一对key/value需占用数组kvoffsets的1个int(整型)大小,数组kvindices的3个int大小,(分别保存所在partition号、key开始位置和value开始位置),所以Hadoop按比例1 : 3将大小为{io.sort.record.percent}(默认为0.8)*{io.sort.mb}(默认为100)的内存空间分配给数组kvoffsets和kvindices,计算公式这里就不详细看了,,结果就是kvbuffer占95%,kvoffsets占3.75%,kvindices1.25%, 当该数组使用率超过io.sort.spill.percent(80%)后,便会触发线程SpillThread将数据写入磁盘。

  2. kvindices
    kvindices即位置索引数组,用于保存key/value值在数据缓冲区kvbuffer中的起始位置。

  3. kvbuffer
    kvbuffer则是数据缓冲区,用于保存真正的key/value的值,默认情况下最多可使用io.sort. mb中的95%,当使用该缓冲区使用率超过io.sort.spill.percent(80%)后,便会触发线程SpillThread将数据写入磁盘。

    下面我们则说一下环形缓冲区kvoffsets和kvbuffer的数据写入的过程
    

(1)环形缓冲区kvoffsets

这个缓冲区通常用一个线性的缓冲区模拟实现环形缓冲区,并通过取模操作实现循环数据存储。下面介绍环形缓冲区kvoffsets的写数据过程。这个写入的过程由指针kvstart/kvend/kvindex来进行控制,其中kvstart表示存有数据的内存段初始位置,kvindex则表示未存储数据的内存段初始位置,而在正常写入的情况下,kvend=kvstart,一旦满足了溢写条件(80%),则kvend=kvindex,此时指针区间[kvstart,kvend)则为有效数据区间。具体的操作如下

操作1:写入缓冲区

直接将数据写入kvindex指针指向的内存空间,同时移动kvindex指向下一个可写入的内存空间首地址,kvindex移动公式为:kvindex=(kvindex+1)%kvoffsets.length。由于kvoffsets为环形缓冲区,因此可能涉及两种写入情况。

情况(1):kvindex > kvend,在这种情况下,指针kvindex在指针kvend后面,如果向缓冲区中写入一个字符串,则kvindex指针后移一位。

MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第7张图片
情况(2):kvindex <= kvend,如图8-11所示。在这种情况下,指针kvindex位于指针kvend前面,如果向缓冲区中写入一个字符串,则kvindex指针后移一位。
MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第8张图片
操作2:溢写到磁盘的时候。当kvoffsets内存空间使用率超过io.sort.spill.percent(默认是80%)后,需将内存中数据写到磁盘上。为了判断是否满足该条件,需先求出kvoffsets已使用内存。如果kvindex>kvend,则已使用内存大小为kvindex-kvend;否则,已使用内存大小为kvoffsets. length-(kvend-kvindex)。

(2)环形缓冲区kvbuffer

环形缓冲区kvbuffer的读写操作过程由指针bufstart/bufend/bufvoid/bufindex/bufmark控制,其中,bufstart/bufend/bufindex含义与kvstart/kvend/kvindex相同,而bufvoid指向kvbuffer中有效内存结束,bufmark表示最后写入的一个完整key/value结束位置

情况(1):初始状态。初始状态下,bufstart=bufend=bufindex=bufmark=0,bufvoid=kvbuffer.length
MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第9张图片
情况(2):写入一个key。写入一个key后,需移动bufindex指针到可写入内存初始位置
MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第10张图片
情况(3):写入一个value。写入key对应的value后,除移动bufindex指针外,还要移动bufmark指针,表示已经写入一个完整的key/value
MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第11张图片
情况(4):不断写入key/value,直到满足溢写条件,即kvoffsets或者kvbuffer空间使用率超过io.sort.spill.percent(默认值为80%)。此时需要将数据写到磁盘上
MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第12张图片

情况(5):溢写。如果达到溢写条件,则让bufend=bufindex,并将缓冲区[bufstart,bufend)之间的数据写到磁盘上
MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第13张图片

溢写完成之后,恢复正常写入状态,让bufstart=bufend
MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第14张图片
.在溢写的同时,Map Task仍可向kvbuffer中写入数据
MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第15张图片

情况(6):写入key时,发生跨界现象。当写入某个key时,缓冲区尾部剩余空间不足以容纳整个key值,此时需要将key值分开存储,其中一部分存到缓冲区末尾,另外一部分存到缓冲区首部
MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第16张图片
情况(7):调整key位置,防止key跨界现象。由于key是排序的关键字,通常需交给RawComparator进行排序,而它要求排序关键字必须在内存中连续存储,因此不允许key跨界存储。为解决该问题,Hadoop将跨界的key值重新存储到缓冲区的首位置。通常可分为以下两种情况

  • bufindex+(bufvoid - bufmark)

System.arraycopy()
src:源数组;
srcPos:源数组要复制的起始位置;
dest:目的数组;
destPos:目的数组放置的起始位置;
length:复制的长度。

int headbytelen = bufvoid - bufmark;
System.arraycopy(kvbuffer,0,kvbuffer,headbytelen,bufindex);
System.arraycopy(kvbuffer,bufvoid,kvbuffer,0,headbytelen);
  • bufindex+(bufvoid - bufmark)>=bufstart:此时缓冲区前半段没有足够的空间容纳整个key值,将key值移到缓冲区开始位置时将触发一次Spill操作。这种情况下,可通过三次内存复制解决跨行问题
 		//申请一个临时缓冲区
 		 byte[] keytmp = new byte[bufindex];
          System.arraycopy(kvbuffer, 0, keytmp, 0, bufindex);
          bufindex = 0;
          //将key值写入缓冲区开始位置
          out.write(kvbuffer, bufmark, headbytelen);
          out.write(keytmp);

情况(8):某个key或者value太大,以至于整个缓冲区不能容纳它。如果一条记录的key或value太大,整个缓冲区都不能容纳它,则MapTask会抛出MapBufferTooSmallException异常,并将该记录单独输出到一个文件中。

当然在从hadoop0.21开始,有两个修改点,

  • 不再将索引和记录分放到不同的环形缓冲区中,而是让它们共用一个环形缓冲区。
  • 引入一个新的指针equator。该指针界定了索引和数据的共同起始存放位置。从该位置开始,索引和数据分别沿相反的方向增长内存使用空间。

通过让索引和记录共享一个环形缓冲区,使得Map Task能够最大限度地利用io.sort.mb空间,进而减少磁盘溢写次数,提高效率。
MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)_第17张图片
这里的缓冲区就很简单了,我相信只要最开始的环形缓冲区能看懂,那么这个从0.21版本开始的优化后的缓冲区大家也能看懂,相对于之前已经变的没有那么麻烦了。

到这里,我们的缓冲区就已经介绍完了,相信也都应该有很深入的了解,接下来我们看一看环形缓冲区的源码分析

 public static class MapOutputBuffer<K extends Object, V extends Object>
      implements MapOutputCollector<K, V>, IndexedSortable {
      // 具体的分区号
    private int partitions;
    private JobConf job;
    private TaskReporter reporter;
    //map输出端的key类型
    private Class<K> keyClass;
    //map输出端的value类型
    private Class<V> valClass;
    //默认行比较器
    private RawComparator<K> comparator;
    // 序列化工厂对象,,,因为保存在环形缓冲区的数据,kv会序列化为字节数组,meta也会序列化
    private SerializationFactory serializationFactory;
    //用来序列号key值,存储到buffer里
    private Serializer<K> keySerializer;
     //用来序列化value值,存储到buffer里
    private Serializer<V> valSerializer;
    //Combiner的运行定义
    private CombinerRunner<K,V> combinerRunner;
	//Combine输出集合
    private CombineOutputCollector<K, V> combineCollector;

    // Compression for map-outputs
    // 默认map输出的压缩器
    private CompressionCodec codec;
	
    // k/v accounting
    //保存元数据的buffer包装成int数组 占4byte
    private IntBuffer kvmeta; // metadata overlay on backing store
    //保存元数据的内存段初始位置 (第一次的位置应该是倒数第四个int元素位置)
    int kvstart;            // marks origin of spill metadata
    //保存元数据在(内存)int数组的结束位置
    int kvend;              // marks end of spill metadata
    //未存储数据的内存段初始位置(存有下一个元数据的起始位置)
    int kvindex;            // marks end of fully serialized records
	//分割meta和key value内容的位置
    int equator;            // marks origin of meta/serialization
    //保存kv对的起始位置
    int bufstart;           // marks beginning of spill
    //保存kv对的结束位置
    int bufend;             // marks beginning of collectable
    //保存kv对的结束位置
    int bufmark;            // marks end of record
    //未存储数据的内存段初始位置(保存下一个kv的起始位置)
    int bufindex;           // marks end of collected
    //buffer缓冲区的总长度
    int bufvoid;            // marks the point where we should stop
                            // reading at the end of the buffer
	//环形缓冲区
    byte[] kvbuffer;        // main output buffer
    //校验是否到边界的数组
    private final byte[] b0 = new byte[0];
	// 一个元数据数组由4个部分组成,在上面的图中也都已经解释过了
	// valstart相对于kvindex的偏移量
    private static final int VALSTART = 0;         // val offset in acct
    // keystart元数据相对于kvindex的偏移量
    private static final int KEYSTART = 1;         // key offset in acct
    // parition元数据相对于kvindex的偏移量
    private static final int PARTITION = 2;        // partition offset in acct
    //vallen元数据相对于kvindex的偏移量
    private static final int VALLEN = 3;           // length of value
    // 一对key value的meta数据在kvmeta中占用的字节数 4 
    private static final int NMETA = 4;            // num meta ints
    // 算出元数据数组的总占字节为16字节 4 * 4 = 16b
    private static final int METASIZE = NMETA * 4; // size in bytes

    // spill accounting
    // 为溢写操作准备的东西
    private int maxRec;
    private int softLimit;
    boolean spillInProgress;;
    // 检查buffer是否达到阈值
    int bufferRemaining;
    volatile Throwable sortSpillException = null;

    int numSpills = 0;
    private int minSpillsForCombine;
    private IndexedSorter sorter;
    final ReentrantLock spillLock = new ReentrantLock();
    final Condition spillDone = spillLock.newCondition();
    final Condition spillReady = spillLock.newCondition();
    final BlockingBuffer bb = new BlockingBuffer();
    volatile boolean spillThreadRunning = false;
    //溢写线程
    final SpillThread spillThread = new SpillThread();
    //溢写的文件系统
    private FileSystem rfs;

key/value序列化的数据和元数据在环形缓冲区中的存储是由equator分隔的,key/value按照索引递增的方向存储,meta则按照索引递减的方向存储,将其数组抽象为一个环形结构之后,以equator为界,key/value顺时针存储,meta逆时针存储。

接下来我们看一下环形缓冲区的初始化操作

public void init(MapOutputCollector.Context context
                    ) throws IOException, ClassNotFoundException {
       //通过上下文获取job对象
      job = context.getJobConf();
      reporter = context.getReporter();
      //返回我们的 maptask
      mapTask = context.getMapTask();
      //这个对象代表我们map输出临时文件的位置
      mapOutputFile = mapTask.getMapOutputFile();
      //设置我们的排序阶段
      sortPhase = mapTask.getSortPhase();
      spilledRecordsCounter = reporter.getCounter(TaskCounter.SPILLED_RECORDS);
      //获取分区个数
      partitions = job.getNumReduceTasks();
      //溢写文件或最后的合并文件存储到本地(默认是存储到本地磁盘)
      rfs = ((LocalFileSystem)FileSystem.getLocal(job)).getRaw();

      //sanity checks
      //获取我们溢写的阈值(默认百分之80)
      final float spillper =
        job.getFloat(JobContext.MAP_SORT_SPILL_PERCENT, (float)0.8);
        //获取我们默认的环形缓冲区大小(默认100mb)
      final int sortmb = job.getInt(JobContext.IO_SORT_MB, 100);
      // 所有的spill index 在内存所占的大小的阈值
      indexCacheMemoryLimit = job.getInt(JobContext.INDEX_CACHE_MEMORY_LIMIT,
                                         INDEX_CACHE_MEMORY_LIMIT_DEFAULT);
      if (spillper > (float)1.0 || spillper <= (float)0.0) {
        throw new IOException("Invalid \"" + JobContext.MAP_SORT_SPILL_PERCENT +
            "\": " + spillper);
      }
      if ((sortmb & 0x7FF) != sortmb) {
        throw new IOException(
            "Invalid \"" + JobContext.IO_SORT_MB + "\": " + sortmb);
      }
      // 排序的实现类,这里可以自己实现,框架默认使用快排
      sorter = ReflectionUtils.newInstance(job.getClass("map.sort.class",
            QuickSort.class, IndexedSorter.class), job);
      // buffers and accounting
      //将缓冲区的内存单位转换为字节
      int maxMemUsage = sortmb << 20;
      // METASIZE是我们元数据的长度,元数据存储的数据由4个部分组成,分别为
 	 // VALSTART、KEYSTART、PARTITION、VALLEN,每个占到4个字节,所以METASIZE长度为16b
 	 // 下面则是计算buffer中最多有多少byte来存元数据
      maxMemUsage -= maxMemUsage % METASIZE;
      //创建kv环形缓冲区
      kvbuffer = new byte[maxMemUsage];
      //初始bufvoid = kvbuffer的长度 
      bufvoid = kvbuffer.length;
      //将kvbuffer转化为int型的kvmeta  以int为单位,也就是4byte
      kvmeta = ByteBuffer.wrap(kvbuffer)
         .order(ByteOrder.nativeOrder())
         .asIntBuffer();
         //设置初始buf和kvmeta的分界线
      setEquator(0);
      //给kvindex进行赋值
      bufstart = bufend = bufindex = equator;
      kvstart = kvend = kvindex;
	  //kvmeta中存放元数据实体的最大个数
      maxRec = kvmeta.capacity() / NMETA;
      //buffer spill时的阈值 (kvbuffer.length*spiller)
      softLimit = (int)(kvbuffer.length * spillper);
      //bufferRemaining这个变量作为衡量是否到达阈值
      bufferRemaining = softLimit;
      LOG.info(JobContext.IO_SORT_MB + ": " + sortmb);
      LOG.info("soft limit at " + softLimit);
      LOG.info("bufstart = " + bufstart + "; bufvoid = " + bufvoid);
      LOG.info("kvstart = " + kvstart + "; length = " + maxRec);
	
      // k/v serialization
      //kv序列化的工作
      //获取比较器
      comparator = job.getOutputKeyComparator();
      //获取map输出端的key类型,用户设置取用户的,用户没有设置默认是Object类型
      keyClass = (Class<K>)job.getMapOutputKeyClass();
      //获取我们map输出端的value类型,用户设置取用户的,用户没有设置默认是Object类型
      valClass = (Class<V>)job.getMapOutputValueClass();
      //为序列化工厂赋值
      serializationFactory = new SerializationFactory(job);
      //获取key的序列化
      keySerializer = serializationFactory.getSerializer(keyClass);
      //bb跟踪源码得知是一个new的普通buffer
      //将bb作为key序列化写入的output
      keySerializer.open(bb);
      //获取value的序列化
      valSerializer = serializationFactory.getSerializer(valClass);
      //bb跟踪源码得知是一个new的普通buffer
      //将bb作为value序列化写入的output
      valSerializer.open(bb);
	
      // output counters
      //获取map输出计数器
      mapOutputByteCounter = reporter.getCounter(TaskCounter.MAP_OUTPUT_BYTES);
      mapOutputRecordCounter =
        reporter.getCounter(TaskCounter.MAP_OUTPUT_RECORDS);
      fileOutputByteCounter = reporter
          .getCounter(TaskCounter.MAP_OUTPUT_MATERIALIZED_BYTES);

      // compression
      //判断用户是否设置了压缩格式
      // 获取默认压缩格式,默认是基于Zlib算法的压缩器 DefaultCodec
     // 一旦启用了压缩机制,Hadoop会为每条记录的key和value值进行压缩
      if (job.getCompressMapOutput()) {
        Class<? extends CompressionCodec> codecClass =
          job.getMapOutputCompressorClass(DefaultCodec.class);
        codec = ReflectionUtils.newInstance(codecClass, job);
      } else {
      	//没有设置则没有压缩
        codec = null;
      }

      // combiner
      //当所有数据处理完成后,Map Task对所有临时文件进行一次合并,以确保最终只会生成一个数据文件
      final Counters.Counter combineInputCounter =
        reporter.getCounter(TaskCounter.COMBINE_INPUT_RECORDS);
      combinerRunner = CombinerRunner.create(job, getTaskID(), 
                                             combineInputCounter,
                                             reporter, null);
       //判断是否开启combiner     
      if (combinerRunner != null) {
        final Counters.Counter combineOutputCounter =
          reporter.getCounter(TaskCounter.COMBINE_OUTPUT_RECORDS);
        combineCollector= new CombineOutputCollector<K,V>(combineOutputCounter, reporter, job);
      } else {
        combineCollector = null;
      }
      spillInProgress = false;
      minSpillsForCombine = job.getInt(JobContext.MAP_COMBINE_MIN_SPILLS, 3);
      spillThread.setDaemon(true);
      //设置线程名字
      spillThread.setName("SpillThread");
      spillLock.lock();
      //将数据写入缓冲区
      try {
        //启动溢写线程
        spillThread.start();
        //进行轮询的方式询问
        while (!spillThreadRunning) {
         //等待spillThread线程结束
          spillDone.await();
        }
      } catch (InterruptedException e) {
        throw new IOException("Spill thread failed to initialize", e);
      } finally {
        spillLock.unlock();    
      }
      if (sortSpillException != null) {
        throw new IOException("Spill thread failed to initialize",
            sortSpillException);
      }
    }

下面我们看一下写入buffer的操作


//这个collect的操作就是序列化kv到缓冲区中

 public synchronized void collect(K key, V value, final int partition
                                     ) throws IOException {
      reporter.progress();
      //着里判断序列化类型的,我们先不看
      if (key.getClass() != keyClass) {
        throw new IOException("Type mismatch in key from map: expected "
                              + keyClass.getName() + ", received "
                              + key.getClass().getName());
      }
      if (value.getClass() != valClass) {
        throw new IOException("Type mismatch in value from map: expected "
                              + valClass.getName() + ", received "
                              + value.getClass().getName());
      }
      if (partition < 0 || partition >= partitions) {
        throw new IOException("Illegal partition for " + key + " (" +
            partition + ")");
      }
      //校验溢写有没有异常发出
      checkSpillException();
      //新数据collect时,先将剩余的空间减去元数据的长度,之后进行判断
      bufferRemaining -= METASIZE;
      if (bufferRemaining <= 0) { 
        // start spill if the thread is not running and the soft limit has been
        // reached
        // 如果到达溢写的条件,那么就先溢写
        spillLock.lock();
        try {
          do {
            //我们跟踪源码 首次溢写时,spillInProgress是false
            if (!spillInProgress) {
              // 得到kvindex的byte位置(因为是int,所以需 * 4)
              final int kvbidx = 4 * kvindex;
              // 得到kvend的byte位置 (因为是int,所以需 * 4)
              final int kvbend = 4 * kvend;
              // serialized, unspilled bytes always lie between kvindex and
              // bufindex, crossing the equator. Note that any void space
              // created by a reset must be included in "used" bytes
              
              // 开始序列化,没有溢出的字节的位置必须位于kvindex和bufindex
              // 元数据的信息必须写在equator指针后面,逆着来写
              final int bUsed = distanceTo(kvbidx, bufindex);
              final boolean bufsoftlimit = bUsed >= softLimit;
              if ((kvbend + METASIZE) % kvbuffer.length !=
                  equator - (equator % METASIZE)) {
                // spill finished, reclaim space
                resetSpill();
                bufferRemaining = Math.min(
                    distanceTo(bufindex, kvbidx) - 2 * METASIZE,
                    softLimit - bUsed) - METASIZE;
                continue;
              } else if (bufsoftlimit && kvindex != kvend) {
                // spill records, if any collected; check latter, as it may
                // be possible for metadata alignment to hit spill pcnt
                startSpill();
                final int avgRec = (int)
                  (mapOutputByteCounter.getCounter() /
                  mapOutputRecordCounter.getCounter());
                // leave at least half the split buffer for serialization data
                // ensure that kvindex >= bufindex
                final int distkvi = distanceTo(bufindex, kvbidx);
                final int newPos = (bufindex +
                  Math.max(2 * METASIZE - 1,
                          Math.min(distkvi / 2,
                                   distkvi / (METASIZE + avgRec) * METASIZE)))
                  % kvbuffer.length;
                setEquator(newPos);
                bufmark = bufindex = newPos;
                final int serBound = 4 * kvend;
                // bytes remaining before the lock must be held and limits
                // checked is the minimum of three arcs: the metadata space, the
                // serialization space, and the soft limit
                bufferRemaining = Math.min(
                    // metadata max
                    distanceTo(bufend, newPos),
                    Math.min(
                      // serialization max
                      distanceTo(newPos, serBound),
                      // soft limit
                      softLimit)) - 2 * METASIZE;
              }
            }
          } while (false);
        } finally {
          spillLock.unlock();
        }
      }

      try {
        // 不溢写,将key value 及元数据信息写入缓冲区
        // serialize key bytes into buffer
        int keystart = bufindex;
        //将key序列化,并且写入到kvbuffer中,并且移动bufindex
        keySerializer.serialize(key);
        if (bufindex < keystart) {
          //如果key所占空间被bufvoid分隔的化,那么就移动key
          // 并且要将它的值放在连续的空间内,以便于sort时候key的比较
          // wrapped the key; must make contiguous  
          bb.shiftBufferedKey();
          keystart = 0;
        }
        // serialize value bytes into buffer
        final int valstart = bufindex;
        //将value序列化,并且写入kvbuffer中,并且移动bufindex的值
        valSerializer.serialize(value);
        // It's possible for records to have zero length, i.e. the serializer
        // will perform no writes. To ensure that the boundary conditions are
        // checked and that the kvindex invariant is maintained, perform a
        // zero-length write into the buffer. The logic monitoring this could be
        // moved into collect, but this is cleaner and inexpensive. For now, it
        // is acceptable.
        bb.write(b0, 0, 0);

        // the record must be marked after the preceding write, as the metadata
        // for this record are not yet written
        //对kvindex进行标记为元数据
        int valend = bb.markRecord();
		//获取计数器
        mapOutputRecordCounter.increment(1);
        //kv对的字节长度进行计数
        mapOutputByteCounter.increment(
            distanceTo(keystart, valend, bufvoid));

        // write accounting info
        //往我们的kv元数据里面放入partition
        kvmeta.put(kvindex + PARTITION, partition);
		 //往我们的kv元数据里面放入keystart
        kvmeta.put(kvindex + KEYSTART, keystart);
         //往我们的kv元数据里面放入valstart
        kvmeta.put(kvindex + VALSTART, valstart);
         //往我们的kv元数据里面放入VALLEN
        kvmeta.put(kvindex + VALLEN, distanceTo(valstart, valend));
        // advance kvindex
        // // 移动kvindex,为下一个kvmeta做准备,移动了int数组的四个位置
        kvindex = (kvindex - NMETA + kvmeta.capacity()) % kvmeta.capacity();
      } catch (MapBufferTooSmallException e) {
        LOG.info("Record too large for in-memory buffer: " + e.getMessage());
        // 当我们的kv值较大的时候(针对于80MB),会直接溢写到磁盘
        spillSingleRecord(key, value, partition);
        //计数
        mapOutputRecordCounter.increment(1);
        return;
      }
    }
    

然后执行write方法将key/value写入kvbuffer中就行,具体细节这里不多做展开,我们直接看一下最后溢写的flush方法的核心方法sortAndSpill

 private void sortAndSpill() throws IOException, ClassNotFoundException,
                                       InterruptedException {
      //approximate the length of the output file to be the length of the
      //buffer + header lengths for the partitions
      final long size = distanceTo(bufstart, bufend, bufvoid) +
                  partitions * APPROX_HEADER_LENGTH;
      FSDataOutputStream out = null;
      try {
        // create spill file
        // 根据分区创建溢写文件
        final SpillRecord spillRec = new SpillRecord(partitions);
        final Path filename =
            mapOutputFile.getSpillFileForWrite(numSpills, size);
        //打开文件输出流
        out = rfs.create(filename);
		//当前位置能存放多少个元数据  /4
        final int mstart = kvend / NMETA;
        // 元数据在mstart和mend之间,(mstart - mend)则是元数据的个数
        final int mend = 1 + // kvend is a valid record
          (kvstart >= kvend
          ? kvstart
          : kvmeta.capacity() + kvstart) / NMETA;
       // 排序  只对元数据进行排序,只调整元数据在kvmeta中的顺序
      // 排序规则是MapOutputBuffer.compare, 
     // 先按照分区编号partition进行排序,然后按照key进行排序。这样
     // 经过排序后,数据以分区为单位聚集在一	起,且同一分区内所有数据按照key有序。
        sorter.sort(MapOutputBuffer.this, mstart, mend, reporter);
        int spindex = mstart;
        // 存放该分区在数据文件中的信息
        final IndexRecord rec = new IndexRecord();
        //创建临时文件,,,,hadoop默认的临时文件为IFile格式
        final InMemValBytes value = new InMemValBytes();
        for (int i = 0; i < partitions; ++i) {
          IFile.Writer<K, V> writer = null;
          try {
            long segmentStart = out.getPos();
            FSDataOutputStream partitionOut = CryptoUtils.wrapIfNecessary(job, out);
            writer = new Writer<K, V>(job, partitionOut, keyClass, valClass, codec,
                                      spilledRecordsCounter);
               // 往磁盘写入前先判断是否设置了combiner,如果设置,则进行一次归并操作
            if (combinerRunner == null) {
              // spill directly
              // 开始溢写操作
              DataInputBuffer key = new DataInputBuffer();
              // 开始写入相同partition的数据操作
              while (spindex < mend &&
                  kvmeta.get(offsetFor(spindex % maxRec) + PARTITION) == i) {
                final int kvoff = offsetFor(spindex % maxRec);
                int keystart = kvmeta.get(kvoff + KEYSTART);
                int valstart = kvmeta.get(kvoff + VALSTART);
                key.reset(kvbuffer, keystart, valstart - keystart);
                getVBytesForOffset(kvoff, value);
                writer.append(key, value);
                ++spindex;
              }
            } else {
              int spstart = spindex;
              while (spindex < mend &&
                  kvmeta.get(offsetFor(spindex % maxRec)
                            + 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);
                combinerRunner.combine(kvIter, combineCollector);
              }
            }

            // close the writer
            // 写操作流关闭
            writer.close();
			
            // record offsets
            rec.startOffset = segmentStart;
            rec.rawLength = writer.getRawLength() + CryptoUtils.cryptoPadding(job);
            rec.partLength = writer.getCompressedLength() + CryptoUtils.cryptoPadding(job);
            spillRec.putIndex(rec, i);

            writer = null;
          } finally {
            if (null != writer) writer.close();
          }
        }
		//判断内存中的index文件是否超出阈值,超出则将index文件写入磁盘
		// 当超出阈值的化就把当前index和之后的index写入磁盘
        if (totalIndexCacheMemory >= indexCacheMemoryLimit) {
          // create spill index file
          // 创建index文件
          Path indexFilename =
              mapOutputFile.getSpillIndexFileForWrite(numSpills, partitions
                  * MAP_OUTPUT_INDEX_RECORD_LENGTH);
          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();
      }
    }

这里总结一下这个方法的步骤

  1. 利用快速排序算法对缓冲区kvbuffer中区间[bufstart, bufend)内的数据进行排序,排序方式是,先按照分区编号partition进行排序,然后按照key进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序。
  2. 按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。
  3. 将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存中索引大小超过1 MB,则将内存索引写到文件output/spillN.out.index中

到这里,我们的环形缓冲区就已经介绍完了,相关核心的源码也都看过了

***接下来,我们继续我们的源码分析***

经过上面对环形缓冲区的认识,我们看看除了对环形缓冲区的初始化还做了哪些操作

public void init(MapOutputCollector.Context context
                    ) throws IOException, ClassNotFoundException {
                    
      ......
     
    // 排序的实现类,通过反射获取,如果用户设置了取用户的,如果用户没有设置框架默认使用快速排序
      sorter = ReflectionUtils.newInstance(job.getClass("map.sort.class",
            QuickSort.class, IndexedSorter.class), job);
      // buffers and accounting
      //将缓冲区的内存单位转换为字节
      int maxMemUsage = sortmb << 20;
      // METASIZE是我们元数据的长度,元数据存储的数据由4个部分组成,分别为
 	 // VALSTART、KEYSTART、PARTITION、VALLEN,每个占到4个字节,所以METASIZE长度为16b
 	 // 下面则是计算buffer中最多有多少byte来存元数据
      maxMemUsage -= maxMemUsage % METASIZE;
      //创建kv环形缓冲区
      kvbuffer = new byte[maxMemUsage];
      //初始bufvoid = kvbuffer的长度 
      bufvoid = kvbuffer.length;
      //将kvbuffer转化为int型的kvmeta  以int为单位,也就是4byte
      kvmeta = ByteBuffer.wrap(kvbuffer)
         .order(ByteOrder.nativeOrder())
         .asIntBuffer();
         //设置初始buf和kvmeta的分界线
      setEquator(0);
      //给kvindex进行赋值
      bufstart = bufend = bufindex = equator;
      kvstart = kvend = kvindex;
	  //kvmeta中存放元数据实体的最大个数
      maxRec = kvmeta.capacity() / NMETA;
      //buffer spill时的阈值 (kvbuffer.length*spiller)
      softLimit = (int)(kvbuffer.length * spillper);
      //bufferRemaining这个变量作为衡量是否到达阈值
      bufferRemaining = softLimit;
      LOG.info(JobContext.IO_SORT_MB + ": " + sortmb);
      LOG.info("soft limit at " + softLimit);
      LOG.info("bufstart = " + bufstart + "; bufvoid = " + bufvoid);
      LOG.info("kvstart = " + kvstart + "; length = " + maxRec);
	
      // k/v serialization
      //kv序列化的工作
      //获取字典比较器,我们展开看一下源码
      comparator = job.getOutputKeyComparator();
      //获取map输出端的key类型,用户设置取用户的,用户没有设置默认是Object类型
      keyClass = (Class<K>)job.getMapOutputKeyClass();
      //获取我们map输出端的value类型,用户设置取用户的,用户没有设置默认是Object类型
      valClass = (Class<V>)job.getMapOutputValueClass();
      //为序列化工厂赋值
      serializationFactory = new SerializationFactory(job);
      //获取key的序列化
      keySerializer = serializationFactory.getSerializer(keyClass);
      //bb跟踪源码得知是一个new的普通buffer
      //将bb作为key序列化写入的output
      keySerializer.open(bb);
      //获取value的序列化
      valSerializer = serializationFactory.getSerializer(valClass);
      //bb跟踪源码得知是一个new的普通buffer
      //将bb作为value序列化写入的output
      valSerializer.open(bb);
	
      // output counters
      //获取map输出计数器
      mapOutputByteCounter = reporter.getCounter(TaskCounter.MAP_OUTPUT_BYTES);
      mapOutputRecordCounter =
        reporter.getCounter(TaskCounter.MAP_OUTPUT_RECORDS);
      fileOutputByteCounter = reporter
          .getCounter(TaskCounter.MAP_OUTPUT_MATERIALIZED_BYTES);

      // compression
      //判断用户是否设置了压缩格式
      // 获取默认压缩格式,默认是基于Zlib算法的压缩器 DefaultCodec
     // 一旦启用了压缩机制,Hadoop会为每条记录的key和value值进行压缩
      if (job.getCompressMapOutput()) {
        Class<? extends CompressionCodec> codecClass =
          job.getMapOutputCompressorClass(DefaultCodec.class);
        codec = ReflectionUtils.newInstance(codecClass, job);
      } else {
      	//没有设置则没有压缩
        codec = null;
      }

      // combiner
      //当所有数据处理完成后,Map Task对所有临时文件进行一次合并,以确保最终只会生成一个数据文件
      final Counters.Counter combineInputCounter =
        reporter.getCounter(TaskCounter.COMBINE_INPUT_RECORDS);
      combinerRunner = CombinerRunner.create(job, getTaskID(), 
                                             combineInputCounter,
                                             reporter, null);
       //判断是否开启combiner     
      if (combinerRunner != null) {
        final Counters.Counter combineOutputCounter =
          reporter.getCounter(TaskCounter.COMBINE_OUTPUT_RECORDS);
        combineCollector= new CombineOutputCollector<K,V>(combineOutputCounter, reporter, job);
      } else {
        combineCollector = null;
      }
      spillInProgress = false;
      minSpillsForCombine = job.getInt(JobContext.MAP_COMBINE_MIN_SPILLS, 3);
      spillThread.setDaemon(true);
      //设置线程名字
      spillThread.setName("SpillThread");
      spillLock.lock();
      //将数据写入缓冲区
      try {
        //启动溢写线程
        spillThread.start();
        //进行轮询的方式询问
        while (!spillThreadRunning) {
         //等待spillThread线程结束
          spillDone.await();
        }
      } catch (InterruptedException e) {
        throw new IOException("Spill thread failed to initialize", e);
      } finally {
        spillLock.unlock();    
      }
      if (sortSpillException != null) {
        throw new IOException("Spill thread failed to initialize",
            sortSpillException);
      }
    }

我们进入到比较器中,查看一下源码

public RawComparator getOutputKeyComparator() {
    Class<? extends RawComparator> theClass = getClass(
      JobContext.KEY_COMPARATOR, null, RawComparator.class);
    if (theClass != null)
     // 如果用户配置了比较器,则直接反射用户配置的生成对象
     // 这里我们可以自定义比较器,比较方便
      return ReflectionUtils.newInstance(theClass, this);
    return 
    //用户没有配置则返回key自身的比较器
    WritableComparator.get(getMapOutputKeyClass().asSubclass(WritableComparable.class), this);
  }

接下来我们继续往下看,会看到一个combiner ,它的功能和reduce差不多,算是一个map阶段的分量reduce。

public void init(MapOutputCollector.Context context
                    ) throws IOException, ClassNotFoundException {
                    
      ......	

      // combiner
      //当所有数据处理完成后,Map Task对所有临时文件进行一次合并,以确保最终只会生成一个数据文件
      final Counters.Counter combineInputCounter =
        reporter.getCounter(TaskCounter.COMBINE_INPUT_RECORDS);
      combinerRunner = CombinerRunner.create(job, getTaskID(), 
                                             combineInputCounter,
                                             reporter, null);
       //判断是否开启combiner     
      if (combinerRunner != null) {
        final Counters.Counter combineOutputCounter =
          reporter.getCounter(TaskCounter.COMBINE_OUTPUT_RECORDS);
        combineCollector= new CombineOutputCollector<K,V>(combineOutputCounter, reporter, job);
      } else {
        combineCollector = null;
      }
      spillInProgress = false;
      //最小的溢写数触发combiner,默认是3
      minSpillsForCombine = job.getInt(JobContext.MAP_COMBINE_MIN_SPILLS, 3);
      spillThread.setDaemon(true);
      //设置线程名字
      spillThread.setName("SpillThread");
      spillLock.lock();
      //将数据写入缓冲区
      try {
        //启动溢写线程
        spillThread.start();
        //进行轮询的方式询问
        while (!spillThreadRunning) {
         //等待spillThread线程结束
          spillDone.await();
        }
      } catch (InterruptedException e) {
        throw new IOException("Spill thread failed to initialize", e);
      } finally {
        spillLock.unlock();    
      }
      if (sortSpillException != null) {
        throw new IOException("Spill thread failed to initialize",
            sortSpillException);
      }
    }

接下来,我们总结一下,在map的输出初始化环节都做了哪些事情

  1. 准备环形缓冲区(字节数组)
  2. 缓冲区内排序(排序器)
  3. 初始化比较器
  4. combiner
  5. 启动溢写线程

然后我们继续看,找到MapOutPutBuffer的collect方法,在前文的分析中,我们得知,map输入的时候真正干活的是行读取器,而输出则是这个MapOutPutBuffer的collect方法,这个方法是我们在前面已经分析的很透彻了,具体干了什么我们也都清楚了。下面我们回到前面看一下,map输出完之后溢写的小文件怎么归并成一个文件

	private <INKEY,INVALUE,OUTKEY,OUTVALUE>
  void runNewMapper(final JobConf job,
                    final TaskSplitIndex splitIndex,
                    final TaskUmbilicalProtocol umbilical,
                    TaskReporter reporter
                    ) throws IOException, ClassNotFoundException,
                            InterruptedException {
            ......
			
	 		 try {
	 		 //只要map的输出结束后,输入就会关闭
      input.initialize(split, mapperContext);
      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 void close(TaskAttemptContext context
                      ) throws IOException,InterruptedException {
      try {
      // 如果溢写了一个文件,内存中还有数据,就调用了这个容器的flush方法,我们进去看一看,很熟悉
        collector.flush();
      } catch (ClassNotFoundException cnf) {
        throw new IOException("can't find class ", cnf);
      }
      collector.close();
    }

接下来,我们进到这个flush方法里面,我们发现,最终是在MapOutPutBuffer这个方法里面

public void flush() throws IOException, ClassNotFoundException,
           InterruptedException {
      LOG.info("Starting flush of map output");
      if (kvbuffer == null) {
        LOG.info("kvbuffer is null. Skipping flush.");
        return;
      }
      spillLock.lock();
      try {
        while (spillInProgress) {
          reporter.progress();
          spillDone.await();
        }
        checkSpillException();

        final int kvbend = 4 * kvend;
        if ((kvbend + METASIZE) % kvbuffer.length !=
            equator - (equator % METASIZE)) {
          // spill finished
          resetSpill();
        }
        if (kvindex != kvend) {
          kvend = (kvindex + NMETA) % kvmeta.capacity();
          bufend = bufmark;
          LOG.info("Spilling map output");
          LOG.info("bufstart = " + bufstart + "; bufend = " + bufmark +
                   "; bufvoid = " + bufvoid);
          LOG.info("kvstart = " + kvstart + "(" + (kvstart * 4) +
                   "); kvend = " + kvend + "(" + (kvend * 4) +
                   "); length = " + (distanceTo(kvend, kvstart,
                         kvmeta.capacity()) + 1) + "/" + maxRec);
           //排序并且溢写,这个方法我们在上面已经详细分析过了。
          sortAndSpill();
        }
      } catch (InterruptedException e) {
        throw new IOException("Interrupted while waiting for the writer", e);
      } finally {
        spillLock.unlock();
      }
      assert !spillLock.isHeldByCurrentThread();
      // 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 new IOException("Spill failed", e);
      }
      // release sort buffer before the merge
      kvbuffer = null;
      // 最后在output关闭的时候,调用合并,把小文件合并
      mergeParts();
      Path outputPath = mapOutputFile.getOutputFile();
      fileOutputByteCounter.increment(rfs.getFileStatus(outputPath).getLen());
    }

我们进到这个 mergeParts方法里面,简单看一下是怎么合并的

private void mergeParts() throws IOException, InterruptedException, 
                                     ClassNotFoundException {
                    
  			 ......
  		// 如果我们没有配置combiner,或者我们的溢写文件小于最小触发溢写Combine的话,默认是3	 
  		if (combinerRunner == null || numSpills < minSpillsForCombine) {
  			//直接写文件
            Merger.writeFile(kvIter, writer, reporter, job);
          } else {
          // 配置了combiner,并且我们的溢写文件大于 3, 则调用combine做一次合并的操作
            combineCollector.setWriter(writer);
            combinerRunner.combine(kvIter, combineCollector);
          }
		 //到这里,我们的输出也就完了,关流
          //close
          writer.close();	


到达这里,我们的Map Task输出也就分析完成了,我相信看到这里的,对map task的源码已经有了大致的了解,并且对环形缓冲区的理解很到位,对环形缓冲区的源码也有不浅的理解。至于优化,这里面有很多可以优化的点,这里就不多说。

你可能感兴趣的:(源码分析)