傳送門:
大數據學習系列:Hadoop3.0苦命學習(一)
大數據學習系列:Hadoop3.0苦命學習(二)
大數據學習系列:Hadoop3.0苦命學習(三)
大數據學習系列:Hadoop3.0苦命學習(四)
目錄
- 1 MapReduce 中的計數器
- 2 規約Combiner
- 3 流量統計
- 數據
- 需求一: 統計求和
- Step 1: 自定義map的輸出value對象FlowBean
- Step 2: 定義FlowMapper類
- Step 3: 定義FlowReducer類
- Step 4: 程序main函數入口JobMain
- 需求二 : 上行流量倒序排序(遞減排序)
- Step 1: 定義FlowBean實現WritableComparable實現比較排序
- Step 2: 定義FlowMapper
- Step 3: 定義FlowReducer
- Step 4: 程序main函數入口
- 需求三 : 手機號碼分區
- 4 MapTask 運行機制
- 5 ReduceTask 工作機制和 ReduceTask 並行度
- 6 Shuffle 過程
- 7 [ 案例] Reduce 端實現 JOIN
1 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執行完成之後,我們都會看到一些日誌記錄出來,其中最重要的一些日誌
記錄如下截圖
所有的這些都是 MapReduce的計數器的功能,既然MapReduce當中有計數器的功能,我
們如何實現自己的計數器?
需求:以上面排序以及序列化爲案例,統計map接收到的數據記錄條數
第一種方式
第一種方式定義計數器,通過context上下文對象可以獲取我們的計數器,進行記錄通過context上下文對象,在map端使用計數器進行統計
public class SortMapper extends Mapper<LongWritable, Text, PairWritable, Text> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//自定義計數器
Counter counter = context.getCounter("MR_COUNT", "MapReduceCounter");
counter.increment(1L);
// 1. 對每一行數據進行拆分,然後封裝到PairWritable對象中,作爲K2
String[] split = value.toString().split("\t");
PairWritable pairWritable = new PairWritable();
pairWritable.setFirst(split[0]);
pairWritable.setSecond(Integer.parseInt(split[1]));
// 2. 將K2和V2寫入上下文中
context.write(pairWritable, value);
}
}
第二種方式
通過enum枚舉類型來定義計數器
統計reduce端數據的輸入的key有多少個,對應的value有多少個
public class SortReducer extends Reducer<PairWritable, Text, PairWritable, NullWritable> {
public static enum MyCounter{
REDUCE_INPUT_KEY_RECORDS, REDUCE_INPUT_VALUE_RECORDS
}
@Override
protected void reduce(PairWritable key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
// 統計Reduce階段key的個數
context.getCounter(MyCounter.REDUCE_INPUT_KEY_RECORDS).increment(1L);
for (Text text: values) {
// 統計Reduce階段value的個數
context.getCounter(MyCounter.REDUCE_INPUT_VALUE_RECORDS).increment(1L);
context.write(key, NullWritable.get());
}
}
}
像前幾節類似(打包、上傳、執行)結果如下:
2 規約Combiner
每一個 map 都可能會產生大量的本地輸出,Combiner 的作用就是對 map 端的輸出先做一次合併,以減少在 map 和 reduce 節點之間的數據傳輸量,以提高網絡IO 性能,是MapReduce 的一種優化手段之一
-
combiner 是 MR 程序中 Mapper 和 Reducer 之外的一種組件
-
combiner 組件的父類就是 Reducer
-
combiner 和 reducer 的區別在於運行的位置
- Combiner 是在每一個 maptask 所在的節點運行
- Reducer 是接收全局所有 Mapper 的輸出結果
-
combiner 的意義就是對每一個 maptask 的輸出進行局部彙總,以減小網絡傳輸量
實現步驟
- 自定義一個 combiner 繼承 Reducer,重寫 reduce 方法
- 在 job 中設置 job.setCombinerClass(CustomCombiner.class)
combiner 能夠應用的前提是不能影響最終的業務邏輯,而且,combiner 的輸出 kv 應該
跟 reducer 的輸入 kv 類型要對應起來
運行實驗
加規約之前:
- 修改
worldcount.txt
內容
hdfs dfs -rm /wordcount/wordcount.txt
刪除原來的wordcount.txt;hdfs dfs -put wordcount.txt /wordcount/
上傳新的文件;rz -E
上傳打包後的jar包,hdfs dfs -rm -r /wordcount_out
刪除已存在的文件夾hdfs dfs -rm -r /wordcount_out
啓動項目
查看計數器:
加規約之後:
- 增加一個
MyCombiner
類
package itcast.mapreduce;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class MyCombiner extends Reducer<Text, LongWritable, Text, LongWritable> {
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
long count = 0;
// 1.遍歷values集合
for (LongWritable value : values) {
// 2.將集合中的值相加
count += value.get();
}
// 3.將k3和v3寫入上下文中
context.write(key, new LongWritable(count));
}
}
- 修改
JobMain
類
// 設置規約類
job.setCombinerClass(MyCombiner.class);
- 打包、上傳、替換,執行
結果如下:
很明顯看到 Reduce input records
減少了。
3 流量統計
數據
data_flow.dat
1363157985066 13726230503 00-FD-07-A4-72-B8:CMCC 120.196.100.82 i02.c.aliimg.com 遊戲娛樂 24 27 2481 24681 200
1363157995052 13826544101 5C-0E-8B-C7-F1-E0:CMCC 120.197.40.4 jd.com 京東購物 4 0 264 0 200
1363157991076 13926435656 20-10-7A-28-CC-0A:CMCC 120.196.100.99 taobao.com 淘寶購物 2 4 132 1512 200
1363154400022 13926251106 5C-0E-8B-8B-B1-50:CMCC 120.197.40.4 cnblogs.com 技術門戶 4 0 240 0 200
1363157993044 18211575961 94-71-AC-CD-E6-18:CMCC-EASY 120.196.100.99 iface.qiyi.com 視頻網站 15 12 1527 2106 200
1363157995074 84138413 5C-0E-8B-8C-E8-20:7DaysInn 120.197.40.4 122.72.52.12 未知 20 16 4116 1432 200
1363157993055 13560439658 C4-17-FE-BA-DE-D9:CMCC 120.196.100.99 sougou.com 綜合門戶 18 15 1116 954 200
1363157995033 15920133257 5C-0E-8B-C7-BA-20:CMCC 120.197.40.4 sug.so.360.cn 信息安全 20 20 3156 2936 200
1363157983019 13719199419 68-A1-B7-03-07-B1:CMCC-EASY 120.196.100.82 baidu.com 綜合搜索 4 0 240 0 200
1363157984041 13660577991 5C-0E-8B-92-5C-20:CMCC-EASY 120.197.40.4 s19.cnzz.com 站點統計 24 9 6960 690 200
1363157973098 15013685858 5C-0E-8B-C7-F7-90:CMCC 120.197.40.4 rank.ie.sogou.com 搜索引擎 28 27 3659 3538 200
1363157986029 15989002119 E8-99-C4-4E-93-E0:CMCC-EASY 120.196.100.99 www.umeng.com 站點統計 3 3 1938 180 200
1363157992093 13560439658 C4-17-FE-BA-DE-D9:CMCC 120.196.100.99 zhilian.com 招聘門戶 15 9 918 4938 200
1363157986041 13480253104 5C-0E-8B-C7-FC-80:CMCC-EASY 120.197.40.4 csdn.net 技術門戶 3 3 180 180 200
1363157984040 13602846565 5C-0E-8B-8B-B6-00:CMCC 120.197.40.4 2052.flash2-http.qq.com 綜合門戶 15 12 1938 2910 200
1363157995093 13922314466 00-FD-07-A2-EC-BA:CMCC 120.196.100.82 img.qfc.cn 圖片大全 12 12 3008 3720 200
1363157982040 13502468823 5C-0A-5B-6A-0B-D4:CMCC-EASY 120.196.100.99 y0.ifengimg.com 綜合門戶 57 102 7335 110349 200
1363157986072 18320173382 84-25-DB-4F-10-1A:CMCC-EASY 120.196.100.99 input.shouji.sogou.com 搜索引擎 21 18 9531 2412 200
1363157990043 13925057413 00-1F-64-E1-E6-9A:CMCC 120.196.100.55 t3.baidu.com 搜索引擎 69 63 11058 48243 200
1363157988072 13760778710 00-FD-07-A4-7B-08:CMCC 120.196.100.82 http://youku.com/ 視頻網站 2 2 120 120 200
1363157985079 13823070001 20-7C-8F-70-68-1F:CMCC 120.196.100.99 img.qfc.cn 圖片瀏覽 6 3 360 180 200
1363157985069 13600217502 00-1F-64-E2-E8-B1:CMCC 120.196.100.55 www.baidu.com 綜合門戶 18 138 1080 186852 200
需求一: 統計求和
統計每個手機號的上行流量總和,下行流量總和,上行總流量之和,下行總流量之和分析:以手機號碼作爲key值,上行流量,下行流量,上行總流量,下行總流量四個字段作爲value值,然後以這個key,和value作爲map階段的輸出,reduce階段的輸入
Step 1: 自定義map的輸出value對象FlowBean
public class FlowBean implements Writable {
private Integer upFlow;
private Integer downFlow;
private Integer upCountFlow;
private Integer downCountFlow;
public Integer getUpFlow() {
return upFlow;
}
public void setUpFlow(Integer upFlow) {
this.upFlow = upFlow;
}
public Integer getDownFlow() {
return downFlow;
}
public void setDownFlow(Integer downFlow) {
this.downFlow = downFlow;
}
public Integer getUpCountFlow() {
return upCountFlow;
}
public void setUpCountFlow(Integer upCountFlow) {
this.upCountFlow = upCountFlow;
}
public Integer getDownCountFlow() {
return downCountFlow;
}
public void setDownCountFlow(Integer downCountFlow) {
this.downCountFlow = downCountFlow;
}
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeInt(upFlow);
dataOutput.writeInt(downFlow);
dataOutput.writeInt(upCountFlow);
dataOutput.writeInt(downCountFlow);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
this.upFlow = dataInput.readInt();
this.downFlow = dataInput.readInt();
this.upCountFlow = dataInput.readInt();
this.downCountFlow = dataInput.readInt();
}
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + upCountFlow + "\t" + downCountFlow;
}
}
Step 2: 定義FlowMapper類
public class FlowCountMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1. 拆分手機號
String[] split = value.toString().split("\t");
String phoneNum = split[1];
// 2. 獲取四個流量字段
FlowBean flowBean = new FlowBean();
flowBean.setUpFlow(Integer.parseInt(split[6]));
flowBean.setDownFlow(Integer.parseInt(split[7]));
flowBean.setUpCountFlow(Integer.parseInt(split[8]));
flowBean.setDownCountFlow(Integer.parseInt(split[9]));
// 3. 將k2和v2寫入上下文中
context.write(new Text(phoneNum), flowBean);
}
}
Step 3: 定義FlowReducer類
public class FlowCountReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {
// 封裝新的FlowBean
FlowBean flowBean = new FlowBean();
Integer upFlow = 0;
Integer downFlow = 0;
Integer upCountFlow = 0;
Integer downCountFlow = 0;
for (FlowBean value : values) {
upFlow += value.getUpFlow();
downFlow += value.getDownFlow();
upCountFlow += value.getUpCountFlow();
downCountFlow += value.getDownCountFlow();
}
flowBean.setUpFlow(upFlow);
flowBean.setDownFlow(downFlow);
flowBean.setUpCountFlow(upCountFlow);
flowBean.setDownCountFlow(downCountFlow);
// 將K3和V3寫入上下文中
context.write(key, flowBean);
}
}
Step 4: 程序main函數入口JobMain
public class JobMain extends Configured implements Tool {
@Override
public int run(String[] strings) throws Exception {
// 創建一個任務對象
Job job = Job.getInstance(super.getConf(), "mapreduce_flowcount");
// 打包放在集羣運行時,需要做一個配置
job.setJarByClass(JobMain.class);
// 第一步:設置讀取文件的類:K1和V1
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job, new Path("hdfs://node01:8020/input/flowcount"));
// 第二步:設置Mapper類
job.setMapperClass(FlowCountMapper.class);
// 設置Map階段的輸出類型:k2和v2的類型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
// 第三、四、五、六步採用默認方式(分區,排序,規約,分組)
// 第七步:設置Reducer類
job.setReducerClass(FlowCountReducer.class);
// 設置Reduce階段的輸出類型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
// 第八步:設置輸出類
job.setOutputFormatClass(TextOutputFormat.class);
// 設置輸出路徑
TextOutputFormat.setOutputPath(job, new Path("hdfs://node01:8020/out/flowcount"));
boolean b = job.waitForCompletion(true);
return b?0:1;
}
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
// 啓動一個任務
ToolRunner.run(configuration, new JobMain(), args);
}
}
打包、替換、上傳、執行,結果如下:
需求二 : 上行流量倒序排序(遞減排序)
分析,以需求一的輸出數據作爲排序的輸入數據,自定義FlowBean,以FlowBean爲map輸
出的key,以手機號作爲Map輸出的value,因爲MapReduce程序會對Map階段輸出的key
進行排序
Step 1: 定義FlowBean實現WritableComparable實現比較排序
Java 的 compareTo 方法說明:
- compareTo 方法用於將當前對象與方法的參數進行比較。
- 如果指定的數與參數相等返回 0。
- 如果指定的數小於參數返回 -1。
- 如果指定的數大於參數返回 1。
例如: o1.compareTo(o2); 返回正數的話,當前對象(調用 compareTo 方法的對象 o1)要排在比較對象(compareTo 傳參對象 o2)後面,返回負數的話,放在前面
public class FlowBean implements WritableComparable<FlowBean> {
private Integer upFlow;
private Integer downFlow;
private Integer upCountFlow;
private Integer downCountFlow;
public Integer getUpFlow() {
return upFlow;
}
public void setUpFlow(Integer upFlow) {
this.upFlow = upFlow;
}
public Integer getDownFlow() {
return downFlow;
}
public void setDownFlow(Integer downFlow) {
this.downFlow = downFlow;
}
public Integer getUpCountFlow() {
return upCountFlow;
}
public void setUpCountFlow(Integer upCountFlow) {
this.upCountFlow = upCountFlow;
}
public Integer getDownCountFlow() {
return downCountFlow;
}
public void setDownCountFlow(Integer downCountFlow) {
this.downCountFlow = downCountFlow;
}
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeInt(upFlow);
dataOutput.writeInt(downFlow);
dataOutput.writeInt(upCountFlow);
dataOutput.writeInt(downCountFlow);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
this.upFlow = dataInput.readInt();
this.downFlow = dataInput.readInt();
this.upCountFlow = dataInput.readInt();
this.downCountFlow = dataInput.readInt();
}
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + upCountFlow + "\t" + downCountFlow;
}
@Override
public int compareTo(FlowBean o) {
return o.getUpFlow().compareTo(this.getUpFlow());
}
}
Step 2: 定義FlowMapper
public class FlowCountSortMapper extends Mapper<LongWritable, Text, FlowBean, Text> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
FlowBean flowBean = new FlowBean();
String[] split = value.toString().split("\t");
// 獲取手機號,作爲V2
String phoneNum = split[0];
// 獲取其他流量字段,封裝flowBean,作爲K2
flowBean.setUpFlow(Integer.parseInt(split[1]));
flowBean.setDownFlow(Integer.parseInt(split[2]));
flowBean.setUpCountFlow(Integer.parseInt(split[3]));
flowBean.setDownCountFlow(Integer.parseInt(split[4]));
context.write(flowBean, new Text(phoneNum));
}
}
Step 3: 定義FlowReducer
public class FlowCountSortReducer extends Reducer<FlowBean, Text, Text, FlowBean> {
@Override
protected void reduce(FlowBean key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
for (Text value : values) {
context.write(value, key);
}
}
}
Step 4: 程序main函數入口
public class JobMain extends Configured implements Tool {
@Override
public int run(String[] strings) throws Exception {
// 創建一個任務對象
Job job = Job.getInstance(super.getConf(), "mapreduce_flowcountsort");
// 打包放在集羣運行時,需要做一個配置
job.setJarByClass(JobMain.class);
// 第一步:設置讀取文件的類:K1和V1
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job, new Path("hdfs://node01:8020/out/flowcount"));
// 第二步:設置Mapper類
job.setMapperClass(FlowCountSortMapper.class);
// 設置Map階段的輸出類型:k2和v2的類型
job.setMapOutputKeyClass(FlowBean.class);
job.setMapOutputValueClass(Text.class);
// 第三、四、五、六步採用默認方式(分區,排序,規約,分組)
// 第七步:設置Reducer類
job.setReducerClass(FlowCountSortReducer.class);
// 設置Reduce階段的輸出類型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
// 第八步:設置輸出類
job.setOutputFormatClass(TextOutputFormat.class);
// 設置輸出路徑
TextOutputFormat.setOutputPath(job, new Path("hdfs://node01:8020/out/flowcount_sort"));
boolean b = job.waitForCompletion(true);
return b?0:1;
}
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
// 啓動一個任務
ToolRunner.run(configuration, new JobMain(), args);
}
}
打包、替換、上傳、執行,結果如下:
需求三 : 手機號碼分區
在需求一的基礎上,繼續完善,將不同的手機號分到不同的數據文件的當中去,需要自定
義分區來實現,這裏我們自定義來模擬分區,將以下數字開頭的手機號進行分開
135 開頭數據到一個分區文件
136 開頭數據到一個分區文件
137 開頭數據到一個分區文件
其他分區
在需求一的代碼上,增加 FlowPartition
類
public class FlowPartition extends Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text text, FlowBean flowBean, int i) {
// 判斷手機號以哪個數字開頭然後返回不同的分區編號
if (text.toString().startsWith("135")) {
return 0;
}else if (text.toString().startsWith("136")) {
return 1;
}else if (text.toString().startsWith("137")) {
return 2;
}else{
return 3;
}
}
}
在 JobMain
上增加:
job.setPartitionerClass(FlowPartition.class);
job.setNumReduceTasks(4);
打包、替換、上傳、執行,結果如下:
一共產生了4個文件,這裏只展示第一個。
4 MapTask 運行機制
整個Map階段流程大體如上圖所示。
簡單概述:inputFile 通過 split 被邏輯切分爲多個 split 文件,通過 Record 按行讀取內容給 map(用戶自己實現的)進行處理,數據被 map 處理結束之後交給OutputCollector 收集器,對其結果key進行分區(默認使用hash分區),然後寫入 buffer,每個 map task 都有一個內存緩衝區,存儲着map的輸出結果,當緩衝區快滿的時候需要將緩衝區的數據以一個臨時文件的方式存放到磁盤,當整個 map task 結束後再對磁盤中這個 map task 產生的所有臨時文件做合併,生成最終的正式輸出文件,然後等待 reduce task 來拉數據。
詳細步驟
-
讀取數據組件 InputFormat (默認 TextInputFormat) 會通過
getSplits
方法對輸入
目錄中文件進行邏輯切片規劃得到splits
, 有多少個 split 就對應啓動多少個
MapTask. split
與block
的對應關係默認是一對一 -
將輸入文件切分爲
splits
之後, 由RecordReader
對象 (默認是LineRecordReader)
進行讀取, 以\n
作爲分隔符, 讀取一行數據, 返回<key ,value>
. Key 表示每行首字符
偏移值, Value 表示這一行文本內容 -
讀取
split
返回<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 對應數據的偏移量
配置
配置 | 默認值 | 解釋 |
---|---|---|
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 | 設置一次合併多少個溢寫文件 |
5 ReduceTask 工作機制和 ReduceTask 並行度
Reduce
大致分爲 copy
、sort
、reduce
三個階段,重點在前兩個階段。cop
y 階段包含一
個eventFetcher
來獲取已完成的 map
列表,由 Fetcher
線程去 copy
數據,在此過程中
會啓動兩個 merge
線程,分別爲 inMemoryMerger
和 onDiskMerger
,分別將內存中的
數據 merge
到磁盤和將磁盤中的數據進行 merge
。待數據 copy
完成之後,copy
階段就
完成了,開始進行 sort
階段,sort
階段主要是執行 finalMerge
操作,純粹的 sort
階段,完成之後就是 reduce
階段,調用用戶定義的 reduce
函數進行處理
詳細步驟
Copy
階段 ,簡單地拉取數據。Reduce進程啓動一些數據copy線程(Fetcher),通過
HTTP方式請求maptask獲取屬於自己的文件。Merge
階段 。這裏的merge如map端的merge動作,只是數組中存放的是不同map端
copy來的數值。Copy過來的數據會先放入內存緩衝區中,這裏的緩衝區大小要比map
端的更爲靈活。merge有三種形式:內存到內存;內存到磁盤;磁盤到磁盤。默認情
況下第一種形式不啓用。當內存中的數據量到達一定閾值,就啓動內存到磁盤的
merge。與map 端類似,這也是溢寫的過程,這個過程中如果你設置有Combiner,
也是會啓用的,然後在磁盤中生成了衆多的溢寫文件。第二種merge方式一直在運
行,直到沒有map端的數據時才結束,然後啓動第三種磁盤到磁盤的merge方式生成
最終的文件。合併排序
。把分散的數據合併成一個大的數據後,還會再對合並後的數據排序。對排序後的鍵值對調用 reduce方法
,鍵相等的鍵值對調用一次reduce方法,每次調用會
產生零個或者多個鍵值對,最後把這些輸出的鍵值對寫入到HDFS文件中。
6 Shuffle 過程
map 階段處理的數據如何傳遞給 reduce 階段,是 MapReduce 框架中最關鍵的一個流
程,這個流程就叫 shuffle
shuffle: 洗牌、發牌 ——(核心機制:數據分區,排序,分組,規約,合併等過程)
shuffle 是 Mapreduce 的核心,它分佈在 Mapreduce 的 map 階段和 reduce 階段。一般
把從 Map 產生輸出開始到 Reduce 取得數據作爲輸入之前的過程稱作 shuffle。
Collect 階段
:將 MapTask 的結果輸出到默認大小爲 100M 的環形緩衝區,保存的是
key/value,Partition 分區信息等。Spill 階段
:當內存中的數據量達到一定的閥值的時候,就會將數據寫入本地磁盤,在
將數據寫入磁盤之前需要對數據進行一次排序的操作,如果配置了 combiner,還會將
有相同分區號和 key 的數據進行排序。Merge 階段
:把所有溢出的臨時文件進行一次合併操作,以確保一個 MapTask 最終只
產生一箇中間數據文件。Copy 階段
:ReduceTask 啓動 Fetcher 線程到已經完成 MapTask 的節點上覆制一份
屬於自己的數據,這些數據默認會保存在內存的緩衝區中,當內存的緩衝區達到一定
的閥值的時候,就會將數據寫到磁盤之上。Merge 階段
:在 ReduceTask 遠程複製數據的同時,會在後臺開啓兩個線程對內存到
本地的數據文件進行合併操作。Sort 階段
:在對數據進行合併的同時,會進行排序操作,由於 MapTask 階段已經對
數據進行了局部的排序,ReduceTask 只需保證 Copy 的數據的最終整體有效性即可。
Shuffle 中的緩衝區大小會影響到 mapreduce 程序的執行效率,原則上說,緩衝區越
大,磁盤io的次數越少,執行速度就越快
緩衝區的大小可以通過參數調整, 參數:mapreduce.task.io.sort.mb 默認100M
7 [ 案例] Reduce 端實現 JOIN
需求
假如數據量巨大,兩表的數據是以文件的形式存儲在 HDFS 中, 需要用 MapReduce 程序來實現以下 SQL 查詢運算
select a.id,a.date,b.name,b.category_id,b.price from t_order a join t_product b on a.pid = b.id
訂單數據表(id date pid amount)
// orders.txt
1001,20150710,p0001,2
1002,20150710,p0002,3
1002,20150710,p0003,3
商品信息表(id pname category_id price)
// product.txt
p0001,小米5,1000,2000
p0002,錘子T1,1000,3000
實現機制
通過將關聯的條件作爲map輸出的key,將兩表滿足join條件的數據並攜帶數據所來源的文
件信息,發往同一個reduce task,在reduce中進行數據的串聯
Step 1: 定義 Mapper
package itcast.mapreduce_reduce_join;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
public class ReduceJoinMapper extends Mapper<LongWritable, Text, Text, Text> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 首先判斷數據來自哪個文件
FileSplit fileSplit = (FileSplit) context.getInputSplit();
String fileName = fileSplit.getPath().getName();
if (fileName.equals("orders.txt")) {
// 獲取pid
String[] split = value.toString().split(",");
context.write(new Text(split[2]), value);
}else{
String[] split = value.toString().split(",");
context.write(new Text(split[0]), value);
}
}
}
Step 2: 定義 Reducer
package itcast.mapreduce_reduce_join;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class ReduceJoinReducer extends Reducer<Text, Text, Text, Text> {
@Override
protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
String first = "";
String second = "";
for (Text value : values) {
if (value.toString().startsWith("p")) {
first = value.toString();
}else{
second = value.toString();
}
}
if (first.equals("")) {
first = "NULL";
}
if (second.equals("")) {
second = "NULL";
}
context.write(key, new Text(first+"\t"+second));
}
}
Step 3: Main 方法
package itcast.mapreduce_reduce_join;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
public class JobMain extends Configured implements Tool {
@Override
public int run(String[] strings) throws Exception {
// 創建一個任務對象
Job job = Job.getInstance(super.getConf(), "mapreduce_reduce_join");
// 打包放在集羣運行時,需要做一個配置
job.setJarByClass(JobMain.class);
// 第一步:設置讀取文件的類:K1和V1
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job, new Path("hdfs://node01:8020/input/reduce_join"));
// 第二步:設置Mapper類
job.setMapperClass(ReduceJoinMapper.class);
// 設置Map階段的輸出類型:k2和v2的類型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(Text.class);
// 第三、四、五、六步採用默認方式(分區,排序,規約,分組)
// 第七步:設置Reducer類
job.setReducerClass(ReduceJoinReducer.class);
// 設置Reduce階段的輸出類型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
// 第八步:設置輸出類
job.setOutputFormatClass(TextOutputFormat.class);
// 設置輸出路徑
TextOutputFormat.setOutputPath(job, new Path("hdfs://node01:8020/out/reduce_join_out"));
boolean b = job.waitForCompletion(true);
return b?0:1;
}
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
// 啓動一個任務
ToolRunner.run(configuration, new JobMain(), args);
}
}
運行結果: