文章目錄
1. MapReduce介紹
1.1 MapReduce的基本思想
MapReduce思想在生活中處處可見。或多或少都曾接觸過這種思想。MapReduce的思想核心是“分而治之”,適用於大量複雜的任務處理場景(大規模數據處理場景)。
MapReduce分爲Map和Reduce兩個階段,Map負責“分”,即把複雜的任務分解爲若干個“簡單的任務”來並行處理,這些小任務可以並行計算,彼此間幾乎沒有依賴關係。Reduce負責“合”,即對map階段的結果進行全局彙總。MapReduce運行在yarn集羣
下圖用介紹了類似MapReduce思想的一個實現過程
在實際的MapReduce執行過程中,可能有多個Reduce
1.2 MapReduce的設計
MapReduce是一個分佈式運算程序的編程框架,核心功能是將用戶編寫的業務邏輯代碼和自帶默認組件整合成一個完整的分佈式運算程序,併發運行在Hadoop集羣上。
MapReduce設計並提供了統一的計算框架,爲程序員隱藏了絕大多數系統層面的處理細節。爲程序員提供一個抽象和高層的編程接口和框架。程序員僅需要關心其應用層的具體計算問題,僅需編寫少量的處理應用本身計算問題的程序代碼。如何具體完成這個並行計算任務所相關的諸多系統層細節被隱藏起來,交給計算框架去處理:
Map和Reduce爲程序員提供了一個清晰的操作接口抽象描述。MapReduce中定義瞭如下的Map和Reduce兩個抽象的編程接口,由用戶去編程實現.Map和Reduce,MapReduce處理的數據類型是**<key,value>鍵值對**。
- Map:
(k1; v1) → [(k2; v2)]
- Reduce:
(k2; [v2]) → [(k3; v3)]
1.3 MapReduce在yarn集羣上的執行流程
-
MRAppMaster 負責整個程序的過程調度及狀態協調
-
MapTask負責map階段的整個數據處理流程
-
ReduceTask負責reduce階段的整個數據處理流程
2. MapReduce運行機制詳解
2.1 Map階段
Map階段又可以分爲Map Task階段和Map shuffle階段,如下圖所示
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zqLW66Jf-1581582983148)(7\1581407538401.png)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-WL9H2P5l-1581582983149)(7\1581407566690.png)]
-
讀取數據組件 InputFormat (默認 TextInputFormat) 會通過
getSplits
方法對輸入目錄中文件進行邏輯切片規劃得到 block, 有多少個block
就對應啓動多少個MapTask
. -
將輸入文件切分爲
block
之後, 由 RecordReader對象 (默認是LineRecordReader) 進行讀取, 以\n
作爲分隔符, 讀取一行數據, 返回<key,value>
. Key 表示每行首字符偏移值, Value 表示這一行文本內容(K1,V1) -
讀取
block
返回<key,value>
, 進入用戶自己繼承的 Mapper 類中,執行用戶重寫的 map 函數, RecordReader 讀取一行這裏調用一次 -
Mapper 邏輯結束之後, 將 Mapper 的每條結果通過 context.write進行collect數據收集. 在 collect 中, 會先對其進行分區處理,默認使用 HashPartitioner
-
MapReduce 提供
Partitioner
接口, 它的作用就是根據Key
或Value
及Reducer
的數量來決定當前的這對輸出數據最終應該交由哪個Reduce task
處理, 默認對 Key Hash 後再以 Reducer 數量取模. 默認的取模方式只是爲了平均 Reducer 的處理能力, 如果用戶自己對 Partitioner 有需求, 可以訂製並設置到 Job 上
-
-
接下來, 會將數據寫入內存, 內存中這片區域叫做環形緩衝區(臨時數據收集), 緩衝區的作用是批量收集 Mapper 結果, 減少磁盤 IO 的影響. 我們的 Key/Value 對以及 Partition 的結果都會被寫入緩衝區. 當然, 寫入之前,Key 與 Value 值都會被序列化成字節數組
-
環形緩衝區其實是一個數組, 數組中存放着 Key, Value 的序列化數據和 Key, Value 的元數據信息, 包括 Partition, Key 的起始位置, Value 的起始位置以及 Value 的長度. 環形結構是一個抽象概念
-
緩衝區是有大小限制, 默認是 100MB. 當 Mapper 的輸出結果很多時, 就可能會撐爆內存, 所以需要在一定條件下將緩衝區中的數據臨時寫入磁盤, 然後重新利用這塊緩衝區. 這個從內存往磁盤寫數據的過程被稱爲 Spill, 中文可譯爲溢寫. 這個溢寫是由單獨線程來完成, 不影響往緩衝區寫 Mapper 結果的線程. 溢寫線程啓動時不應該阻止 Mapper 的結果輸出, 所以整個緩衝區有個溢寫的比例
spill.percent
. 這個比例默認是 0.8, 也就是當緩衝區的數據已經達到閾值buffer size * spill percent = 100MB * 0.8 = 80MB
, 溢寫線程啓動, 鎖定這 80MB 的內存, 執行溢寫過程. Mapper 的輸出結果還可以往剩下的 20MB 內存中寫, 互不影響
-
-
當溢寫線程啓動後, 需要對這 80MB 空間內的 Key 做排序 (Sort). 排序是 MapReduce 模型默認的行爲, 這裏的排序也是對序列化的字節做的排序
-
如果 Job 設置過 Combiner, 那麼現在就是使用 Combiner 的時候了. 將有相同 Key 的 Key/Value 對的 Value 加起來, 減少溢寫到磁盤的數據量. Combiner 會優化 MapReduce 的中間結果, 所以它在整個模型中會多次使用
-
那哪些場景才能使用 Combiner 呢? 從這裏分析, Combiner 的輸出是 Reducer 的輸入, Combiner 絕不能改變最終的計算結果. Combiner 只應該用於那種 Reduce 的輸入 Key/Value 與輸出 Key/Value 類型完全一致, 且不影響最終結果的場景. 比如累加, 最大值等. Combiner 的使用一定得慎重, 如果用好, 它對 Job 執行效率有幫助, 反之會影響 Reducer 的最終結果
-
-
合併溢寫文件, 每次溢寫會在磁盤上生成一個臨時文件 (寫之前判斷是否有 Combiner), 如果 Mapper 的輸出結果真的很大, 有多次這樣的溢寫發生, 磁盤上相應的就會有多個臨時文件存在. 當整個數據處理結束之後開始對磁盤中的臨時文件進行 Merge 合併, 因爲最終的文件只有一個, 寫入磁盤, 並且爲這個文件提供了一個索引文件, 以記錄每個reduce對應數據的偏移量
配置hadoop時可以對溢寫進行配置,在hadoop/mapre-site.xml
中進行配置
配置 | 默認值 | 解釋 |
---|---|---|
mapreduce.task.io.sort.mb |
100 | 設置環型緩衝區的內存值大小 |
mapreduce.map.sort.spill.percent |
0.8 | 設置溢寫的比例 |
mapreduce.cluster.local.dir |
${hadoop.tmp.dir}/mapred/local |
溢寫數據目錄 |
mapreduce.task.io.sort.factor |
10 | 設置一次合併多少個溢寫文件 |
2.1 Reduce階段
Reduce階段分爲Reduce shuffle階段和Reduce Task階段,如下圖所示
- Copy階段,簡單地拉取數據。Reduce進程啓動一些數據copy線程(Fetcher),通過HTTP方式請求maptask獲取屬於自己的文件。
- Merge階段。這裏的merge如map端的merge動作,只是數組中存放的是不同map端copy來的數值。Copy過來的數據會先放入內存緩衝區中,這裏的緩衝區大小要比map端的更爲靈活。merge有三種形式:內存到內存;內存到磁盤;磁盤到磁盤。默認情況下第一種形式不啓用。當內存中的數據量到達一定閾值,就啓動內存到磁盤的merge。與map 端類似,這也是溢寫的過程,這個過程中如果你設置有Combiner,也是會啓用的,然後在磁盤中生成了衆多的溢寫文件。第二種merge方式一直在運行,直到沒有map端的數據時才結束,然後啓動第三種磁盤到磁盤的merge方式生成最終的文件。
- 合併排序。把分散的數據合併成一個大的數據後,還會再對合並後的數據排序。
- 對排序後的鍵值對調用reduce方法,鍵相等的鍵值對調用一次reduce方法,每次調用會產生零個或者多個鍵值對,最後把這些輸出的鍵值對寫入到HDFS文件中。
3. MapReduce編程規範
MapReduce 的開發一共有八個步驟, 其中 Map 階段分爲 2 個步驟,Shuffle 階段 4 個步驟,Reduce 階段分爲 2 個步驟
Map 階段 2 個步驟
- 設置 InputFormat 類, 將數據切分爲 Key-Value**(K1和V1)** 對, 輸入到第二步
- 自定義 Map 邏輯, 將第一步的結果轉換成另外的 Key-Value(K2和V2) 對, 輸出結果
Shuffle 階段 4 個步驟
- 對輸出的 Key-Value 對進行分區
- 對不同分區的數據按照相同的 Key 排序
- 對數據進行分組, 相同 Key 的 Value 放入一個集合中
- (可選) 對分組過的數據初步規約, 降低數據的網絡拷貝
-
在 MapReduce 中, 通過我們指定分區, 會將同一個分區的數據發送到同一個 Reduce 當中進行處理Reduce默認分區只有一個,但是在實際應用中可能有多個結果文件輸出,因此往往需要重新設置分區,分區的簡單流程如下圖所示
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-XmzoAfEU-1581582983155)(7\1581044894242.png)]
-
規約(Combiner) 的作用就是對 map 端的輸出先做一次合併,以減少在 map 和 reduce 節點之間的數據傳輸量,以提高網絡IO 性能,是 MapReduce 的一種優化手段之一
- combiner 是 MR 程序中 Mapper 和 Reducer 之外的一種組件
- combiner 組件的父類就是 Reducer
- combiner 和 reducer 的區別在於運行的位置
- Combiner 是在每一個 maptask 所在的節點運行
- Reducer 是接收全局所有 Mapper 的輸出結果
- combiner 的意義就是對每一個 maptask 的輸出進行局部彙總,以減小網絡傳輸量
Reduce 階段 2 個步驟
- 對多個 Map 任務的結果進行排序以及合併, 編寫 Reduce 函數實現自己的邏輯, 對輸入的 Key-Value 進行處理, 轉爲新的 Key-Value(K3和V3)輸出
- 設置 OutputFormat 處理並保存 Reduce 輸出的 Key-Value 數據
5. MapReduce中的計數器
計數器是收集作業統計信息的有效手段之一,用於質量控制或應用級統計。計數器還可輔助診斷系統故障。如果需要將日誌信息傳輸到 map 或 reduce 任務, 更好的方法通常是看能否用一個計數器值來記錄某一特定事件的發生。對於大型分佈式作業而言,使用計數器更爲方便。除了因爲獲取計數器值比輸出日誌更方便,還有根據計數器值統計特定事件的發生次數要比分析一堆日誌文件容易得多。
- hadoop內置計數器列表
MapReduce任務計數器 | org.apache.hadoop.mapreduce.TaskCounter |
---|---|
文件系統計數器 | org.apache.hadoop.mapreduce.FileSystemCounter |
FileInputFormat計數器 | org.apache.hadoop.mapreduce.lib.input.FileInputFormatCounter |
FileOutputFormat計數器 | org.apache.hadoop.mapreduce.lib.output.FileOutputFormatCounter |
作業計數器 | org.apache.hadoop.mapreduce.JobCounter |
每次mapreduce執行完成之後,我們都會看到一些日誌記錄出來,其中包括一些重要的日誌記錄
-
除內置計數器之外,我們還可以自定義計數器
-
通過context上下文對象,在map端使用計數器進行統計
public class PartitionMapper extends Mapper<LongWritable,Text,Text,NullWritable>{ //map方法將K1和V1轉爲K2和V2 @Override protected void map(LongWritable key, Text value, Context context) throws Exception{ Counter counter = context.getCounter("MR_COUNT", "MyRecordCounter"); counter.increment(1L); context.write(value,NullWritable.get()); } }
-
通過enum枚舉類型來定義計數器,例如統計reduce端數據的輸入的key有多少個
public class PartitionerReducer extends Reducer<Text,NullWritable,Text,NullWritable> { public static enum Counter{ MY_REDUCE_INPUT_RECORDS,MY_REDUCE_INPUT_BYTES } @Override protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException { context.getCounter(Counter.MY_REDUCE_INPUT_RECORDS).increment(1L); context.write(key, NullWritable.get()); } }
-
-
序列化 (Serialization) 是指把結構化對象轉化爲字節流
-
反序列化 (Deserialization) 是序列化的逆過程. 把字節流轉爲結構化對象. 當要在進程間傳遞對象或持久化對象的時候, 就需要序列化對象成字節流, 反之當要將接收到或從磁盤讀取的字節流轉換爲對象, 就要進行反序列化
-
Java 的序列化 (Serializable) 是一個重量級序列化框架, 一個對象被序列化後, 會附帶很多額外的信息 (各種校驗信息, header, 繼承體系等), 不便於在網絡中高效傳輸. 所以, Hadoop 自己開發了一套序列化機制(Writable), 精簡高效. 不用像 Java 對象類一樣傳輸多層的父子關係, 需要哪個屬性就傳輸哪個屬性值, 大大的減少網絡傳輸的開銷
-
Writable 是 Hadoop 的序列化接口. 一個類要支持可序列化只需實現這個接口即可
-
另外 Writable 有一個子接口是 WritableComparable, WritableComparable 是既可實現序列化, 也可以對key進行比較, 我們這裏可以通過自定義 Key 實現 WritableComparable 來實現我們的排序功能