MapReduce的Shuffle過程詳解

在前一篇博文,我已經講解了MapReduce的過程。這一篇文章,我將會就MapReduce最重要的一個環節:Shuffle,來做具體的講解,這部分內容將幫助你優化你的mapreduce程序,使你的程序更加高效。本文主要引用了http://langyu.iteye.com/blog/992916的內容,並做了一些更改。

(一)什麼是shuffle

Shuffle過程是MapReduce的核心(也被稱爲奇蹟發生的地方),要想深入理解MapReduce,Shuffle是必須要了解的。本人也看過不少相關資料,但每次看完都雲裏霧裏的,很難理清大致的邏輯。最近一直在做MapReduce job性能調優的工作,不得已深入代碼研究了MapReduce的運行機制,這纔對Shuffle探了個究竟。如果你對這篇博文有任何疑問或建議,歡迎大家留言討論。

Shuffle的意思是洗牌(弄亂),可能大家更熟悉的是Java API裏的Collections.shuffle(List<T>)方法,它會隨機地打亂list裏元素的順序。那麼MapReduce裏Shuffle是什麼,請看這張圖: 


這張是官方對Shuffle過程的描述,單從這張圖你基本不可能明白Shuffle的過程,因爲它與事實相差挺多,細節也是錯亂的。後面我會具體描述Shuffle的事實情況,所以這裏你只要清楚Shuffle的大致範圍就可以:怎樣把map task的輸出結果有效地傳送到reduce端。也可以這樣理解,Shuffle描述着數據從map task輸出到reduce task輸入的這段過程。 

        在Hadoop這樣的集羣環境中,大部分map task與reduce task的執行是在不同的節點上。很多情況下Reduce執行時需要跨節點去拉取其它節點上的map task結果。如果集羣正在運行的job有很多,那麼task的正常執行對集羣內部的網絡資源消耗很嚴重。這種網絡消耗是正常的,我們不能限制,能做的就是最大化地減少不必要的消耗。還有在節點內,相比於內存操作,磁盤IO對job完成時間的影響也是可觀的。從最基本的要求來說,我們對Shuffle過程的期望可以有: 

(1)完整地從map task端拉取數據到reduce端。

(2)在跨節點拉取數據時,儘可能地減少對帶寬的不必要消耗。

(3)減少磁盤IO對task執行的影響。

這裏大家可以想想,如果是自己來設計這段Shuffle過程,那麼你的設計目標是什麼,想能優化的地方主要在於減少拉取數據的量,並儘量使用內存而不是磁盤
接下來,以WordCount爲例,假設它有8個map task和3個reduce task。從上圖看出,Shuffle過程橫跨map與reduce兩端,所以下面我也會分兩部分來展開。 
先看看map端的情況,如下圖: 

        上圖可能是某個map task的運行情況。拿它與官方圖的左半邊比較,會發現很多不一致。官方圖沒有清楚地說明partition, sort與combiner到底作用在哪個階段。通過這張圖,大家能清晰地瞭解從map數據輸入到map端所有數據準備好的全過程。 
        整個流程分了四步。簡單些可以這樣說,每個map task都有一個環形內存緩衝區(kv buffer),存儲着map的輸出結果,當緩衝區快滿的時候需要將緩衝區的數據以一個臨時文件的方式存放到磁盤(spill),當整個map task結束後再對磁盤中這個map task產生的所有臨時文件做合併,生成最終的正式輸出文件,然後等待reduce task來拉數據。 

當然這裏的每一步都可能包含着多個步驟與細節,下面我對細節來一一說明: 

(1)在map task執行時,它的輸入數據來源於HDFS的block,當然在MapReduce概念中,map task只讀取split。Split與block的對應關係可能是多對一,默認是一對一。在WordCount例子裏,假設map的輸入數據都是像“aaa”這樣的字符串。

(2)在經過mapper的運行後,我們得知mapper的輸出是這樣一個key/value對: key是“aaa”, value是數值1。因爲當前map端只做加1的操作,在reduce task裏纔去合併結果集。前面我們知道這個job有3個reduce task,到底當前的“aaa”應該交由哪個reduce去做呢,是需要現在決定的。 
MapReduce提供Partitioner接口,它的作用就是根據key或value及reduce的數量來決定當前的這對輸出數據最終應該交由哪個reduce task處理。默認對key取hash後再以reduce task數量取模。默認的取模方式只是爲了平均reduce的處理能力,如果用戶自己對Partitioner有需求,可以訂製並設置到job上。 
在我們的例子中,“aaa”經過Partitioner後返回0,也就是這對值應當交由第一個reducer來處理。接下來,需要將數據寫入內存緩衝區中,緩衝區的作用是批量收集map結果,減少磁盤IO的影響。我們的kv對以及Partition的結果都會被寫入緩衝區。寫入之前,key與value值都會被序列化成字節數組。 

整個內存緩衝區就是一個字節數組,它的字節索引及key/value存儲結構我沒有研究過。

(3)這個內存緩衝區是有大小限制的,默認是100MB(參數mapreduce.task.io.sort.mb控制)。當map task的輸出結果很多時,就可能會撐爆內存,所以需要在一定條件下將緩衝區中的數據臨時寫入磁盤,然後重新利用這塊緩衝區。這個從內存往磁盤寫數據的過程被稱爲Spill(溢寫)。這個溢寫是由單獨線程完成的,不影響往緩衝區寫map結果的線程。溢寫線程啓動時,map結果也要正常向緩衝區寫kv對,這個操作不應受spill操作影響。所以整個緩衝區有個溢寫的比例spill.percent(由參數mapreduce.map.sort.spill.percent控制)。這個比例默認是0.8,也就是當緩衝區的數據已經達到閾值(buffer size * spill percent = 100MB * 0.8 = 80MB),溢寫線程啓動,鎖定這80MB的內存,執行溢寫過程。Map task的輸出結果還可以往剩下的20MB內存中寫,互不影響。 當溢寫線程啓動後,需要對這80MB空間內的key做排序(Sort)。排序是MapReduce默認的行爲,是對序列化的字節做的排序。 
因爲map task的輸出是需要發送到不同的reduce端去,而內存緩衝區沒有對將發送到相同reduce端的數據做合併,那麼這種合併應該是體現是磁盤文件中的。從官方圖上也可以看到寫到磁盤中的溢寫文件是對不同的reduce端的數值做過合併。所以溢寫過程一個很重要的細節在於,如果有很多個key/value對需要發送到某個reduce端去,那麼需要將這些key/value值拼接到一塊,減少與partition相關的索引記錄。 
在針對每個reduce端而合併數據時,有些數據可能像這樣:“aaa”/1, “aaa”/1。對於WordCount例子,就是簡單地統計單詞出現的次數,如果在同一個map task的結果中有很多個像“aaa”一樣出現多次的key,我們就應該把它們的值合併到一塊,這個過程叫reduce也叫combine。但MapReduce的術語中,reduce只指reduce端執行從多個map task取數據做計算的過程。除reduce外,非正式地合併數據只能算做combine了。其實MapReduce中將Combiner等同於Reducer。 

如果client設置過Combiner,那麼現在就是使用Combiner的時候了。將有相同key的key/value對的value加起來,減少溢寫到磁盤的數據量。Combiner會優化MapReduce的中間結果,所以它在整個模型中會多次使用。那哪些場景才能使用Combiner呢?從這裏分析,Combiner的輸出是Reducer的輸入,Combiner絕不能改變最終的計算結果。所以從我的想法來看,Combiner只應該用於那種Reduce的輸入key/value與輸出key/value類型完全一致,且不影響最終結果的場景。比如累加,最大值等。Combiner的使用一定得慎重,如果用好,它對job執行效率有幫助,反之會影響reduce的最終結果。 

(4) 每次spill會在磁盤上生成一個溢寫文件,如果map的輸出結果真的很大,有多次這樣的溢寫發生,磁盤上相應的就會有多個溢寫文件存在。當map task真正完成時,內存緩衝區中的數據也全部溢寫到磁盤中形成一個溢寫文件。最終磁盤中會至少有一個這樣的溢寫文件存在(如果map的輸出結果很少,可能只會產生一個溢寫文件),因爲最終的文件只有一個,故需要將這些溢寫文件做merge。Merge是怎樣實現的,如前面的例子,“aaa”從某個map task讀取過來時值是5,從另外一個map讀取時值是8,因爲它們有相同的key,所以被merge成group,即{“aaa”, [5, 8, 2, …]}的形式,數組中的值就是從不同spill文件中讀取出來的,然後再把這些值加起來。請注意,因爲merge是將多個spill文件合併到一個文件,所以可能也有相同的key存在。這個過程中,如果client設置過Combiner,也會使用Combiner來合併相同的key。 
至此,map端的所有工作都已結束,最終生成的這個文件也存放在TaskTracker能夠找到的某個本地目錄內。每個reduce task不斷地通過RPC從JobTracker那裏獲取map task是否完成的信息,如果reduce task得到通知,獲知某臺TaskTracker上的map task執行完成,Shuffle的後半段過程開始啓動。 

簡單講,reduce task在執行之前的工作就是不斷地拉取當前job裏每個map task的最終結果,然後對從不同地方拉取過來的數據不斷地做merge,也最終形成一個文件作爲reduce task的輸入文件。見下圖:
 

如同map端的細節圖,Shuffle在reduce端的過程也能用圖上標明的三點來概括。當前reduce copy數據的前提是它要從JobTracker獲得有哪些map task已執行結束。Reducer真正運行之前,所有的時間都是在拉取數據,做merge,且不斷重複地在做。如前面的方式一樣,接下來,分段地描述reduce端的Shuffle細節: 

(1)Copy過程(拉取數據):Reduce進程啓動一些數據copy線程(Fetcher),通過HTTP方式請求map task所在的TaskTracker獲取map task的輸出文件。因爲map task早已結束,這些文件就歸TaskTracker管理在本地磁盤中。

(2)Merge階段:reduce端的merge與map端的merge動作相似,只是數組中存放的是不同map端copy來的數值。Copy過來的數據會先放入內存緩衝區中,這裏的緩衝區大小要比map端的更靈活,它基於JVM的heap size設置,因爲Shuffle階段Reducer不運行,所以應該把絕大部分的內存都給Shuffle用。需要強調的是,merge有三種形式:內存到內存;內存到磁盤;磁盤到磁盤。默認情況下第一種形式不啓用。當內存中的數據量到達一定閾值,就啓動內存到磁盤的merge。與map 端類似,這也是溢寫的過程,這個過程中如果設置有Combiner,也會啓用的,然後在磁盤中生成了衆多溢寫文件。第二種merge方式一直在運行,直到沒有map端的數據時才結束,然後啓動第三種磁盤到磁盤的merge方式生成最終的那個文件。 

(3)Reducer的輸入文件。不斷地merge後,最後會生成一個“最終文件”。爲什麼加引號?因爲這個文件可能存在於磁盤上,也可能存在於內存中。對我們來說,當然希望它存放於內存中,直接作爲Reducer的輸入,但默認情況下,這個文件是存放於磁盤中的。當Reducer的輸入文件已定,整個Shuffle才最終結束。然後就是Reducer執行,把結果放到HDFS上。 

上面就是整個Shuffle的過程。很多細節都略過了,只試着把要點說明白。當然,本人可能也有理解或表述上的很多問題,不吝指點。我希望不斷地完善和修改這篇文章,能讓它通俗、易懂,看完就能知道Shuffle的方方面面。


發佈了38 篇原創文章 · 獲贊 61 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章