1.在map处理的过程中有几个线程?各个线程又分别完成了什么任务?
答:mainThread:主要任务是获取(k,v)数据,map处理数据,paritition分区,seralize序列化,结果写入缓冲区。
spillThread:当mainThread发现内存缓冲区的占用百分比已经达到某个临界值(默认为0.8)时就会唤醒spillThead线程。spillThead线程把内存缓冲区里的数据sortAndSpill到硬盘上,每次spill都会溢写一个文件(如果有combine,则先进行combine操作,再写入硬盘中),这样会产生多个溢写文件到硬盘上。
3.map的输出是如何管理的?
答:map的输出是先放到一个环形的内存缓冲区mapoutputbuffer中。写入缓冲区的方法是之前提到的collect,内存缓冲区的代码是 kvnetx=(kvindex+1)%kvoffsets.length。数据在mapoutputbuffer中是一个两级索引结构,包括了kvoffsets和kvindices。kvoffsets可以当作是引用,剩下的作为对象的数组。从缓冲区取出数据有两种方法,一是在sortAndSpill方法中直接从内部数组中读取,另外还可以使用combine,MRResultIterator将缓冲区内的数据组织成KV对的读取。
4.Shuffle中的Spill具体过程是怎样的?
答:如果内存缓冲区所占空间达到一定的阈值,就需要把内存缓冲区的数据spill写入硬盘中,以免造成过大的内存开销。SpillThread调用的是sortAndSpill()方法,其中sort是先使用QuickSort()对内存缓冲区中需要spill的数据进行排序,然后使用write写入硬盘中。
5.为什么要排序?如何实现排序的?
答:排序的目的是先按照partition进行排序,每个partition内key有序,排序是shuffle的关键所在。快排需要两个操作,分别是比较大小和互换位置。下面是实现排序的程序:
//排序使用的是QuickSort
sorter = ReflectionUtils.newInstance(
job.getClass("map.sort.class", QuickSort.class, IndexedSorter.class), job);
//比较函数
public int compare(int i, int j) {
final int ii = kvoffsets[i % kvoffsets.length];
final int ij = kvoffsets[j % kvoffsets.length];
// sort by partitionif (kvindices[ii + PARTITION] != kvindices[ij + PARTITION]) {
return kvindices[ii + PARTITION] - kvindices[ij + PARTITION];
}
// sort by keyreturn comparator.compare(kvbuffer,
kvindices[ii + KEYSTART],
kvindices[ii + VALSTART] - kvindices[ii + KEYSTART],
kvbuffer,
kvindices[ij + KEYSTART],
kvindices[ij + VALSTART] - kvindices[ij + KEYSTART]);
}
//交换函数
public void swap(int i, int j) {
i %= kvoffsets.length;
j %= kvoffsets.length;
int tmp = kvoffsets[i];
kvoffsets[i] = kvoffsets[j];
kvoffsets[j] = tmp;
}
6.内存缓冲区是怎么设计的?
答:内存缓冲区的设计有两个目标,一个是可以存储不定长的数据,另一个是可以完成对这些数据按照partition和key进行排序。对于不定长的数据,一般是通过建立(起始位置,长度)的索引来指示数据的位置,也就是例如(startOfPartition, lengthOfPartition, startOfKey, lengthOfKey, startOfValue, lengthOfValue)的索引。排序则是将这个作为单位来移动的。在MapOutputBuffer中利用kvindices来进行索引,具体的(k,v)的值存在kvbuffer中。
7.内存缓冲区中key和value分别是怎样读取的?
答:key的读取
public DataInputBuffer getKey() throws IOException {
final int kvoff = kvoffsets[current % kvoffsets.length];
keybuf.reset(kvbuffer, kvindices[kvoff + KEYSTART],
kvindices[kvoff + VALSTART] - kvindices[kvoff + KEYSTART]);
return keybuf;
}
kv在kvbuffer中是连续存放的,即kvkvkv… 。key虽然在理论上也存在写入时不连续性的问题,但在mapreduce中通过reset()函数来保证key是连续的,也是为了后面按照key进行排序做准备。(value的起始地址-key的起始地址)就是key的值。
value的读取
public DataInputBuffer getValue() throws IOException {
getVBytesForOffset(kvoffsets[current % kvoffsets.length], vbytes);
return vbytes;
}
private void getVBytesForOffset(int kvoff, InMemValBytes vbytes) {
final int nextindex = (kvoff / ACCTSIZE == (kvend - 1 + kvoffsets.length) % kvoffsets.length)
? bufend : kvindices[(kvoff + ACCTSIZE + KEYSTART) % kvindices.length];
int vallen = (nextindex >= kvindices[kvoff + VALSTART])
? nextindex - kvindices[kvoff + VALSTART] : (bufvoid - kvindices[kvoff + VALSTART]) + nextindex;
vbytes.reset(kvbuffer, kvindices[kvoff + VALSTART], vallen);
}
(下一个key的起始地址-value的起始地址)之间的数据就是value的值。但因为缓冲区是环形的,虽然可以通过reset()来保证key的连续性,但value的连续性无法保证。InMemValBytes这个类就是为了解决value不连续的问题。
8.combine和merge的关系?
答:如果用户设置了combine,则在进行spill的同时会进行combine,combine是将过多的spill文件进行合并的过程,combine的输入数据是还没有写入硬盘的spill文件,即输入数据在内存中,输出是把spill文件进行合并后输出到硬盘,也就是说spill中间执行。combine从缓冲区中把文件读取出来,进行处理,写入文件。
9.为什么要merge?merge过程是怎样进行的?merge和spill有什么关系?
答:spill的过程会执行多次,导致会产生多个溢写文件。如果直接将这么多的溢写文件交给后续的reducer端去处理,必然会产生大量的磁盘IO开销,所以通过merge把文件数量减少。merge的过程是将多个溢写文件合并成一个文件,并且保持按照partition和key有序排列。merge处理的spill完成后的文件,是减少spill溢写文件总量的过程。