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開始存儲,而原先尾部的空間(紅色圈出來的)不再存儲數據。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章