MapReduce之mapOutputBuffer解析

转载地址:http://blog.csdn.net/wangqinghuan1993/article/details/53785403

MapOutPutBuffer就是map任务暂存记录的内存缓冲区。不过这个缓冲区是有限的,当写入的数据超过缓冲区设定的阈值时,需要将缓冲区的数据写入到磁盘,这个过程叫spill。在溢出数据到磁盘的时候,会按照key进行排序,保证刷新到磁盘的记录时排好序的。该缓冲区的设计非常有意思,它做到了将数据的meta data(索引)和raw data(原始数据)一起存放到了该缓冲区中,并且,在spill的过程中,仍然能够往该缓冲区中写入数据,我们在下面会详细分析该缓冲区是怎么实现这些功能的。

缓冲区分析

      MapoutPutBuffer是一个环形缓冲区,每个输入的key->value键值对以及其索引信息都会写入到该缓冲区,当缓冲区块满的时候,有一个后台的守护线程会负责对数据排序,将其写入到磁盘。

核心成员变量

1、 kvbuffer :字节数组,数据和数据的索引都会存在该数组中

2、 kvmeta:只是kvbuffer中索引存储部分的一个视角,为什么这么说?因为索引往往是按整型存储(4个字节),所以使用kvmeta来重新组织该部分的字节(kvmeta中的一个单元相当于4个字节,但是kvmeta并没有重新开辟内存,其指向的还是kvbuffer

3、 equator:缓冲区的分割线,用来分割数据和数据的索引信息。

4、 kvindex:下次要插入的索引的位置

5、 kvstart:溢出时索引的起始位置

6、 kvend:溢出时索引的结束位置

7、 bufindex:下次要写入的raw数据的位置

8、 bufstart:溢出时raw数据的起始位置

9、 bufend:溢出时raw数据的结束位置

10、spiller:当数据占用超过这个比例时,就溢出

11、sortmb:kvbuffer总的内存量,默认值是100m,可以配置

12、indexCacheMemoryLimit:存放溢出文件信息的缓存大小,默认1m,可以配置

13、bufferremaining:buffer剩余空间,字节为单位

14、softLimit:溢出阈值,超出后就溢出。Sortmb*spiller

初始状态

 

 

      初始时,equator=0,在写入数据时,raw data往数组下标增大的方向延伸,而meta data(索引信息)往从数组后面往下标减小的方向延伸。从上图来看,raw data就是按照顺时针来写入数据,而meta data按照逆时针写入数据。我们再看一下各个变量的初始化情况,raw data部分的变量,bufstart、bufend、bufindex都初始化为0。Meta data部分的变量,kvstart 、kvend、kvindex都是按逆时针偏移了16个字节(metasize=16个字节),因为一个meta data占用16个字节(4个整数,分别存储keystart,valuestart,partion,valuelen),所以需要逆时针偏移16个字节来标记第一个存储的metadata的起始位置。还有一个重要的变量,bufferremaining = softlimit(默认是sortmb*80%)。

我们下面看一下对应这部分的初始化代码:

[java] view plain copy

1. public void init(MapOutputCollector.Context context  

2.                    ) throws IOException, ClassNotFoundException {  

3.      job = context.getJobConf();  

4.      reporter = context.getReporter();  

5.      mapTask = context.getMapTask();  

6.      mapOutputFile = mapTask.getMapOutputFile();  

7.      sortPhase = mapTask.getSortPhase();  

8.      spilledRecordsCounter = reporter.getCounter(TaskCounter.SPILLED_RECORDS);  

9.      //获取reduce的数量,作为分区数  

10.      partitions = job.getNumReduceTasks();  

11.      rfs = ((LocalFileSystem)FileSystem.getLocal(job)).getRaw();  

12.   

13.      //sanity checks  

14.     //获取到spiller,默认80%  

15.      final float spillper =  

16.      job.getFloat(JobContext.MAP_SORT_SPILL_PERCENT, (float)0.8);  

17.      //获取sortmb,就是整个缓冲区的大小,默认100M  

18.      final int sortmb = job.getInt(JobContext.IO_SORT_MB, 100);  

19.      indexCacheMemoryLimit = job.getInt(JobContext.INDEX_CACHE_MEMORY_LIMIT,  

20.                                         INDEX_CACHE_MEMORY_LIMIT_DEFAULT);  

21.      if (spillper > (float)1.0 || spillper <= (float)0.0) {  

22.        throw new IOException("Invalid \"" + JobContext.MAP_SORT_SPILL_PERCENT +  

23.            "\": " + spillper);  

24.      }  

25.      if ((sortmb & 0x7FF) != sortmb) {  

26.        throw new IOException(  

27.            "Invalid \"" + JobContext.IO_SORT_MB + "\": " + sortmb);  

28.      }  

29.     //获取排序方法,使用快速排序的方法  

30.      sorter = ReflectionUtils.newInstance(job.getClass("map.sort.class",  

31.            QuickSort.class, IndexedSorter.class), job);  

32.      // buffers and accounting  

33.     //将mb转化成byte,sortmb<<20就是sortmb*104*1024  

34.      int maxMemUsage = sortmb << 20;  

35.      maxMemUsage -= maxMemUsage % METASIZE;  

36.      //生成kvbuffer  

37.      kvbuffer = new byte[maxMemUsage];  

38.      bufvoid = kvbuffer.length;  

39.      //生成kvmeta,就像前面所说的,kvmeta只是kvbuffer的一种视角,下面会详细讲解kvmeta对kvbuffer的封装  

40.      kvmeta = ByteBuffer.wrap(kvbuffer)  

41.         .order(ByteOrder.nativeOrder())  

42.         .asIntBuffer();  

43.      //设置分割线为0  

44.      setEquator(0);  

45.      //初始化bufstart,bufend,bufindex,equator都为0  

46.      bufstart = bufend = bufindex = equator;  

47.      //初始化kvstart,kvend,kvindex都为kvbuffer.length-metasize,kvindex在setEquator中已经计算出来了  

48.      kvstart = kvend = kvindex;  

49.      //计算kvmeta能存储的最大数量  

50.      maxRec = kvmeta.capacity() / NMETA;  

51.      //设置softlimit为缓存的80%  

52.      softLimit = (int)(kvbuffer.length * spillper);  

53.      //设置bufferRemaining为softlimit  

54.      bufferRemaining = softLimit;  

55.      if (LOG.isInfoEnabled()) {  

56.        LOG.info(JobContext.IO_SORT_MB + ": " + sortmb);  

57.        LOG.info("soft limit at " + softLimit);  

58.        LOG.info("bufstart = " + bufstart + "; bufvoid = " + bufvoid);  

59.        LOG.info("kvstart = " + kvstart + "; length = " + maxRec);  

60.      }  

61.   

62.      // k/v serialization  

63.      //生成keySerializer,valueSerializer  

64.      comparator = job.getOutputKeyComparator();  

65.      keyClass = (Class<K>)job.getMapOutputKeyClass();  

66.      valClass = (Class<V>)job.getMapOutputValueClass();  

67.      serializationFactory = new SerializationFactory(job);  

68.      keySerializer = serializationFactory.getSerializer(keyClass);  

69.      keySerializer.open(bb);  

70.      valSerializer = serializationFactory.getSerializer(valClass);  

71.      valSerializer.open(bb);  

72.   

73.      // output counters  

74.     //输出统计,byteCounter,recorderCounter。返回给用户  

75.      mapOutputByteCounter = reporter.getCounter(TaskCounter.MAP_OUTPUT_BYTES);  

76.      mapOutputRecordCounter =  

77.        reporter.getCounter(TaskCounter.MAP_OUTPUT_RECORDS);  

78.      fileOutputByteCounter = reporter  

79.          .getCounter(TaskCounter.MAP_OUTPUT_MATERIALIZED_BYTES);  

80.   

81.      // compression  

82.      if (job.getCompressMapOutput()) {  

83.        Class<? extends CompressionCodec> codecClass =  

84.          job.getMapOutputCompressorClass(DefaultCodec.class);  

85.        codec = ReflectionUtils.newInstance(codecClass, job);  

86.      } else {  

87.        codec = null;  

88.      }  

89.   

90.      // combiner  

91.      final Counters.Counter combineInputCounter =  

92.        reporter.getCounter(TaskCounter.COMBINE_INPUT_RECORDS);  

93.      combinerRunner = CombinerRunner.create(job, getTaskID(),   

94.                                             combineInputCounter,  

95.                                             reporter, null);  

96.      if (combinerRunner != null) {  

97.        final Counters.Counter combineOutputCounter =  

98.          reporter.getCounter(TaskCounter.COMBINE_OUTPUT_RECORDS);  

99.        combineCollector= new CombineOutputCollector<K,V>(combineOutputCounter, reporter, job);  

100.      } else {  

101.        combineCollector = null;  

102.      }  

103.      spillInProgress = false;  

104.      minSpillsForCombine = job.getInt(JobContext.MAP_COMBINE_MIN_SPILLS, 3);  

105.      //溢出文件的后台线程  

106.      spillThread.setDaemon(true);  

107.       spillThread.setName("SpillThread");  

108.      spillLock.lock();  

109.      try {  

110.        spillThread.start();  

111.        while (!spillThreadRunning) {  

112.          spillDone.await();  

113.        }  

114.      } catch (InterruptedException e) {  

115.        throw new IOException("Spill thread failed to initialize", e);  

116.      } finally {  

117.        spillLock.unlock();  

118.      }  

119.      if (sortSpillException != null) {  

120.        throw new IOException("Spill thread failed to initialize",  

121.            sortSpillException);  

122.      }  

123.    }  

 

 写入第一个<key,value>的状态

 

 

     我们看一下写入第一个<key,value>的情况,首先放入key,在bufferindex的基础上累加key的字节数,然后放入value,继续累加bufferindex的字节数。接下来放入metadata,meta data一共包括4个整数,第一个int放valuestart,第二个int放keystart,第三个int放partion,第四个int放value的长度。为什么只有value的长度,没有key的长度?个人理解key的长度可以有valuestart – keystart得出,不需要额外的空间来存储key的长度。需要注意的是,bufindex和kvindex发生了变化,分别指向了下一个数据需要插入的地方。但是bufstart,endstart,kvstart,kvend都没有变化,bufferremaining相应地减少了meta data 和raw data占据的空间。

     我们再来看一下对应这部分的代码:

[java] view plain copy

1. // serialize key bytes into buffer  

2. //序列化key到buffer中  

3.  int keystart = bufindex;  

4.  keySerializer.serialize(key);  

5.  //如果序列化完key以后,bufindex<keystart了,由于循环缓冲区的原因,key在数组尾部存储了一部分,在数组头部也存储了一部分,  

6.  需要将key往后偏移,保证key是连续的,不能发生一部分在尾部一部分在头部的情况。下面的“key跨边界的情况”章节会详细介绍该特殊情况  

7. if (bufindex < keystart){  

8.    // wrapped the key; must make contiguous  

9.    bb.shiftBufferedKey();  

10.    keystart = 0;  

11.  }  

12.  // serialize value bytes into buffer  

13.  //序列化value到kvbuffer中  

14.  final int valstart= bufindex;  

15.  valSerializer.serialize(value);  

16.  // It's possible for records to have zero length, i.e. the serializer  

17.  // will perform no writes. To ensure that the boundary conditions are  

18.  // checked and that the kvindex invariant is maintained, perform a  

19.  // zero-length write into the buffer. The logic monitoring this could be  

20.  // moved into collect, but this is cleaner and inexpensive. For now, it  

21.  // is acceptable.  

22.  bb.write(b0, 00);  

23.   

24.  // the record must be marked after the preceding write, as the metadata  

25.  // for this record are not yet written  

26.  //得到valend,为写入meta data数据时做准备  

27.  int valend = bb.markRecord();  

28.  //recordCounter加1  

29.  mapOutputRecordCounter.increment(1);  

30.  //byteCounter加上key和value的字节长度  

31.  mapOutputByteCounter.increment(  

32.      distanceTo(keystart, valend, bufvoid));  

33.   

34.  // write accounting info  

35.  //下面是写入kvmeta信息,就不做赘述了  

36.  kvmeta.put(kvindex + PARTITION, partition);  

37.  kvmeta.put(kvindex + KEYSTART, keystart);  

38.  kvmeta.put(kvindex + VALSTART, valstart);  

39.  kvmeta.put(kvindex + VALLEN, distanceTo(valstart, valend));  

40.  // advance kvindex  

41.  //偏移kvindex  

42.  kvindex= (kvindex- NMETA+ kvmeta.capacity())% kvmeta.capacity();  

溢出文件

 

                                                                                  第一次达到spill的阈值

        随着kvindex和bufindex的不断偏移,剩余的空间越来越小,当剩余空间不足时,就会触发spill操作。如上图,kvindex和bufindex之间的空间已经很小了。

                                                                重新划分equator,开始溢出

      溢出文件开始前,需要先更新kvend、bufend,kvend 需要更新成kvindex + 4 int,因为kvindex始终指向下一个需要写入的meta data的位置,必须往后回退4 int 才是meta data真正结束的位置,如上图,kvend加了4int往顺时针方向偏移了。Kvstart指向最后一个meta data写入的位置。Bufstart标识着最后一个key 开始的位置,bufend 标识最第一个value的结束位置。Kvstart和kvend之间(黄色部分)是需要溢出的meta data。Bufstart和bufend之间(浅绿色)是需要溢出的raw data。溢出的时候,其他的缓存空间(深绿色)仍然可以写入数据,不会被溢出操作阻塞住。默认的Spiller是80%,也就是还有20%的空间可以在溢出的时候使用。 

      溢出开始前,需要确定新的equator,新的equator一定在kvend和bufend之间。新的equator一定要做到合适的划分,保证能写入更多的metadata和raw data。确定了equator后,我们需要更新bufindex和kvindex的位置,更新bufferremaining的大小,bufferremaining要选择equator到bufindex、kvindex较小的那个。任何一个用完了,都代表不能写入数据,这也说明了equator划分均匀的重要性。

                                                                                                           溢出完成后的状态 

         在溢出完成后,空间都已经释放出来,溢出完成后的缓存状态就变成了上图:meta data从新的equator开始逆时针写入数据,raw data从新的equator开始顺时针写入数据。当剩余的空间又到了溢出的阈值时,再次划分equator,再次溢出文件。

下面看一下这部分对应的代码:

[java] view plain copy

1. //每次写入数据之前bufferremaining先减去16个字节的大小  

2.    bufferRemaining -= METASIZE;  

3.      if (bufferRemaining <= 0) {    

4.        // start spill if the thread is not running and the soft limit has been  

5.        // reached  

6.        //如果soft limit 达到了,就需要溢出文件  

7.        spillLock.lock();  

8.        try {  

9.          do {  

10.            if (!spillInProgress) {  

11.              final int kvbidx = 4 * kvindex;  

12.              final int kvbend = 4 * kvend;  

13.              // serialized, unspilled bytes always lie between kvindex and  

14.              // bufindex, crossing the equator. Note that any void space  

15.              // created by a reset must be included in "used" bytes  

16.              //已经序列化的,但是未进行spill的文件,总是在kvindex和bufindex之间(中间横跨equator),所以可以通过kvindex和bufindex计算出使用的字节数。  

17.              final int bUsed = distanceTo(kvbidx, bufindex);  

18.              final boolean bufsoftlimit = bUsed >= softLimit;  

19.              //注意这里,(kvbend + METASIZE) % kvbuffer.length != equator - (equator % METASIZE),说明已经发生了spill操作(进行spill操作时,kvend会调整,equator会重新划分),而程序能够进来,说明溢出操作已经结束  

20.              if ((kvbend + METASIZE) % kvbuffer.length !=  

21.                  equator - (equator % METASIZE)) {  

22.                // spill finished, reclaim space  

23.               //resetSPill中,就是将bufstart和bufend重置为equator,kvstart和kvend重置为第一条meta record的开始位置  

24. esetSpill();  

25.              //重新计算bufferremaing   

26. bufferRemaining = Math.min(  

27.                    distanceTo(bufindex, kvbidx) - 2 * METASIZE,  

28.                    softLimit - bUsed) - METASIZE;  

29.                continue;  

30.              } else if (bufsoftlimit && kvindex != kvend) {  

31.           

32.                // spill records, if any collected; check latter, as it may  

33.                // be possible for metadata alignment to hit spill pcnt  

34.                //startSpill中,将kvend  

35. startSpill();  

36.                final int avgRec = (int)  

37.                  (mapOutputByteCounter.getCounter() /  

38.                  mapOutputRecordCounter.getCounter());  

39.                // leave at least half the split buffer for serialization data  

40.                // ensure that kvindex >= bufindex  

41.                final int distkvi = distanceTo(bufindex, kvbidx);  

42.                final int newPos = (bufindex +  

43.                  Math.max(2 * METASIZE - 1,  

44.                          Math.min(distkvi / 2,  

45.                                   distkvi / (METASIZE + avgRec) * METASIZE)))  

46.                  % kvbuffer.length;  

47.                setEquator(newPos);  

48.                bufmark = bufindex = newPos;  

49.                final int serBound = 4 * kvend;  

50.                // bytes remaining before the lock must be held and limits  

51.                // checked is the minimum of three arcs: the metadata space, the  

52.                // serialization space, and the soft limit  

53.                bufferRemaining = Math.min(  

54.                    // metadata max  

55.                    distanceTo(bufend, newPos),  

56.                    Math.min(  

57.                      // serialization max  

58.                      distanceTo(newPos, serBound),  

59.                      // soft limit  

60.                      softLimit)) - 2 * METASIZE;  

61.              }  

62.            }  

63.          } while (false);  

64.        } finally {  

65.          spillLock.unlock();  

66.        }  

67.      }  

Key跨边界的情况

 

                                                                                                  Key可能存在跨越边界的情况

 

                                                                              发生key跨边界的情况后,进行key偏移

 

         在这里,我们讨论一种特殊情况的处理,就是key跨越数组边界的情况。因为我们使用字节数组来实现循环缓冲区,所以肯定会存在某些数据跨越数组边界的情况。对于value跨越边界的情况,我们无需处理。而对于key跨越边界的情况,我们需要处理。为什么?因为map任务在溢出文件时,需要按照key进行排序,排序就需要取出key的值比较大小,如果key跨边界的话,取值时就不方便了。那么如何处理呢?就是将key进行偏移,使得key从数组的头部开始存储,而数组的尾部存储key的部分完全空闲出来,不再存储数据。如上图:key进行偏移后,从数组坐标0开始存储,而原先尾部的空间(红色圈出来的)不再存储数据。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值