學會定製MapReduce裡的partition,sort和grouping,Secondary Sort Made Easy
通過初期的幾個開發員培訓班,我發現有不少學員容易“偏愛”預設的MapReduce行為,而忽略如何在程式碼里根據自己應用的需要來定製不同於系統預設的行為。這篇文章結合Secondary Sort來介紹“Shuffle & Sort”裡涉及到的三個重要操作。
預設情況下,MapReduce Framework的Shuffle & Sort過程將所有和某一個鍵相關聯的值“組合”(group)在一起,傳送到一個唯一確定的Reducer,而且傳送到每個Reducer的鍵是“排序”的(sort)。這對應到三個操作:1)“組合”; 2)“排序”; 和 3)partition(確定哪個鍵及其值的組合送到哪個Reducer)。
這三個操作涉及到最基本的MapRedcue工作原理,好理解。但是初學者容易忽略的地方是他們很容易記住這三個操作的預設行為,卻不清楚其實其中每一個的行為都是可以在程式碼裡進行定製的。所以,下面首先介紹如何控制這三個操作行為:
- 定義partitioner來告訴MapReduce framework如何將見到的鍵和值送到哪個Reducer
- 定義key comparator來告訴MapReduce framework如何排序鍵
- 定義grouping comparator來告訴MapReduce framework如何控制“組合”鍵值在一起
這些很重要,它意味著MapReduce framework其實非常靈活,允許你根據應用來定製系統不同於預設情況下的行為。這三個可以定製的行為在Secondary Sort裡得到了應用和體現。下面我們介紹Secondary Sort。
用Hadoop做排序是一個常見的應用。其中,Secondary Sort則是一類特殊的排序問題,即:我們不僅要求key是排序的,而且要求對應相同鍵的值也是排序的。
譬如我們有一組原始資料:
a 1
z 3
b 2
a 100
a 3
b 1
如果只對鍵進行升序排序,編寫並執行相應排序MapReduce,可能某次執行結果我們得到:
a 1
a 100
a 3
b 2
b 1
z 3
而另一次我們則得到不同的結果,可能為:
a 3
a 100
a 1
b 1
b 2
z 3
也就是說,結果是按鍵升序排序,但是對應相同的鍵,值不是排序的,並且不同執行得到的值得次序是不定的。如果我們需要對應相同的鍵,值按照數字升序排序,那麼就是做一個secondary sort。對應上面的資料,預期的結果為:
a 1
a 3
a 100
b 1
b 2
z 3
實現Secondary Sort有些額外操作,其中某些步驟比較費解,因為要求不同於系統的預設行為。下面結合輸入輸出的變化對如何實現Secondary Sort進行一些講解。
首先,在Map端,我們需要產生一個複合鍵,其中一部分來自原始的“自然鍵”(譬如上面的a, b, z等),另一部分來自“自然鍵”對應的值。而Map的輸出則是對應原始鍵-值對,產生相應的複合鍵-值對。
對應我們上面一開始給出的原始資料,中間產生的結果如下
a#1 1
z#3 3
b#2 2
a#100 100
a#3 3
b#1 1
注:這裡為了講解方便,我用“#”來連線原始的鍵-值對從而產生相應的複合鍵。在實際應用中,應該為其定義一個類。
接下來,我們在Map端還有三件事情需要做。
第一,需要保證我們期望的排序。這個在整個複合鍵上進行:首先按照複合建中的原始“自然鍵”部分進行排序;如果“自然鍵”相同,則按照複合鍵中的原始值部分進行排序。具體實現可以按照上述排序方法過載複合鍵類的compareTo方法;或者更好的辦法是定義一個Comparator類按照上述排序方法過載compare方法,並在作業配置中指明使用該類:
conf.setOutputKeyComparatorClass(MyOKCC.class);
第二,我們需要定一個partitioner來保證相同“自然鍵”對應的記錄都被送到同一個Reducer,並在作業配置中指明使用該類:
conf.setPartitionerClass(MyPartitioner.class);
第三,這個也是最費解的部分,有一些學員一開始都不是太理解,也是為什麼我要寫這篇文章的主要動因。通過Mapper產生複合鍵,以及上面兩步,我們保證了相同自然鍵對應的記錄都能到達同一個Reducer,並且按照我們所需要的方式排序。不過,費解的是,如果我們的Mapper產生如下這樣的中間結果:
a#1 1
z#3 3
b#2 2
a#100 100
a#3 3
b#1 1
雖然 (a#1) 1, (a#3) 3,和(a#100) 100這三個記錄能被送到同一個Reducer,可是它們的鍵並不相同,所以對應這三個仍然是分開的記錄,而我們希望他們被“組合”在一起!
怎麼辦? MapReduce相當靈活,給我們提供了強大的機制來解決這個問題,這就涉及到我一開頭說的控制“組合”操作。這個操作實際涉及到兩方面:1)判定鍵相同;2)把相同鍵的值“組合”在一起。
正如我們一開始提到的,MapReduce的靈活性機制體現在這裡,允許應用指定如何判定鍵相同。如果我們告訴MapReduce Framework只按照複合鍵的自然鍵部分進行判定,那麼對於三個記錄(a#1) 1, (a#3) 3,和(a#100) 100,在MapReduce的“眼裡”,由於自然鍵部分都是“a”,那麼他們是相同的。因而對應的三個值1, 3 和100將被“組合”放在一個列表裡(1,3,100)作為對應(a#1)的值。也就是說,首先(a#1,1)被處理,接著系統看到(a#3,3),由於我們告訴系統這個鍵"a#3"和(a#1,1)的鍵"a#1"相同,系統將3作為和鍵"a#1"相關聯的值來看待和進行“組合”;類似地,100也被作為和a#1相關聯的值,等等。結果就是,傳到Reducer的中間結果為(a#1, [1, 3, 100])。
這個判定鍵大小的部分通過以下來實現:首先實現一個Comparator類(譬如叫“MyOVGC”),只按照複合鍵中的自然鍵部分進行比較來過載compare方法;然後,在作業配置中指明使用該類:
conf.setOutputValueGroupingComparator(MyOVGC.class);
這樣,Reducer收到的中間結果如下:
a#1,[1,3,100]
b#1,[1,2]
z#3,[3]
最後的事情就簡單了,Reducer輸出複合鍵裡的原始自然鍵部分以及每一個值作為一個新的輸出記錄。由於鍵是排序的,而值也是排序的,最後的結果就是滿足需要的結果。
下面我們把整個資料流列在下面以方便理解整個過程。
原始資料
a 1
z 3
b 2
a 100
a 3
b 1
Mapper產生的中間結果
a#1 1
z#3 3
b#2 2
a#100 100
a#3 3
b#1 1
只使用一個Reducer,看到的輸入為
a#1 [1,3,100]
b#1 [1,2]
z#3 [3]
最後的輸出為
a 1
a 3
a 100
b 1
b 2
z 3