在這篇文章中张峰,我們會(huì)探究泪蔫,Mapper和Reducer的一些不為人知的秘密。
為什么說(shuō)不為人知呢喘批?畢竟Hadoop是開源的鸥滨,你可以閱讀源碼獲取一切你想要的信息。你要是這樣做谤祖,我無(wú)法反駁,因?yàn)檫@確實(shí)是最權(quán)威的方式老速。
在閱讀《Hadoop: The Definitive Guide 4th Edition》時(shí)粥喜,我們都見過(guò)這么一副圖片,它簡(jiǎn)單的解釋了Mapper和Reducer究竟是如何溝通的橘券,究竟做了些什么:
盡管這幅圖片確實(shí)非常正確额湘,但是,很遺憾的是旁舰,它并不是非常詳細(xì)锋华。
至少在我閱讀這本書的時(shí)候,當(dāng)時(shí)看到這幅圖片箭窜,只能大致了解一下過(guò)程毯焕,而對(duì)一些細(xì)節(jié)并不是非常清楚。只有在后來(lái)搞明白了,再回來(lái)看這幅圖片纳猫,才覺得確實(shí)非常正確婆咸。
所以,在這篇文章中芜辕,我們會(huì)用具體的例子尚骄,來(lái)一步步地解析這個(gè)過(guò)程。
但是侵续,有一點(diǎn)需要各位注意的是倔丈,我只是用例子一步步地驗(yàn)證了這個(gè)過(guò)程,而并不是閱讀源碼來(lái)得出了這個(gè)過(guò)程状蜗。盡管實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn)需五,但是在并不清楚真理是什么而靠實(shí)踐得到的結(jié)果來(lái)總結(jié)真理的情況下,并不是那么嚴(yán)格诗舰,可能會(huì)有一些偏差警儒。
所以,各位要做好一個(gè)心理準(zhǔn)備眶根,就是這篇文章中的內(nèi)容蜀铲,可能有錯(cuò)誤,各位要有一個(gè)質(zhì)疑的思想属百。
最近我也是打算閱讀Hadoop的源碼的记劝,到時(shí)也會(huì)通過(guò)源碼來(lái)驗(yàn)證這個(gè)過(guò)程是否正確。
那我們開始吧族扰。
總體輪廓
盡管上面我們已經(jīng)給了一副圖片厌丑,但是我還是總結(jié)了一副我認(rèn)為更加直觀的圖片,呈現(xiàn)給大家渔呵。
下面我們就會(huì)一一介紹為何他們他們是這個(gè)順序怒竿。
問(wèn)題
我們會(huì)用兩個(gè)問(wèn)題來(lái)闡述,如果搞懂這兩個(gè)問(wèn)題的處理過(guò)程扩氢,這個(gè)過(guò)程也就懂了耕驰。
問(wèn)題一:?jiǎn)卧~計(jì)數(shù)器。統(tǒng)計(jì)有相同首字母的單詞中录豺,全部的單詞的個(gè)數(shù)朦肘,以及不重復(fù)的單詞的個(gè)數(shù)。比如双饥,有(a, aa, aa, bb, ba),那么輸出應(yīng)該是(a, 3, 2), (b, 2, 2)媒抠。其中每個(gè)元組中的第二列為全部單詞的數(shù)量,第三列為不重復(fù)單詞的數(shù)量咏花。
問(wèn)題二:?jiǎn)卧~計(jì)數(shù)器趴生。跟上面的問(wèn)題大體相同,只是現(xiàn)在是統(tǒng)計(jì)有相同尾字母的單詞中,全部的單詞的個(gè)數(shù)冲秽,以及不重復(fù)的單詞的個(gè)數(shù)舍咖。還是上面的那個(gè)例子,現(xiàn)在輸出應(yīng)該是(a, 4, 3), (b, 1, 1)锉桑。
過(guò)程
問(wèn)題一
我們先來(lái)解決第一個(gè)問(wèn)題排霉。
很顯然,我們要是把單詞按照首字母分組民轴,那么就跟傳統(tǒng)的單詞計(jì)數(shù)器類似了攻柠。
所以,我們寫一個(gè)Partitioner后裸,將單詞們按照首字母分區(qū)瑰钮,保證首字母相同的單詞會(huì)被分到同一個(gè)Reducer中。
Partitioner的實(shí)現(xiàn)跟HashPartitioner的實(shí)現(xiàn)類似微驶,如下所示:
很簡(jiǎn)單對(duì)吧浪谴。
然后我們的Reducer端處理過(guò)程如下:
你可能很奇怪,為什么我們明明可以用HashSet來(lái)統(tǒng)計(jì)不重復(fù)單詞因苹,而偏偏要采用這種形式苟耻。
因?yàn)槿绻捎肏ashSet,就不會(huì)理解它排序的過(guò)程扶檐。
我們可以看到凶杖,我們?cè)赗educer端,就是判斷一下是否前一個(gè)分區(qū)已經(jīng)處理完畢款筑,如果已經(jīng)處理完智蝠,那么就將結(jié)果寫入到輸出中。最后在cleanup()函數(shù)中奈梳,再寫一遍杈湾,防止最后一個(gè)分區(qū)的結(jié)果被漏掉。
那么我們?yōu)槭裁纯梢赃@樣處理呢攘须?
因?yàn)槲覀兊膍apper的輸出如下:
(a, 1), (aa, 1), (aa, 1), (bb, 1), (ba, 1)
然后經(jīng)過(guò)Partitioner之后漆撞,產(chǎn)生了這么兩個(gè)Partition:
- Partition 1: (a, 1), (aa, 1), (aa, 1)
- Partition 2: (bb, 1), (ba, 1)
然后分別兩個(gè)Reducer來(lái)處理他們。當(dāng)然阻课,如果你強(qiáng)制只有一個(gè)Reducer,那么它們還是會(huì)被同一個(gè)Reducer處理艰匙。所以才有了我們?cè)赗educer中判斷上一個(gè)Partition是否處理完畢的邏輯限煞。
數(shù)據(jù)在Reducer端會(huì)先進(jìn)行一個(gè)排序,那么它是如何進(jìn)行排序的呢员凝?
默認(rèn)情況下署驻,是按照Key以及其類型進(jìn)行排序。這里我們的Key的類型為Text,所以會(huì)將這個(gè)Reducer接收到的數(shù)據(jù)按照Text類型的Comparator進(jìn)行排序旺上。
我們這里假設(shè)僅有一個(gè)Reducer瓶蚂。
由于Text的Comparator會(huì)將輸入排序成(a, 1), (aa, 1), (aa, 1), (ba, 1), (bb, 1)這種順序,所以上面的代碼沒有什么問(wèn)題宣吱。
而你需要注意的是窃这,如果你用的是自定義的類型,或者自定義的Comparator征候,那么經(jīng)過(guò)排序后杭攻,可能是亂序的,可能以a開頭的單詞和以b開頭的單詞就是亂序的了疤坝。具體的例子兆解,我們會(huì)在下一個(gè)問(wèn)題中介紹到。
然后跑揉,Reducer端會(huì)進(jìn)行merge操作锅睛,會(huì)將(a, 1), (aa, 1), (aa, 1), (ba, 1), (bb, 1)merge為((a, 1), (aa, (1, 1)), (ba, 1), (bb, 1))。
這樣看历谍,我們的Reducer是正確的现拒。
但是這里同樣有一個(gè)坑。
你怎么知道它是如何進(jìn)行merge的扮饶?
這個(gè)問(wèn)題具练,我們也會(huì)在問(wèn)題二中介紹。
現(xiàn)在你只需要清楚甜无,它也是按照Text進(jìn)行merge的就好了扛点。
Ok。那結(jié)果自然是正確的岂丘。
問(wèn)題二
問(wèn)題二看起來(lái)好像跟問(wèn)題一類似陵究?
對(duì)的。
跟問(wèn)題一的區(qū)別是奥帘,我們需要對(duì)尾字母進(jìn)行分區(qū)铜邮?
Yes。
所以其實(shí)有一個(gè)很簡(jiǎn)單的解決這個(gè)問(wèn)題的方式寨蹋,即把尾字母提到前面去松蒜,成為首字母,然后這個(gè)問(wèn)題就跟問(wèn)題一一樣了已旧,類似于動(dòng)態(tài)規(guī)劃秸苗。對(duì)吧?
但是這里我們不采用這種方法运褪。因?yàn)檫@種方法對(duì)我們理解在問(wèn)題一中提到的惊楼,Reducer端如何進(jìn)行排序玖瘸,如何進(jìn)行merge并沒有幫助。
我們采用另一種方法檀咙。
還是對(duì)尾字母進(jìn)行排序雅倒。
然后我們的Partitioner如下:
Reducer如下:
你會(huì)發(fā)現(xiàn),當(dāng)你的Reducer的數(shù)量大于或者等于你的Partition的數(shù)量時(shí)弧可,everything works well蔑匣。但是一旦你只有一個(gè)Reducer,就出現(xiàn)問(wèn)題了侣诺。
為什么呢殖演?
我們上面提到過(guò),Reducer端會(huì)將收到的數(shù)據(jù)先按照Key以及Key的對(duì)應(yīng)的類型的Comparator進(jìn)行排序年鸳。
假設(shè)它收到了這么兩個(gè)Partition的數(shù)據(jù):
- Partition 1: (aa, 1), (ba, 1),
- Partition 2: (bb, 1), (ab, 1)
我們希望它如何排序趴久?
(aa, 1), (ba, 1), (ab, 1), (bb, 1)
反正就是相同尾字母的單詞都是相鄰的。
而實(shí)際上搔确,它會(huì)按照Text的Comparator進(jìn)行排序彼棍,那么排序的結(jié)果是什么呢?
(aa, 1), (ab, 1), (ba, 1), (bb, 1)
那結(jié)果自然是不正確膳算。
為什么你的Reducer的數(shù)量大于或者等于你的Partition的數(shù)量時(shí)座硕,Everything works well呢?我想你應(yīng)該已經(jīng)有答案了涕蜂。
好华匾,找到問(wèn)題所在了。那我們自然就想到解決方案机隙,我們自己定義一個(gè)排序函數(shù)不就好了蜘拉?
Yes.
所以我們自定義了一個(gè)數(shù)據(jù)類型,以及一個(gè)對(duì)應(yīng)的Comparator有鹿。其實(shí)我這里寫的有些麻煩了旭旭,不用自定義數(shù)據(jù)類型的。各位自己寫的時(shí)候要注意葱跋。
我們可以看到持寄,在SuffixCompareText的compare(SuffixCompareText o)中,會(huì)先根據(jù)尾字母進(jìn)行排序娱俺,如果尾字母相同稍味,則根據(jù)剩下的字符串進(jìn)行排序。
這樣我們不僅能保證具有相同尾字母的單詞是連續(xù)的荠卷,還能保證具有相同尾字母且相同的單詞也是連續(xù)的模庐。
為什么需要這種保證呢?
因?yàn)閙erge僵朗。
現(xiàn)在是時(shí)候介紹一下merge的過(guò)程了赖欣。
- 首先查找你是否通過(guò)Job.setGroupComparatorClass(YourComparatorClass)方法指定了merge的方法,如果有验庙,則根據(jù)這個(gè)方法顶吮,對(duì)輸入的相鄰的數(shù)據(jù)進(jìn)行比較,如果相同粪薛,則合并悴了。注意,是相鄰的數(shù)據(jù)违寿。比如湃交,如果輸入數(shù)據(jù)是(aa, 1), (aa, 1), (ab, 1),則會(huì)被合并為(aa, (1, 1)), (ab, 1)藤巢。而如果輸入數(shù)據(jù)是(aa, 1), (ab, 1), (aa, 1)搞莺,則并不會(huì)進(jìn)行合并,結(jié)果還是(aa, 1), (ab, 1), (aa, 1)掂咒。
- 如果你沒有指定這個(gè)方法才沧,那么就查看你是否通過(guò)Job.setSortComparatorClass(SortComparatorClass)指定了對(duì)數(shù)據(jù)排序的方法,如果指定了的話绍刮,那么會(huì)根據(jù)這個(gè)方法温圆,對(duì)輸入的相鄰的數(shù)據(jù)進(jìn)行比較,如果相同孩革,則合并岁歉。所以,你可以看到膝蜈,如果上面的SuffixCompareText的compare中不注明當(dāng)尾字母相同時(shí)锅移,對(duì)剩下的字符串進(jìn)行比較。那么彬檀,(ab, 1), (cb, 1), (bb, 1)會(huì)被合并為(ab, (1, 1, 1))帆啃。因?yàn)樵?strong>compare()方法中,它們比較的結(jié)果為0窍帝,即相同努潘。這樣我們就無(wú)法統(tǒng)計(jì)不重復(fù)單詞的數(shù)量了。
- 如果你上面的兩個(gè)方法都沒有指定坤学,那么就根據(jù)你的Reducer的input的Key進(jìn)行合并疯坤。也是根據(jù)Key的Comparator。
其實(shí)不管怎么說(shuō)深浮,由于merge時(shí)压怠,是對(duì)相鄰的數(shù)據(jù)進(jìn)行對(duì)比的,所以你一定需要讓Reducer拿到的數(shù)據(jù)是有序的飞苇。
這里似乎GroupComparator沒有什么用菌瘫,因?yàn)槲覀兊妮斎氲捻樞蛞呀?jīng)是有序的了蜗顽。但是,理解它的過(guò)程雨让,也是有一些好處的雇盖。比如,萬(wàn)一以后你不僅僅只是想按照相同的key進(jìn)行merge呢栖忠?
總結(jié)
在上文中崔挖,我們已經(jīng)解密了mapper到reducer的過(guò)程。理解這些對(duì)你以后會(huì)大有裨益的庵寞。
源碼
關(guān)于這個(gè)Demo的源碼狸相,我已經(jīng)發(fā)布到Github上了,點(diǎn)擊這里捐川。
你也可以復(fù)制這個(gè)鏈接到瀏覽器來(lái)查看脓鹃。
https://github.com/AlstonWilliams/WordCountForHadoop