转载地址: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开始存储,而原先尾部的空间(红色圈出来的)不再存储数据。