一、先看簡單理解
對於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):
翻譯成漢語的解釋也有不少,隨便粘一個"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)時在上述兩條配置的說明下面還有兩條注意事項,其中主要疑問在第一條,如圖:
只看第一條就行,我開始對這條英文的理解比較模糊,於是上網找了一些官網的翻譯(http://blog.csdn.net/u011812294/article/details/53379338?locationNum=8&fps=1),如圖:
雖然我對這段英文理解的比較模糊,但是也能看出這個翻譯是明顯錯誤的(只是就事論事,沒有批評作者的意思,畢竟官網那么多英文都大體翻譯了過來已經很了不起了,誰都有沒有理解不是很透徹的地方,而且好多內容我都是看這篇翻譯弄懂的),那到底會不會起額外的線程呢,spill到磁盤的數據是只有percent * buffer的數據量呢還是另有門道呢?這算是"疑問一"。於是我就又看了官網的mapred-default.xml文件對這個配置的說明,如圖:
這解釋前半部分沒什么說的,后半部分透露出一個信息,當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完畢。
要看更詳細信息還是看這篇文章"http://www.tuicool.com/articles/7FNN32"。