Hadoop如何组织中间数据的存储和传输(源码级分析)2

Hadoop如何组织中间数据的存储和传输(源码级分析)1 解读了MapTask的整体执行流程,该文档将分析MapTask从内存缓冲区刷新到本地磁盘的过程。
MapTask环境设置:io.sort.mb = 200MB, io.sort.spill.percent=0.8.
1、处理内存缓冲区位于MapTask.MapOutputBuffer类中,所有的信息都被存储在byte[] kvbuffer中,它的长度为bufferlen=200*1024*1024 对于任意一条<K,V>记录,需要记录如下:
1)META DATA,主要包括:
    private static final int INDEX = 0;            // index offset in acct
    private static final int VALSTART = 1;         // val offset in acct
    private static final int KEYSTART = 2;         // key offset in acct
    private static final int PARTITION = 3;        // partition offset in acct
因此META SIZE =4*4=16, 每一条<K,V>的META info占有16bytes
2)<Key,Value>,它占有的大小和Key,Value的数据类型相关,例如,wordcount中,key往往是Text,value为IntWritable,则可以使用当key的Text的长度不超过127,Text中的第一个byte标示它的长度,这样比字符串的长度大1。Value单位是IntWritable,和原始的数据类型的长度相同,占有4bytes.
2、MapOutputBuffer会使用一个Equator在kvbuffer记录<K,V>的位置,起始位置为0,这样正式的<K,V>数据从kvbuffer[0]开始存储,而<K,V>的META Data从kvbuffer[bufferlen-16],每次Mapper阶段map函数内context.write(K,V)都会启动相关的操作,K,V的数据会从kvbuffer数组向后推进,META Data 则从数组底部向前推进。这种方法就利用随机访问的数组模拟了一个环形的存取池。
--------------------------------------------------------------------------------------
/key-value/key-value//////-> ....kvbuffer......<-\\\\\\\\(key-value Meta Data) 16bytes
--------------------------------------------------------------------------------------
3、当kvbuffer数组使用的长度超过了io.sort.mb * io.sort.spill.percent的长度时候,通过高级并发控制条件变量(Condition Variable)来唤醒等待spill到硬盘的线程。在Java并发编程总结---hadoop源码解读一文中介绍并发控制的方法。由于设置的是soft limit,它并不暂停向缓冲区的写入。

4、初始缓冲区的equator的位置为0,在达到soft limit之后,会重新设置一个新的equator。重新设置equator考虑了当前<K,V>长度与Meta Data的比值来设置的。设置以后的缓冲区样子如下(假定最左边位置为0):
--------------------------------------------------------------------------------------
spill in progress...    <-(Meta data)(Equator)/key-value/->   .....spill in progress
------------------------------------------------------------------------------------- 

    final IntBuffer kvmeta; // metadata overlay on backing store
    int kvstart;            // marks origin of spill metadata
    int kvend;              // marks end of spill metadata
    int kvindex;            // marks end of fully serialized records

    int equator;            // marks origin of meta/serialization
    int bufstart;           // marks beginning of spill
    int bufend;             // marks beginning of collectable
    int bufmark;            // marks end of record
    int bufindex;           // marks end of collected
    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];

    private static final int INDEX = 0;            // index offset in acct
    private static final int VALSTART = 1;         // val offset in acct
    private static final int KEYSTART = 2;         // key offset in acct
    private static final int PARTITION = 3;        // partition offset in acct
    private static final int NMETA = 4;            // num meta ints
    private static final int METASIZE = NMETA * 4; // size in bytes

在spillThread是另外一个线程,前端对于<K,V>的处理仍在继续,它处理的位置在kvindex,所以,kvstart与kvend用来记录spill的record的个数。
5、spill过程之前,会首先在内存中进行sort,这里的sort使用了hadoop.util包下的QuickSort,按照同一partition下的对于key进行排序,例如:
key:value:partition
bcd:1:2
adc:2:1
abd:4:0
abcd:1:0
经过排序以后,前后顺序为:abcd:1:0, abd:4:0, adc:2:1, bcd:1:2
在内存组织中,每一个KV Record都提供了Meta Data,每一个record都有16bytes,4个int构成,(1)存储record的Meta Data的在kvbuffer数组中的起始位置(下标);(2)value start,(3)key start,(4)partition;
在key-value的长度比较小的时候,kvbuffer数组中存储了大量的Meta信息,其实,如何更加高效的利用kvbuffer有很多可以改进的点。(比方,能否将拆分key-value的粒度变大一些,当然这个与应用的类型有关。看大家的应用特点吧,如果有需求的给我发邮件或者message我吧。)
sort过程由MapTask类实现了的swap、compare接口来实现对应的QuickSort排序,由于在Meta信息中有记录的Index信息,所以,每次根据partition和key比较完成以后,只需交换两个record的meta data的index数据。这样要保证每次获取数据的时候,是通过kvmeta.get(INDEX)为基准获取key和value,以及partition。
下面使用示意图描述:假定当前之用两个记录bcd:1:2, abd:2:1  <Text, IntWritable>
假定kvbuffer的长度为128,equator为0,存储的Text,它是有结构的String,有部分构成,一个是内涵的String的长度,以及String,由于这里String的长度都小于127,因此只需多1位byte即可,例如bcd用Text表示为3bcd,abcd用Text表示为4abcd。IntWritable的长度为4.
----------------------------------------------------------------------------------
3 b c d|      1|3 a b  d |          2 ...          |95| 12| 8 | 1 |111|4  |0  |2
0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15 ...           95| 99|103|107|111|115|119|123|
经过排序以后,bcd:1:2由于它partition为2,比abd:2:1的值要大,因此,abd:2:1小于bcd:1:2,只需调整index即可。调整之后如下所示:
----------------------------------------------------------------------------------
3 b c d|      1|3 a b  d |          2 ...          |111| 12| 8 | 1|95 |4  |0  |2
0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15 ...           95| 99|103|107|111|115|119|123|
只是交换了原index的位置的内的表达,最终结果只需查看按照正常的kvbuffer的index由起始位置,到最终位置(111)去读取。每个index下数组元素的值表示了sort以后该位置的顺序。
增加index位,给含有很多冗余信息对象排列的提供了一种更合理的解决方案。
6、每次spill结束之后,会生成一个spill文件,文件内部按照partition对当前文件加tag划分,spillRecord记录以partition为tag记录数据的起始位置、长度、以及压缩长度,这些内容表示成IndexRecord.
class IndexRecord {
  long startOffset;
  long rawLength;
  long partLength;
  public IndexRecord() { }
  public IndexRecord(long startOffset, long rawLength, long partLength) {
    this.startOffset = startOffset;
    this.rawLength = rawLength;
    this.partLength = partLength;
  }
}
每次spill文件都是按照partition从小到大的顺序组织,这每一块在文件的偏移量和长度使用IndexRecord描述,所有partition对应的IndexRecord,被组织成一个SpillRecord,在文件过程中会生成多个Spill文件,每个文件都对应一个SpillRecord,这些所有的SpillRecord会被加入到indexCacheList中。
7、Merge阶段是Map和Reduce过程都要经历的最后一个过程。与该过程相关的参数是:io.sort.factor,控制了一次最多能合并多少Segment。每一个Segment与一个IndexRecord对应。
在一个以partition的循环中,将具有相同partition的segment从各个spillfile中提取出来,归并排序。然后就是涉及到是否对于这样的merge过程之后的数据是否进行Combine过程。与此相关的有两个因素:一是程序设置了Combine阶段,二是spill过程生成的文件的个数不小于min.num.spills.for.combine。注意在map阶段两次都用到了combine过程,除这次之外,在合成spill文件之前,也使用了combine处理了结果。
这样经过合并,所有spill文件中相同partition的部分被合成到一起,类似使用IndexRecord来管理每一个新的部分,这些内容会写入file.out.index文件中。

总结
到此为止map阶段对于中间数据的处理已经完成。
前面写了很多的内容,这里总结以下:
前端------------map()函数处理------
后端 ---spill---spill---spill------Merge--
spill要经历按照partition和key结合方式排序,进行了combine过程,partition相同的放在一起,通过indexRecord记录在文件中的偏移和长度,加快对于文件数据块的读取速度。在map()处理结束之后,会启动对应merge操作,合并spill文件,将对应partition下的数据部分归并在一起,并考虑在merge的过程是否还继续进行Combine.

猜你喜欢

转载自hxl123789.iteye.com/blog/1827606