微信公衆號:小林玩大數據
作者:林中鳥
如果你覺得此文對你有幫助,歡迎點贊!
1. MapReduce 簡介
MapReduce 編程範式將數據處理拆分成了兩個基本階段:Map 階段與 Reduce 階段。每個階段的輸入和輸出均爲鍵值對。
Map 階段對應的進程爲Mapper。Mapper 是在 JVM 上運行的 Java 進程,通常在要處理的數據節點上啓動。利用數據本地性是 MapReduce 的一個很重要的原則。對於大數據集而言,將處理的進程分配至包含數據的節點上(計算向數據移動),比通過網絡傳輸數據要高效的多。當 Mapper 處理完輸入數據之後,會進入 sort & shuffle 階段,在此階段,數據會進行排序與分區,產生 (k,v,p) 型數據。
Recucer 階段會將分區後的有序數據通過網絡 I/O 發送給 Reducer 所在的 JVM ,Reducer 會讀取這些按照鍵進行分區的有序數據。Reducer 拿到這些記錄,reducer()方法可以對數據執行各種操作。Reducer 通常會把處理好的數據聚合到 HDFS 或 HBase 的存儲中。
總的來說,JVM 有兩類進程,一類讀取無序數據,一類處理分區後的有序數據。
2. MapReduce 的處理特點
- Mapper 以鍵值對形式處理輸入數據,並且每次只能處理一個鍵值對。Mapper 的個數是由框架而非開發人員決定的。
- Mapper 將鍵值對作爲輸出傳遞給 Reducer ,但是不能將數據傳遞給其它 Mapper。Reducer 之間也不能通信。
- Mapper 和 Reducer 通常不會使用太多的內存,一般會將 JVM 的堆大小設置爲相對較小的值。
- 一般來說,每一個 Reducer 都會有單個輸出的數據流,在默認情況下,這是一個文件合集,命名形式如:Part-r-0000、Part-r-00001等。存在客戶端指定的 HDFS 目錄下。
- Mapper 和 Reducer 的輸出均會寫入磁盤。如果 Reducer 輸出的結果還需要額外的處理,那麼整個數據集將會寫入磁盤中,再讀取一遍。這種模式被成爲同步屏障(synchronization barrier)。這也是使用 MapReduce 迭代式處理數據比較低效的主要原因。
注意:MapReduce 有兩大缺點:其一是啓動時間較長。即使 MapReduce 過程幾乎什麼都不做,啓動也需要大概10~30秒;其二是 MapReduce 會頻繁寫入磁盤,以便容錯。這兩大弱點使 MapReduce 不適用於迭代算法。
3. MapReduce 內部實現機制
3.1 MapReduce 執行流程
-
大數據集文件在集羣的 HDFS 中以塊存儲,並把業務邏輯的計算程序上傳到 HDFS 中,客戶端提交作業給ResourceManager,ResourceManager 會在集羣內隨機挑一個不忙的節點,隨機創建一個AppMstr進程。
-
AppMstr 會從 HDFS 中獲取由 InputFormat 實現好的邏輯切片清單信息,並向 ResourceManager 申請計算資源,ResourceManager 會根據切片清單信息返回一個 container 並開始作業。
-
Map 計算過程中,一個 InputSplit 對應一個 Mapper,Map 計算結束後,通過 Hash 算法,計算 key 的hashCode值,再對分區數(partition)取模,實現 (k,v,p) 型數據。(相同的 key 爲一組,最後會被分配到同一個 Reducer)。
-
此時 不會直接寫入磁盤,否則 I/O 切換次數過於頻繁,內部採取了一個 buffer 機制,默認大小爲100M;當緩存滿了 80%(此參數可調)之後,讀取一次 I/O ,把緩存區中的數據根據 p(分區) 溢寫到磁盤,磁盤上的每個文件再根據 key 進行 sort,最終的結果是形成一個個內部由序,外部無序的小文件。
-
當 Map 輸出結束,對磁盤上的小文件進行一次歸併排序,歸併的過程將同一分區的數據放在一起,此時仍然內部的 key 有序;各個 Reduce 會從多個 Map shuffle 屬於自己分區的小文件,shuffle 到 reduce 中再一次歸併成多個全排序文件進行 reduce 計算,形成以 Part-r-00000、Part-r-00001命名的文件,並寫到 HDFS 中。
注意:
1.上述第5步的再一次歸併成多個全排序文件,主要是爲了減少 I/O 時間。
2.上述過程的時間並非線性,在溢寫出採用了時間複用。舉個例子:假設前面有500個 Map ,有20個 Map 跑完了,剩下480個繼續跑,reduce 會把跑完的20個 Map 先 shuffle 來做歸併並計算。
3.雖然上述過程有一次 sort ,兩次歸併。 但是 reduce 沒有重新排序的能力,而是強依賴之前 Map 的 sort 結果。
MapReduce 執行流程圖
MapReduce 的強大之處在於,它不僅僅是由 Map 和 Reduce 任務構成的,其中還包括協同工作的多個組件。每個組件均可以由開發人員擴展。下面從 Map 階段、Reduce 階段兩個方面介紹一個任務執行所涉及的主要組件。
3.2 Map階段
Map 階段主要涉及的組件有:InputFormat、RecordReader、Mapper 的 setuP()、Mapper 的 map()、patitioner(分區器)、Mapper 的cleanup()、Combiner。
- InputFormat Map 的輸入數據主要是數據的邏輯分片,此過程主要由 InputFormat 類實現。該類主要有一下兩個抽象方法:
- getSplits()
這個方法實現的是將輸入分步到多個 Map 進程的具體邏輯。框架默認的是 TextInputFormat ,它會爲每個數據塊生成一個輸入分片,並將對應數據塊的位置發送給 Map 任務。框架會針對每個分片啓動一個 Mapper 進程執行處理。正因如此,開發人員常常假定 MapReduce 任務中,Mapper 的個數就是待處理數據集的數據塊個數。
createRecordReader()
- 這一方法爲 Map 任務提供了 Reader 機制,賦予了 Map 訪問待處理數據的能力,該方法會返回一個記錄讀取器,一般常用的是 LineRecordReader 行記錄讀取器,
//獲取 InputSplit 抽象方法
public abstract List<InputSplit> getSplits(JobContext context)
throws IOException,InterruptedException;
//獲取記錄讀取器抽象方法
public abstract RecordReader<K,V> createRecordReader(InputSplit split,
TaskAttemptContext context) throws IOException, InterruptedException;
-
RecordReader RecordReader 類讀取數據塊的內容,將讀取到的鍵值對記錄返回到 Map 任務。初始化 RecordReader 實例,使用的是需要讀取的數據塊在文件中的起始位置,以及該文件在 HDFS 中的 URI 地址。尋址到這一起始位置之後,每次調用 nextKeyValue()方法查找下一個行分隔符,並讀取下一條記錄。
-
Mapper 的 setuP() 在 Map 任務的 map 方法調用之前,Mapper 的 setup()方法會被首先調用一次。這一方法可以使開發人員初始化 Map 進程中會用到的變量和文件句柄。setup()最常用的作用是從配置對象中獲取配置項的值。
-
Mapper 的 map() Mapper的核心方法就是 map()方法,開發人員需要重寫 map()方法。該方法有三個輸入參數:鍵(key)、值(value)及上下文(context)。其中 key 和 value 可以通過 RecordReader 獲得,它們包含map()方法需要處理的數據。context 對象爲 Mapper 更多行爲的實現提供了支持:將輸出發送給 Reduceer,從Configuration 對象中讀取值,計數器自增以彙報 Map 任務進度。
-
Partitioner 分區器實現了在 Reducer 之間進行數據分區的邏輯。默認的 Partitioner 處理邏輯:首先獲得 key 的標準哈希函數散列,再與 Reducer(開發人員可控制數量) 數取模,餘數則決定這條記錄的目標 Reducer。
-
Combiner MapReduce 中的Combiner可以提供一種簡單的方法,減少 Mapper 和 Reducer 間的數據傳輸。在 buffer 中,數據 sort(默認快排)完之後,如果定義了 Combiner,你就可以調用 combine()方法,對 Mapper 產生的數據進行聚合操作了,因爲這個方法會在 Mapper 執行的節點上執行,所以這種聚合行爲可以減少網絡 I/O。
-
Mapper 的 cleanup() cleanup()方法在 map 方法處理了所有的數據之後調用。這裏通常要執行文件的關閉操作,以及最後的報告和總結,比如將最終狀態寫入日誌中。
3.3 Reduce 階段
Reducer 任務沒有 Map 任務那麼複雜,主要涉及的組件有:Shuffle、Reducer 的 setup()、Reducer 的reduce()、OutputFormat、Reducer 的 cleanup()。
-
Shuffle 在 Reducer 開始之前,Reducer 任務會將 Mapper 的輸出從 Map 節點複製到 Reducer 節點中,這個過程稱爲 Shuffle。每一個 Reducer 都需要從多個 Mapper Shuffle 數據,所以我們要讓每一個 Reducer 按照 Map 任務的方式讀取本地的數據,提高處理性能。
-
Reducer 的 setup() Reducer 的 setup()步驟與 Map 的 setup()相似。這一方法在 Reducer 處理單個記錄之前,通常用來初始化變量和文件句柄。
-
Reducer 的 reduce() reduce()方法是進行絕大多數數據處理的地方。與 Map 的 map()方法類似:
但在輸入方面有幾點不同,首先,key 是有序的;其次,value 從原來的一個 value 變成了 values(從一次處理一個值,變爲一次處理多個值)。
在輸出方面,map()方法中,調用 context.write(k,v)方法可以將輸出放入緩存區,緩存區中的數據會被 sort,然後被 Reducer 讀取。而在 reduce()方法中,調用 context.write(k,v)方法會將輸出發送給 OutputFormat。 -
OutputFormat 在 Map 階段,InputFormat 負責處理輸入數據的讀取,OutputFormat 負責數據的格式化和把輸出數據寫出(通常是寫到 HDFS)。對於 OutputFormat 類來說,一個 Reducer 只會寫一個文件,因此在 HDFS 上看到一個 Reducer 對應一個 輸出文件。
林中鳥兒們,這期就和大家分享到這兒!掃一掃來個關注,下期爲大家解析 MapReduce 關鍵組件的源碼!敬請期待!
林中鳥公衆號