本文是基於CentOS 7.3系統環境,進行MapReduce的學習和使用本文是基於CentOS 7.3系統環境,進行MapReduce的學習和使用
1. MapReduce簡介
1.1 MapReduce定義
MapReduce是一個分佈式運算程序的編程框架,是基於Hadoop的數據分析計算的核心框架
1.2 MapReduce處理過程
主要分爲兩個階段:Map和Reduce
- Map負責把一個任務分解成多個任務
- Reduce負責把分解後多任務處理的結果進行彙總
1.3 MapReduce的優點
- MapReduce易於編程 只需要實現一些簡單接口,就可以完成一個分佈式程序,這個分佈式程序可以分佈到大量廉價的PC機器上運行。也就是說你寫一個分佈式程序,就跟寫一個簡單的串行程序是一模一樣的。
- 良好的擴展性(hadoop的特點) 當你的計算資源不能滿足的時候,你可以通過簡單的增加機器(nodemanager)來擴展它的計算能力
- 高容錯性 MapReduce設計的初衷就是使程序能夠部署在廉價的PC機器上,這就要求它具有很高的容錯性,比如其中一臺機器掛了,它可以把上面的計算任務轉移到另外一個節點上運行,不至於整個任務運行失敗。
- 適合PB級以上海量數據的離線處理 可以實現上千臺服務器集羣併發工作,提供數據處理能力
1.4 MapReduce的缺點
- 不擅長實時計算 MapReduce無法像MySQL一樣,在毫秒或者秒級內返回結果
- 不擅長流式計算 流式計算的輸入數據是動態的,而MapReduce的輸入數據集是靜態的,不能動態變化。這是因爲MapReduce自身的設計特點決定了數據源必須是靜態的
- 不擅長DAG有向圖計算 多個應用程序之間存在依賴關係,後一個應用程序的輸入爲前一個程序的輸出。在這種情況下,每個MapReduce作業的輸出結果都會寫入到磁盤,會造成大量的磁盤IO,導致性能非常低下
1.5 MapReduce核心編程思想
分佈式的運算程序往往需要分成至少2個階段。 第一個階段的MapTask併發實例,完全並行運行,互不相干。 第二個階段的ReduceTask併發實例互不相干,但是他們的數據依賴於上一個階段的所有MapTask併發實例的輸出。 MapReduce編程模型只能包含一個Map階段和一個Reduce階段,如果用戶的業務邏輯非常複雜,那就只能多個MapReduce程序,串行運行。
1.6 MapReduce進程
- MrAppMaster 負責整個程序的過程調度及狀態協調
- MapTask 負責Map階段的整個數據處理流程
- ReduceTask 負責Reduce階段的整個數據處理流程
1.7 數據切片與MapTask並行度機制
1.8 FileInputFormat切片機制
- 切片機制
簡單地安裝文件的內容長度進行切片 切片大小,默認等於Block大小 切片時不考慮數據集整體,而是逐個針對每一個文件單獨切片
- 案例分析
輸入數據有兩個文件:
file1.txt 320M
file2.txt 10M
經過FileInputFormat的切片機制運算後,形成的切片信息如下:
file1.txt.split1 ---- 0~128M
file1.txt.split2 ---- 128M~256M
file1.txt.split1 ---- 256M~320M
file2.txt.split1 ---- 0~10M
1.9 CombineTextInputFormat切片機制
框架默認的TextInputFormat切片機制是對任務按文件規劃切片,不管文件多小,都會是一個單獨的切片,都會交給一個MapTask,這樣如果有大量小文件,就會產生大量的MapTask,而創建MapTask的開銷比較大,處理效率極其低下。
- 應用場景 CombineTextInputFormat用於小文件過多的場景,它可以將多個小文件從邏輯上規劃到一個切片中,這樣,多個小文件就可以交給一個MapTask處理。
- 虛擬存儲切片最大值設置 CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
- 切片機制 生成切片過程包括:虛擬存儲過程和切片過程兩部分。
虛擬存儲過程: 將輸入目錄下所有文件大小,依次和設置的setMaxInputSplitSize值比較,如果不大於設置的最大值,邏輯上劃分一個塊。如果輸入文件大於設置的最大值且大於兩倍,那麼以最大值切割一塊;當剩餘數據大小超過設置的最大值且不大於最大值2倍,此時將文件均分成2個虛擬存儲塊(防止出現太小切片)。 切片過程:
- (a)判斷虛擬存儲的文件大小是否大於setMaxInputSplitSize值,大於等於則單獨形成一個切片。
- (b)如果不大於則跟下一個虛擬存儲文件進行合併,共同形成一個切片。
4. 案例分析
例如setMaxInputSplitSize值爲4M,輸入文件大小爲8.02M,則先邏輯上分成一個4M。剩餘的大小爲4.02M,如果按照4M邏輯劃分,就會出現0.02M的小的虛擬存儲文件,所以將剩餘的4.02M文件切分成(2.01M和2.01M)兩個文件。 有4個小文件大小分別爲1.7M、5.1M、3.4M以及6.8M這四個小文件,則虛擬存儲之後形成6個文件塊,大小分別爲: 1.7M
,(2.55M、2.55M)
,3.4M
以及(3.4M、3.4M)
最終會形成3個切片,大小分別爲: (1.7+2.55)M
,(2.55+3.4)M
,(3.4+3.4)M
1.10 FileInputFormat實現類
InputFormat | 切片規則(getSplits) | 把切片分解成KV(createRecordReader) |
---|---|---|
FileInputFormat | 按文件->塊大小 | 沒有實現 |
TextInputFormat | 繼承FileInputFormat | LineRecordReader<偏移量,行數據> |
CombineTextInputFormat | 重寫了getSplit,小塊合併切 | CombineFileRecordReader(和LineRecordReader處理一樣,只不過跨文件了)<偏移量,行數據> |
KeyValueTextInputFormat | 繼承FileInputFormat | KeyValueLineRecordReader<分隔符前,分隔符後> |
NLineInputFormat | 重寫了getSplit,按行切 | LineRecordReader<偏移量,行數據> |
自定義 | 繼承FileInputFormat | 自定義RecordReader |
1.11 Shuffle機制
1.12 分區
分區數量等於ReduceTask的進程數
1. 分區機制
- 如果ReduceTask的數量>getPartition的結果數,則會多產生幾個空的輸出文件part-r-000xxx;
- 如果1<ReduceTask的數量<getPartition的結果數,則有一部分分區數據無處安放,會報Exception;
- 如果ReduceTask的數量=1,則不管MapTask端輸出多少個分區文件,最終結果都交給這一個ReduceTask,最終也就只會產生一個結果文件part-r-00000;
- 分區號必須從零開始,逐一累加。
2. 案例分析 例如:假設自定義分區數爲5,
- job.setNumReduceTasks(6);大於5,程序會正常運行,則會多產生幾個空的輸出文件
- job.setNumReduceTasks(2);會報Exception;
- job.setNumReduceTasks(1),會正常運行,最終也就只會產生一個輸出文件;
1.13 排序
1. 排序概述
- 排序是MapReduce框架中最重要的操作之一
- MapTask和ReduceTask均會對數據按照key進行排序。該操作數據Hadoop的默認行爲。任何應用程序中的數據均會被排序,而不是邏輯上是否需要。
- 默認排序是按照字典順序排序,且實現該排序的方法是快速排序。
2. 排序分類
- 部分排序:MapReduce根據輸入記錄的鍵對數據集排序,保障輸出的每個文件內部有序。
- 全排序:最終輸出結果只有一個文件,且文件內部有序。
- 輔助排序(GroupingComparator):在Reduce端對key進行分組
- 二次排序:在自定義排序過程中,如果compareTo中的判斷條件爲兩個即爲二次排序
1.14 Combiner合併
- Combiner是MR程序中Mapper和Reducer之外的一種組件
- Combiner組件的父類就是Reducer
- Combiner和Reducer的區別在於運行的位置不同(Combiner是在每一個MapTask所在的節點運行;Reducer是接收全局所有Mapper的輸出結果)
- Combiner的意義就是對每一個MapTask的輸出進行局部彙總,以減少網絡傳輸量
- Combiner能夠應用的前提是不能影響最終的業務邏輯,而且Combiner的輸出kv應用跟Reducer的輸入kv類型要對應起來
1.15 MapTask工作機制
-
Read階段:MapTask通過用戶編寫的RecordReader,從輸入InputSplit中解析出一個個key/value。
-
Map階段:該節點主要是將解析出的key/value交給用戶編寫map()函數處理,併產生一系列新的key/value。
-
Collect收集階段:在用戶編寫map()函數中,當數據處理完成後,一般會調用OutputCollector.collect()輸出結果。在該函數內部,它會將生成的key/value分區(調用Partitioner),並寫入一個環形內存緩衝區中。
-
Spill階段:即“溢寫”,當環形緩衝區滿後,MapReduce會將數據寫到本地磁盤上,生成一個臨時文件。需要注意的是,將數據寫入本地磁盤之前,先要對數據進行一次本地排序,並在必要時對數據進行合併、壓縮等操作。
溢寫階段詳情 **步驟1:**利用快速排序算法對緩存區內的數據進行排序,排序方式是,先按照分區編號Partition進行排序,然後按照key進行排序。這樣,經過排序後,數據以分區爲單位聚集在一起,且同一分區內所有數據按照key有序。 **步驟2:**按照分區編號由小到大依次將每個分區中的數據寫入任務工作目錄下的臨時文件output/spillN.out(N表示當前溢寫次數)中。如果用戶設置了Combiner,則寫入文件之前,對每個分區中的數據進行一次聚集操作。 **步驟3:**將分區數據的元信息寫到內存索引數據結構SpillRecord中,其中每個分區的元信息包括在臨時文件中的偏移量、壓縮前數據大小和壓縮後數據大小。如果當前內存索引大小超過1MB,則將內存索引寫到文件output/spillN.out.index中。
-
Combine階段:當所有數據處理完成後,MapTask對所有臨時文件進行一次合併,以確保最終只會生成一個數據文件。
當所有數據處理完後,MapTask會將所有臨時文件合併成一個大文件,並保存到文件output/file.out中,同時生成相應的索引文件output/file.out.index。
在進行文件合併過程中,MapTask以分區爲單位進行合併。對於某個分區,它將採用多輪遞歸合併的方式。每輪合併io.sort.factor(默認10)個文件,並將產生的文件重新加入待合併列表中,對文件排序後,重複以上過程,直到最終得到一個大文件。
讓每個MapTask最終只生成一個數據文件,可避免同時打開大量文件和同時讀取大量小文件產生的隨機讀取帶來的開銷。
1.16 ReduceTask工作機制
- Copy階段:ReduceTask從各個MapTask上遠程拷貝一片數據,並針對某一片數據,如果其大小超過一定閾值,則寫到磁盤上,否則直接放到內存中。
- Merge階段:在遠程拷貝數據的同時,ReduceTask啓動了兩個後臺線程對內存和磁盤上的文件進行合併,以防止內存使用過多或磁盤上文件過多。
- Sort階段:按照MapReduce語義,用戶編寫reduce()函數輸入數據是按key進行聚集的一組數據。爲了將key相同的數據聚在一起,Hadoop採用了基於排序的策略。由於各個MapTask已經實現對自己的處理結果進行了局部排序,因此,ReduceTask只需對所有數據進行一次歸併排序即可。
- Reduce階段:reduce()函數將計算結果寫到HDFS上。
1.17 ReduceTask並行度
ReduceTask的並行度影響整個job的執行併發度和執行效率,但與MapTask的併發數有切片數決定不同,ReduceTask數量的決定是可以直接手動設置的
- ReduceTask=0,表示沒有Reduce階段,輸出文件個數和Map個數一致,在實際開發中,如果可以不用Reduce,可以將值設置爲0,因爲在整個MR階段,比較耗時的shuffle,省掉了Reduce,就相當於省掉了shuffle;
- ReduceTask默認值就是1,所以輸出文件個數爲1;
- 如果數據分佈不均勻,就有可能在Reduce階段產生數據傾斜;
- ReduceTask數量並不是任意設置,還要考慮業務邏輯需求,有些情況下,需要計算全局彙總結果,就只能有1個ReduceTask;
- 具體多少個ReduceTask,需要根據集羣性能而定;
- 如果分區數不是1,但是ReduceTask爲1,是否執行分區過程。答案是:不執行分區過程,因爲在MapTask的源碼中,執行分區的前提就是先判斷ReduceNum個數是否大於1,不大於1肯定不執行。
1.18 Reduce Join工作原理
**Map端的主要工作:**爲來自不同表或文件的key/value對,打標籤以區別不同來源的記錄。然後用連接字段作爲key,其餘部分和新加的標誌作爲value,最後進行輸出。 **Reduce端的主要工作:**在Reduce端以連接字段作爲key的分組已經完成,我們只需要在每一個分組當中將那些來源於不同文件的記錄(在Map階段已經打標誌)分開,最後進行合併就OK了。 **缺點:**這種方式,合併的操作是在Reduce階段完成,Reduce端的處理壓力太大,Map節點的運算負載則很低,資源利用率不高,且在Reduce階段極易產生數據傾斜 **解決方案:**Map端實現數據合併
1.19 Map Join工作原理
**適用場景:**Map Join試驗於一張表十分大,一張表十分小的場景 **優點:**在Map端緩存多張表,提前出來業務邏輯,這樣增加Map端業務,減少Reduce端數據的壓力,儘可能的減少數據傾斜
1.20 計數器應用
**定義:**Hadoop爲每個作業維護若干內置計數器,以描述多項指標。例如,某些計數器記錄已處理的字節數和記錄數,使用戶可監控已處理的輸入數據量和已產生的輸出數據量
計數器API:
採用枚舉的方式統計計數 採用計數器組、計數器名稱的方式統計
1.21 數據壓縮
**定義:**壓縮技術能夠有效減少底層存儲系統(HDFS)讀寫字節數。壓縮提高了網絡帶寬和磁盤空間的效率。在運行MR程序時,I/O操作、網絡數據傳輸、shuffle和Merge要花大量的時間,尤其是數據規模很大和工作負載密集的情況下。因此,使用數據壓縮顯得非常重要。
**優點:**鑑於磁盤I/O和網絡帶寬是Hadoop的寶貴資源,數據壓縮對於節省資源、最小化磁盤I/O和網絡傳輸非常有幫助。可以在任意MapReduce階段啓用壓縮。
**缺點:**不過,儘管壓縮與解壓操作的CPU開銷不高,其性能的提升和資源的節省並非沒有代價
**壓縮策略:**壓縮是提供Hadoop運行效率的一種優化策略。 通過對mapper、reducer運行過程的數據進行壓縮,以減少磁盤I/O,提高MR程序運行速度。
壓縮原則:
- 運算密集型的job,少用壓縮
- I/O密集型的job,多用壓縮
1.22 MR支持的壓縮編碼
壓縮格式 | hadoop自帶 | 算法文件擴展名 | 是否可切分 | 壓縮後,原來的程序是否需要修改 |
---|---|---|---|---|
DEFLATE | 是,直接使用 | DEFLATE.deflate | 否 | 和文本處理一樣,不需要修改 |
gzip | 是,直接使用 | DEFLATE.gz | 否 | 和文本處理一樣,不需要修改 |
bzip2 | 是,直接使用 | bzip2.bz | 是 | 和文本處理一樣,不需要修改 |
LZO | 否,需要安裝 | LZO.lzo | 是 | 需要建索引,還需要指定輸入格式 |
Snappy | 否,需要安裝 | Snappy.snappy | 否 | 和文本處理一樣,不需要修改 |
壓縮算法 | 原始文件大小 | 壓縮文件大小 | 壓縮速度 | 解壓速度 |
---|---|---|---|---|
gzip | 8.3G | 1.8G | 17.5MB/s | 58MB/s |
bzip | 28.3G | 1.1G | 2.4MB/s | 9.5MB/s |
gzip | 8.3G | 2.9G | 49.3MB/s | 74.6MB/s |
snappy | 8.3G | * | 250MB/s | 500MB/s |
1.23 壓縮方式選擇
- Gzip壓縮
**優點:**壓縮率比較高,而且壓縮/解壓速度也比較快;Hadoop本身支持,在應用中處理gzip格式的文件就和直接處理文本一樣;大部分Linux系統都自帶gzip命令,使用方便 **缺點:**不支持split **應用場景:**當每個文件壓縮之後在130M以內的(1個塊大小內),都可以考慮gzip壓縮格式
- Bzip2壓縮
**優點:**支持split,具有很高壓縮率,比Gzip壓縮率高;Hadoop本身支持,使用方便 **缺點:**壓縮/解壓速度比較慢 **應用場景:**適合對速度要求不高,但需要較高的壓縮率的時候;或者輸出之後的數據比較大,處理之後的數據需要壓縮存檔減少磁盤空間並且以後數據用得比較少的情況;或者對單個很大的文本文件想壓縮減少存儲空間,同時又需要支持split,而且兼容之前的應用程序的情況
- Lzo壓縮
**優點:**壓縮/解壓速度也比較快,合理的壓縮率;支持split,是Hadoop中最流行的壓縮格式;可以在Linux系統下安裝lzop命令,使用方便 **缺點:**壓縮率比Gzip要低一些;Hadoop本身不支持,需要安裝;在應用中對Lzo格式的文件需要做一些特殊處理(爲了支持split要建索引,還需要指定InputFormat爲Lzo格式) **應用場景:**一個很大的文本文件,壓縮之後還大於200M以上的可以考慮,而且單個文件越大,Lzo優點越明顯
- snappy壓縮
**優點:**高速壓縮/解壓速度,合理的壓縮率; **缺點:**不支持split;壓縮率比Gzip要低;Hadoop本身不支持,需要安裝 **應用場景:**當MapReduce作業的map輸出的數據比較大的時候,作爲map到reduce的中間數據的壓縮格式;或者作爲一個MapReduce作業的輸出和另一個MapReduce作業的輸入。
2. MapReduce編程規範
2.1 編寫Mapper類
- 繼承org.apache.hadoop.mapreduce.Mapper類
- 設置mapper類的輸入類型<LongWritable, Text>
- 設置mapper類的輸出類型<Text, IntWritable>
- 將輸入類型中的Text轉換成String類型,並按照指定分隔符進行分割
- 通過context.write()方法進行輸出
package com.lytdev.dw.mapr;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
private Text k = new Text();
private IntWritable v = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] words = line.split(" ");
for (String word : words) {
k.set(word);
context.write(k, v);
}
}
}
2.2 編寫Reducer類
- 繼承org.apache.hadoop.mapreduce.Reducer類
- 設置reducer類的輸入類型<Text, IntWritable>
- 設置reducer類的輸出類型<Text, IntWritable>
- 對values值進行彙總求和
- 通過context.write()方法進行輸出
package com.lytdev.dw.mapr.demo;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class WordcountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private int sum;
private IntWritable v = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
sum = 0;
for (IntWritable value : values) {
sum += value.get();
}
v.set(sum);
context.write(key, v);
}
}
2.3 編寫Driver類
- 創建一個org.apache.hadoop.conf.Configuration類對象
- 通過Job.getInstance(conf)獲得一個job對象
- 設置job的3個類,driver、mapper、reducer
- 設置job的2個輸出類型,map輸出和總體輸出
- 設置一個輸入輸出路徑
- 調用job.waitForCompletion(true)進行提交任務
package com.lytdev.dw.mapr.demo;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class WordcountDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(WordcountDriver.class);
job.setMapperClass(WordcountMapper.class);
job.setReducerClass(WordcountReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
job.setOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
FileInputFormat.setInputPaths(job, new Path("/input"));
FileOutputFormat.setOutputPath(job, new Path("/output"));
boolean result = job.waitForCompletion(true);
System.out.println(result);
}
}