大數據學習系列:Hadoop3.0苦命學習(四)

傳送門:
大數據學習系列:Hadoop3.0苦命學習(一)
大數據學習系列:Hadoop3.0苦命學習(二)
大數據學習系列:Hadoop3.0苦命學習(三)
大數據學習系列:Hadoop3.0苦命學習(四)

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 的輸出進行局部彙總,以減小網絡傳輸量

實現步驟

  1. 自定義一個 combiner 繼承 Reducer,重寫 reduce 方法
  2. 在 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 來拉數據。

詳細步驟

  1. 讀取數據組件 InputFormat (默認 TextInputFormat) 會通過 getSplits 方法對輸入
    目錄中文件進行邏輯切片規劃得到 splits, 有多少個 split 就對應啓動多少個
    MapTask. splitblock 的對應關係默認是一對一

  2. 將輸入文件切分爲 splits 之後, 由 RecordReader 對象 (默認是LineRecordReader)
    進行讀取, 以 \n 作爲分隔符, 讀取一行數據, 返回 <key ,value> . Key 表示每行首字符
    偏移值, Value 表示這一行文本內容

  3. 讀取 split 返回 <key,value> , 進入用戶自己繼承的 Mapper 類中,執行用戶重寫
    的 map 函數, RecordReader 讀取一行這裏調用一次

  4. Mapper 邏輯結束之後, 將 Mapper 的每條結果通過 context.write 進行collect數據
    收集. 在 collect 中, 會先對其進行分區處理,默認使用 HashPartitioner

    MapReduce 提供 Partitioner 接口, 它的作用就是根據 KeyValue
    Reducer 的數量來決定當前的這對輸出數據最終應該交由哪個 Reduce task
    處理, 默認對 Key Hash 後再以 Reducer 數量取模. 默認的取模方式只是爲了
    平均 Reducer 的處理能力, 如果用戶自己對 Partitioner 有需求, 可以訂製並設
    置到 Job 上

  5. 接下來, 會將數據寫入內存, 內存中這片區域叫做環形緩衝區, 緩衝區的作用是批量收集 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 內存中寫, 互不影響

  6. 當溢寫線程啓動後, 需要對這 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 的最終結果

  7. 合併溢寫文件, 每次溢寫會在磁盤上生成一個臨時文件 (寫之前判斷是否有 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 大致分爲 copysortreduce 三個階段,重點在前兩個階段。copy 階段包含一
eventFetcher 來獲取已完成的 map 列表,由 Fetcher 線程去 copy 數據,在此過程中
會啓動兩個 merge 線程,分別爲 inMemoryMergeronDiskMerger,分別將內存中的
數據 merge 到磁盤和將磁盤中的數據進行 merge。待數據 copy 完成之後,copy 階段就
完成了,開始進行 sort 階段,sort 階段主要是執行 finalMerge 操作,純粹的 sort 階段,完成之後就是 reduce 階段,調用用戶定義的 reduce 函數進行處理

詳細步驟

  1. Copy 階段 ,簡單地拉取數據。Reduce進程啓動一些數據copy線程(Fetcher),通過
    HTTP方式請求maptask獲取屬於自己的文件。
  2. Merge 階段 。這裏的merge如map端的merge動作,只是數組中存放的是不同map端
    copy來的數值。Copy過來的數據會先放入內存緩衝區中,這裏的緩衝區大小要比map
    端的更爲靈活。merge有三種形式:內存到內存;內存到磁盤;磁盤到磁盤。默認情
    況下第一種形式不啓用。當內存中的數據量到達一定閾值,就啓動內存到磁盤的
    merge。與map 端類似,這也是溢寫的過程,這個過程中如果你設置有Combiner,
    也是會啓用的,然後在磁盤中生成了衆多的溢寫文件。第二種merge方式一直在運
    行,直到沒有map端的數據時才結束,然後啓動第三種磁盤到磁盤的merge方式生成
    最終的文件。
  3. 合併排序 。把分散的數據合併成一個大的數據後,還會再對合並後的數據排序。
  4. 對排序後的鍵值對調用 reduce方法 ,鍵相等的鍵值對調用一次reduce方法,每次調用會
    產生零個或者多個鍵值對,最後把這些輸出的鍵值對寫入到HDFS文件中。

6 Shuffle 過程

map 階段處理的數據如何傳遞給 reduce 階段,是 MapReduce 框架中最關鍵的一個流
程,這個流程就叫 shuffle
shuffle: 洗牌、發牌 ——(核心機制:數據分區,排序,分組,規約,合併等過程)

在這裏插入圖片描述

shuffle 是 Mapreduce 的核心,它分佈在 Mapreduce 的 map 階段和 reduce 階段。一般
把從 Map 產生輸出開始到 Reduce 取得數據作爲輸入之前的過程稱作 shuffle。

  1. Collect 階段 :將 MapTask 的結果輸出到默認大小爲 100M 的環形緩衝區,保存的是
    key/value,Partition 分區信息等。
  2. Spill 階段 :當內存中的數據量達到一定的閥值的時候,就會將數據寫入本地磁盤,在
    將數據寫入磁盤之前需要對數據進行一次排序的操作,如果配置了 combiner,還會將
    有相同分區號和 key 的數據進行排序。
  3. Merge 階段 :把所有溢出的臨時文件進行一次合併操作,以確保一個 MapTask 最終只
    產生一箇中間數據文件。
  4. Copy 階段 :ReduceTask 啓動 Fetcher 線程到已經完成 MapTask 的節點上覆制一份
    屬於自己的數據,這些數據默認會保存在內存的緩衝區中,當內存的緩衝區達到一定
    的閥值的時候,就會將數據寫到磁盤之上。
  5. Merge 階段 :在 ReduceTask 遠程複製數據的同時,會在後臺開啓兩個線程對內存到
    本地的數據文件進行合併操作。
  6. 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);
    }
}

運行結果:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章