本文主要介紹MapReduce的map與reduce所包含的各各階段
MapReduce中的每個map任務可以細分4個階段:record reader、mapper、combiner和partitioner。map任務的輸出被稱
爲中間鍵和中間值,會被髮送到reducer做後續處理。reduce任務可以分爲4個階段:混排(shuffle)、排序(sort)、reducer
和輸出格式(output format)。map任務運行的節點會優先選擇在數據所在的節點,因此,一般可以通過在本地機器上進行計算
來減少數據的網絡傳輸。
Mapreduce作業的輸入是一系列存儲在Hadoop分佈式文件系統(HDFS)上的文件。在MapReduce中,這些文件通過輸入格
式(input format)被分成了一系列的輸入split(input split)。輸入split可以看做是文件在字節層面的分塊表示,每個split由一
個map任務負責處理。
record reader
record reader通過輸入格式將輸入split解析成記錄。record reader的目的只是將輸入數據解析成記錄,但不負責解析記錄
本身。它將數據轉化爲鍵/值(key/value)對的形式,並傳遞給mapper處理。通常鍵是數據在文件中的位置,值是組成記錄的
數據塊。
map
在mapper中,用戶定義的map代碼通過處理record reader解析的每個鍵/值對來產生0個或多個新的鍵/值對結果。鍵/值的
選擇對MapReduce作業的完成效率來說非常重要。鍵是數據在reducer中處理時被分組的依據,值是reducer需要分析的數
據。
combiner
combiner是一個可選的本地reducer,可以在map階段聚合數據。combiner通過執行用戶指定的來自mapper的中間鍵對
map的中間結果做單個map範圍內的聚合。
例如,一個聚合的計數是每個部分計數的總和,用戶可以先將每個中間結果取和,再將中間結果的和相加,從而得到最終
結果。
在很多情況下,這樣可以明顯地減少通過網絡傳輸的數據量。在網絡上發送一次(hello,3)要比三次(hello,1)節省更
多的字節量。通過combiner可以產生特別大的性能提升,並且沒有副作用,因此combiner的應用非常廣泛。
partitioner
partitioner的作用是將mapper(如果使用了combiner的話就是combiner)輸出的鍵/值對拆分爲分片(shard),每reducer
對應一個分片。默認情況下,partitioner先計算目標的散列值(通常爲md5值)。然後,通過reducer個數執行取模運算
key.hashCode()%(reducer的個數)。這種方式不僅能夠隨機地將整個鍵空間平均分發給每個reducer,同時也能確保不同mapper
產生的相同鍵能被分發至同一個reducer。用戶可以定製partitioner的默認行爲,並可以使用更高級的模式,如排序。當然,一
般情況下是不需要改寫partitioner的。對於每個map任務,其分好區的數據最終會寫入本地文件系統,等待其各自的reducer拉
取。
混排和排序reduce任務開始於混排和排序這一步驟。該步驟主要是將所有partitioner寫入的輸出文件拉取到運行reducer的本地機器
上,然後將這些數據按照鍵排序並寫到一個較大的數據列表中。排序的目的是將相同鍵的記錄聚合在一起,這樣其所對應的值
就可以很方便地在reduce任務中進行迭代處理。這個過程完全不可定製,而且是由框架自動處理的。開發人員只能通過自定義
Comparator對象來確定鍵如何排序和分組。
reduce
reducer將已經分好組的數據作爲輸入,並依次爲每個鍵對應分組執行reduce函數。reduce函數的輸入是鍵以及包含與該鍵
對應的所有值的迭代器。在後文介紹的模式中,我們將看到在這個函數中有很多種處理方法。這些數據可以被聚合、過濾或以
多種方式合併。當reduce函數執行完畢後,會將0個或多個鍵/值對發送到最後的處理步驟——輸出格式。和map函數一樣,因
爲reduce函數是業務處理邏輯的核心部分,所以不同作業的reduce函數也是不相同。
輸出格式
輸出格式獲取reduce函數輸出的最終鍵/值對,並通過record write將它寫入到輸出文件中。每條記錄的鍵和值默認通過tab
分隔,不同記錄通過換行符分隔。雖然一般情況下可以通過自定義實現非常多的輸出格式,但是,不管什麼格式,最終的結果
都將寫到HDFS上。
附:
Mapper、Reducer類的源碼,通過源碼我們可以瞭解到在map、reduce階段也可以做一些自定義處理。
Mapper類中有4個主要的函數分別是:setup、map、cleanup、run,源碼如下:
由源碼可知,當調用到map時,通常會先執行一個setup函數,最後會執行一個cleanup函數。而默認情況下,這兩個函數的內容都是nothing。因此,public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> { /** * The <code>Context</code> passed on to the {@link Mapper} implementations. */ public abstract class Context implements MapContext<KEYIN,VALUEIN,KEYOUT,VALUEOUT> { } /** * Called once at the beginning of the task. */ protected void setup(Context context ) throws IOException, InterruptedException { // NOTHING } /** * Called once for each key/value pair in the input split. Most applications * should override this, but the default is the identity function. */ @SuppressWarnings("unchecked") protected void map(KEYIN key, VALUEIN value, Context context) throws IOException, InterruptedException { context.write((KEYOUT) key, (VALUEOUT) value); } /** * Called once at the end of the task. */ protected void cleanup(Context context ) throws IOException, InterruptedException { // NOTHING } /** * Expert users can override this method for more complete control over the * execution of the Mapper. * @param context * @throws IOException */ public void run(Context context) throws IOException, InterruptedException { setup(context); try { while (context.nextKeyValue()) { map(context.getCurrentKey(), context.getCurrentValue(), context); } } finally { cleanup(context); } } }
當map方法不符合應用要求時,可以試着通過增加setup和cleanup的內容來滿足應用的需求。
Recuder類中也有4個主要的函數分別是:setup、reduce、cleanup、run,源碼如下:
由源碼可知,當調用到reduce時,通常會先執行一個setup函數,最後會執行一個cleanup函數。而默認情況下,這兩個函數的內容都是nothing。因此public class Reducer<KEYIN,VALUEIN,KEYOUT,VALUEOUT> { public abstract class Context implements ReduceContext<KEYIN,VALUEIN,KEYOUT,VALUEOUT> { } protected void setup(Context context ) throws IOException, InterruptedException { // NOTHING } @SuppressWarnings("unchecked") protected void reduce(KEYIN key, Iterable<VALUEIN> values, Context context ) throws IOException, InterruptedException { for(VALUEIN value: values) { context.write((KEYOUT) key, (VALUEOUT) value); } } protected void cleanup(Context context ) throws IOException, InterruptedException { // NOTHING } public void run(Context context) throws IOException, InterruptedException { setup(context); try { while (context.nextKey()) { reduce(context.getCurrentKey(), context.getValues(), context); // If a back up store is used, reset it Iterator<VALUEIN> iter = context.getValues().iterator(); if(iter instanceof ReduceContext.ValueIterator) { ((ReduceContext.ValueIterator<VALUEIN>)iter).resetBackupStore(); } } } finally { cleanup(context); } } }
一般情況下reduce是中的setup和cleanup是沒有內容的,當處理結果不符合或需要更多處理時我們要編寫setup和cleanup的內容。