MapReduce的shuffle過程

       這篇博客實際上是http://blog.csdn.net/gyflyx/article/details/16831015的修改版。因其部分語句不通,且說得較爲繁瑣,所以對其稍加修改。


       MapReduce是一個非常流行的分佈式計算框架,它被設計用於並行計算海量數據。第一個提出該技術框架的是Google 公司,而Google 的靈感則來自於函數式編程語言,如LISP,Scheme,ML 等。

       MapReduce的核心步驟主要分兩部分:map和reduce。當你向MapReduce提交一個計算作業時,它會首先把計算作業拆分成若干個map任務,然後分配到不同的節點上去執行,每一個map任務處理輸入數據中的一部分,當map任務完成後,它會生成一些中間文件,這些中間文件將會作爲reduce 任務的輸入數據。reduce 任務的主要目標就是把前面若干個map的輸出彙總到一起並輸出。

       本文的重點是剖析MapReduce的核心過程——shuffle。shuffle是指從Map產生輸出開始,包括系統執行排序以及傳送map輸出到reduce作爲輸入的過程。這裏我們將去探究shuffle是如何工作的,因爲對基礎的理解有助於對MapReduce程序進行調優。


       首先從map端開始分析。當map開始產生輸出時,它並不是簡單地把數據寫到磁盤,因爲頻繁的磁盤操作會導致性能嚴重下降。它的處理過程更復雜,數據首先是寫到內存中的一個緩衝區,並做了一些預排序,以提升效率。

       每個map任務都有一個用來寫入輸出數據的循環內存緩衝區。這個緩衝區默認大小是100MB,可以通過io.sort.mb屬性來設置具體大小。當緩衝區中的數據量達到一個特定閥值時,系統將會啓動一個後臺線程把緩衝區中的內容spill 到磁盤。在spill 過程中,map的輸出將會繼續寫入到緩衝區,但如果緩衝區已滿,map就會被阻塞直到spill 完成。spill線程在把緩衝區的數據寫到磁盤前,會對它進行一個二次快速排序,首先根據數據所屬的partition 排序,然後每個partition中再按key排序。輸出包括一個索引文件和數據文件。如果設定了Combiner,將在排序輸出的基礎上運行。Combiner 就是一個mini reducer,它在執行map任務的節點本身運行,先對map的輸出做一次簡單reduce,使得map的輸出更少,更少的數據會被寫入磁盤和傳送到reducer。spill 文件保存在由mapred.local.dir指定的目錄中,map 任務結束後刪除。

       每當內存中的數據達到spill 閥值的時候,都會產生一個新的spill 文件,所以在map任務寫完它的最後一個輸出記錄時,可能會有多個spill 文件。在map任務完成前,所有的spill文件將會被歸併排序爲一個索引文件和數據文件。這是一個多路歸併過程,最大歸併路數由io.sort.factor 控制。如果設定了Combiner,並且spill文件的數量至少是3,那麼Combiner 將在輸出文件被寫入磁盤前運行以壓縮數據。

       對寫入到磁盤的數據進行壓縮通常是一個很好的方法,因爲這樣做使得數據寫入磁盤的速度更快,節省磁盤空間,並減少需要傳送到reducer 的數據量。默認輸出是不被壓縮的, 但可以很簡單的設置mapred.compress.map.output 爲true 啓用該功能。壓縮所使用的庫由mapred.map.output.compression.codec 來設定。

       當spill 文件歸併完畢後,map將刪除所有的臨時spill 文件,並告知TaskTracker 任務已完成。reducers 通過HTTP 來獲取對應的數據。用來傳輸partitions 數據的工作線程數由tasktracker.http.threads 控制,這個設定是針對每一個TaskTracker 的,並不是單個map,默認值爲40,在運行大作業的大集羣上可以增大以提升數據傳輸速率。


       現在讓我們轉到shuffle的reduce 部分。map的輸出文件放置在運行map任務的TaskTracker 的本地磁盤上(注意:map 輸出總是寫到本地磁盤,但reduce 輸出不是,一般是寫到HDFS),它是運行reduce 任務的TaskTracker 所需要的輸入數據。reduce 任務的輸入數據分佈在集羣內的多個map任務的輸出中,map任務可能會在不同的時間內完成,只要有其中的一個map任務完成,reduce任務就開始拷貝它的輸出。這個階段稱之爲拷貝階段。reduce任務擁有多個拷貝線程,可以並行的獲取map輸出。可以通過設定mapred.reduce.parallel.copies 來改變線程數,默認是5。

       reducer 是怎麼知道從哪些TaskTrackers 中獲取map的輸出呢?當map任務完成之後,會通知它們的父TaskTracker,告知狀態更新,然後TaskTracker 再轉告JobTracker。這些通知信息是通過心跳通信機制傳輸的。因此針對一個特定的作業,JobTracker 知道map輸出與TaskTrackers 的映射關係。reducer 中有一個線程會間歇地向JobTracker 詢問map輸出的地址,直到把所有的數據都取到。在reducer 取走了map 輸出之後,TaskTrackers 不會立即刪除這些數據,因爲reducer 可能會失敗。它們會在整個作業完成後,JobTracker告知它們要刪除的時候纔去刪除。

       如果map輸出足夠小,它們會被拷貝到reduce TaskTracker 的內存中;如果緩衝區空間不足,會被拷貝到磁盤上。當內存中的緩衝區用量達到一定比例閥值,或者達到了map輸出的閥值大小,緩衝區中的數據將會被歸併然後spill 到磁盤。

       拷貝來的數據疊加在磁盤上,有一個後臺線程會將它們歸併爲更大的排序文件,這樣做節省了後期歸併的時間。對於經過壓縮的map輸出,系統會自動把它們解壓到內存方便對其執行歸併。這個階段會對所有的map輸出進行歸併排序,這個工作會重複多次才能完成。

       假設這裏有50 個map輸出(可能有保存在內存中的),並且歸併因子是10,那最終需要5 次歸併。每次歸併會把10個文件歸併爲一個,最終生成5 箇中間文件。在這一步之後,系統把5 箇中間文件排序後直接送給reduce 函數,省去向磁盤寫數據這一步。最終歸併的數據可以是混合數據,既有內存上的也有磁盤上的。由於歸併的目的是歸併最少的文件數目,使得在最後一次歸併時總文件個數達到歸併因子的數目,所以每次操作所涉及的文件個數在實際中會更微妙些。譬如,如果有40 個文件,並不是每次都歸併10 個最終得到4 個文件,相反第一次只歸併4 個文件,然後再實現3次歸併,每次10 個,最終得到4 個歸併好的文件和6 個未歸併的文件。要注意,這種做法並沒有改變歸併的次數,只是最小化寫入磁盤的數據優化措施,因爲最後一次歸併的數據總是直接送到reduce 函數那裏。

       在reduce 階段,reduce 函數會作用在排序輸出的每一個key上。這個階段的輸出被直接寫到輸出文件系統,一般是HDFS。在HDFS 中,因爲TaskTracker 節點也運行着一個DataNode 進程,所以第一個塊備份會直接寫到本地磁盤。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章