一、先看简单理解

对于hadoop的map端配置项"mapreduce.task.io.sort.mb"和"mapreduce.map.sort.spill.percent"应该都比较熟悉了,如图解释(http://hadoop.apache.org/docs/current/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html):

hadoop中bootstrap进程 hadoop spill_spill

翻译成汉语的解释也有不少,随便粘一个"mapreduce.task.io.sort.mb 任务内部排序缓冲区大小,默认100","mapreduce.map.sort.spill.percent Map阶段溢写文件的阈值(排序缓冲区大小的百分比),默认0.8,也就是80%"。这些内容一点都不难理解,就是map的结果先放入缓冲区(其实先序列化),当缓冲区的数据量达到阈值时(默认100M * 0.8 = 80M),溢出行为会在一个后台线程执行开始spill操作。

二、发现问题

问题的核心在于mapreduce.map.sort.spill.percent这个配置。

前两天浏览官网(http://hadoop.apache.org/docs/current/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html)时在上述两条配置的说明下面还有两条注意事项,其中主要疑问在第一条,如图:

hadoop中bootstrap进程 hadoop spill_spill_02

只看第一条就行,我开始对这条英文的理解比较模糊,于是上网找了一些官网的翻译如图:

hadoop中bootstrap进程 hadoop spill_spill_03

 

虽然我对这段英文理解的比较模糊,但是也能看出这个翻译是明显错误的(只是就事论事,没有批评作者的意思,毕竟官网那么多英文都大体翻译了过来已经很了不起了,谁都有没有理解不是很透彻的地方,而且好多内容我都是看这篇翻译弄懂的),那到底会不会起额外的线程呢,spill到磁盘的数据是只有percent * buffer的数据量呢还是另有门道呢?这算是"疑问一"。于是我就又看了官网的mapred-default.xml文件对这个配置的说明,如图:

hadoop中bootstrap进程 hadoop spill_kvStart_04

这解释前半部分没什么说的,后半部分透露出一个信息,当percent小于0.5的时候spill到磁盘的数据量有可能会大于percent * buffer,这又是为什么呢?这算是"疑问二"。下面就是我通过大量的请教别人和上网查资料之后,对这两个问题的解释。

三、解释两个疑问

对我帮助最大的应该是这篇文章"http://www.tuicool.com/articles/7FNN32",对此作者表示由衷的感谢。

要解释这两个疑问需要用到再稍微深一层的知识,我再这里只对深一层的东西做一个简单的抽象,能解决问题就行,例如实际有3个环形缓冲区,我只抽象成一个。

简单来说,map的输出到一个缓存区(中间还有个序列化过程不讨论),这个环形缓冲区有三个索引(或者指针),分别叫kvStart、kvEnd、kvIndex。map结果每进来一个keValue对,kvIndex更新一次,也就是始终标识下一个可用的地址,此时还没达到阈值,没有开始spill,kvStart=kvEnd,指向标识当spill开始时的起始位置,然后,阈值到了,后台开启一个spill线程,此时,kvStart暂时不变,而对kvEnd进行重新赋值,kvEnd=kvIndex,然后kvIndex不受影响继续随着map结果的写入而不断更新直到缓冲区满了为止。此时,spill要处理的数据其实已经确定了,就是kvStart到(kvEnd-1)这区间的数据进行溢写,此过程kvEnd正常情况不变,而每溢写成功一条数据kvStart好像是更新一次,直到最后spill成功时kvStart=kvEnd,为下一次的spill做准备。

上一段描述的是一般情况下的spill过程抽象,但是还是不能解释我提的两个疑问,其实从英文解释可以看的出我的两个疑问是些另外的情况,而这另外的情况指的就是percent小于0.5的可能会发生的情况。下面举例子说明这种情况。

例如mapreduce.map.sort.spill.percent=0.33,当缓冲区第一次达到阈值时,启动一个后台spill线程开始正常的溢写操作,由于缓冲区没满,map结果继续写入缓冲区(这一过程称为Collect),当又一个0.33*buffer被写入之后,便再次触发溢写过程,但是此时不会另外启动一个spill线程,不过呢kvEnd会被重新赋值,kvEnd被重新赋值之后呢,kvStart要到达新的kvEnd时才能结束,这样两次触发一共要处理的数据总量就是2 * 0.33 * buffer=0.66 * buffer了(当然也有可能出现3次出发,结果0.99 * buffer的情况),但是多次触发很明显只能是percent<0.5的情况,因为当percent>0.5时剩余buffer即使满了也达不到0.5,不能触发。

这样就很好的解释了这段英文"Note that collection will not block if this threshold is exceeded while a spill is already in progress, so spills may be larger than this threshold when it is set to less than .5"

还有一句英文"and the remainder of the buffer is filled while the spill runs, the next spill will include all the collected records, or 0.66 of the buffer, and will not generate additional spills",我感觉这还真有点灵活翻译理解的味道,关键字在于对"or"的理解,我认为翻译成"或者"并不是很恰当,而应该翻译成"也就是",换句话说,"0.66"就是对前面"all the collected records"用一种说法做的说明,而不是指的两种情况,当然"0.66"更不是指的percent=0.66了。至此,两个疑问都解释清楚了,当然底层模型我抽象的比较简单,要了解真正的过程看对我帮助最大的那篇文章,解释的非常详细,不过内容也十分多,需要自己找。

四、对spill过程中的锁做一个简单的摘抄,也来自文章"http://www.tuicool.com/articles/7FNN32"

两种信号:spillDone和spillReady,它们的逻辑如下:

1)对于写线程来说,如果写满了,就调用spillDone.await等待spillDone信号;否则不断往缓冲区里面写,到了一定程度,就发送spillReady.signal这个信号给读线程,发完这个信号后如果缓冲区没满,就释放锁继续写(这段代码无需锁),如果满了,就等待spillDone信号;

2)对于读线程来说,在平时调用spillReady.await等待spillReady这个信号,当读取之后(此时写线程要么释放锁了,要么调用spillDone.await在等待了,读线程肯定可以获得锁),则把锁释放掉,开始Spill(这段代码无需锁),完了读线程再次获取锁,修改相应参数,发送信号spillDone给写线程,表明Spill完毕。