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