大數據學習筆記之MapReduce

一、神馬是高大上的MapReduce

  MapReduce是Google的一項重要技術,它首先是一個編程模型,用以進行大數據量的計算。對於大數據量的計算,通常採用的處理手法就是並行計算。但對許多開發者來說,自己完完全全實現一個並行計算程序難度太大,而MapReduce就是一種簡化並行計算的編程模型,它使得那些沒有多有多少並行計算經驗的開發人員也可以開發並行應用程序。這也就是MapReduce的價值所在,通過簡化編程模型,降低了開發並行應用的入門門檻

1.1 MapReduce是什麼

  Hadoop MapReduce是一個軟件框架,基於該框架能夠容易地編寫應用程序,這些應用程序能夠運行在由上千個商用機器組成的大集羣上,並以一種可靠的,具有容錯能力的方式並行地處理上TB級別的海量數據集。這個定義裏面有着這些關鍵詞,一是軟件框架,二是並行處理,三是可靠且容錯,四是大規模集羣,五是海量數據集。

因此,對於MapReduce,可以簡潔地認爲,它是一個軟件框架,海量數據是它的“菜”,它在大規模集羣上以一種可靠且容錯的方式並行地“烹飪這道菜”。

1.2 MapReduce做什麼

  簡單地講,MapReduce可以做大數據處理。所謂大數據處理,即以價值爲導向,對大數據加工、挖掘和優化等各種處理。

  MapReduce擅長處理大數據,它爲什麼具有這種能力呢?這可由MapReduce的設計思想發覺。MapReduce的思想就是“分而治之”。

  (1)Mapper負責“分”,即把複雜的任務分解爲若干個“簡單的任務”來處理。“簡單的任務”包含三層含義:一是數據或計算的規模相對原任務要大大縮小;二是就近計算原則,即任務會分配到存放着所需數據的節點上進行計算;三是這些小任務可以並行計算,彼此間幾乎沒有依賴關係。

  (2)Reducer負責對map階段的結果進行彙總。至於需要多少個Reducer,用戶可以根據具體問題,通過在mapred-site.xml配置文件裏設置參數mapred.reduce.tasks的值,缺省值爲1。

一個比較形象的語言解釋MapReduce:  

We want to count all the books in the library. You count up shelf #1, I count up shelf #2. That’s map. The more people we get, the faster it goes.

我們要數圖書館中的所有書。你數1號書架,我數2號書架。這就是“Map”。我們人越多,數書就更快。

Now we get together and add our individual counts. That’s reduce.

現在我們到一起,把所有人的統計數加在一起。這就是“Reduce”。

1.3 MapReduce工作機制

  MapReduce的整個工作過程如上圖所示,它包含如下4個獨立的實體:

  實體一:客戶端,用來提交MapReduce作業。

  實體二:JobTracker,用來協調作業的運行。

  實體三:TaskTracker,用來處理作業劃分後的任務。

  實體四:HDFS,用來在其它實體間共享作業文件。

  通過審閱MapReduce的工作流程圖,可以看出MapReduce整個工作過程有序地包含如下工作環節:

二、Hadoop中的MapReduce框架

  在Hadoop中,一個MapReduce作業通常會把輸入的數據集切分爲若干獨立的數據塊,由Map任務以完全並行的方式去處理它們。框架會對Map的輸出先進行排序,然後把結果輸入給Reduce任務。通常作業的輸入和輸出都會被存儲在文件系統中,整個框架負責任務的調度和監控,以及重新執行已經關閉的任務。

  通常,MapReduce框架和分佈式文件系統是運行在一組相同的節點上,也就是說,計算節點和存儲節點通常都是在一起的。這種配置允許框架在那些已經存好數據的節點上高效地調度任務,這可以使得整個集羣的網絡帶寬被非常高效地利用。

2.1 MapReduce框架的組成

mapreduce

  (1)JobTracker

  JobTracker負責調度構成一個作業的所有任務,這些任務分佈在不同的TaskTracker上(由上圖的JobTracker可以看到2 assign map 和 3 assign reduce)。你可以將其理解爲公司的項目經理,項目經理接受項目需求,並劃分具體的任務給下面的開發工程師。

  (2)TaskTracker

  TaskTracker負責執行由JobTracker指派的任務,這裏我們就可以將其理解爲開發工程師,完成項目經理安排的開發任務即可。

2.2 MapReduce的輸入輸出

  MapReduce框架運轉在<key,value>鍵值對上,也就是說,框架把作業的輸入看成是一組<key,value>鍵值對,同樣也產生一組<key,value>鍵值對作爲作業的輸出,這兩組鍵值對有可能是不同的。

  一個MapReduce作業的輸入和輸出類型如下圖所示:可以看出在整個流程中,會有三組<key,value>鍵值對類型的存在。

2.3 MapReduce的處理流程

  這裏以WordCount單詞計數爲例,介紹map和reduce兩個階段需要進行哪些處理。單詞計數主要完成的功能是:統計一系列文本文件中每個單詞出現的次數,如圖所示:

  (1)map任務處理

  (2)reduce任務處理

三、第一個MapReduce程序:WordCount

  WordCount單詞計數是最簡單也是最能體現MapReduce思想的程序之一,該程序完整的代碼可以在Hadoop安裝包的src/examples目錄下找到。

  WordCount單詞計數主要完成的功能是:統計一系列文本文件中每個單詞出現的次數

3.1 初始化一個words.txt文件並上傳HDFS

  首先在Linux中通過Vim編輯一個簡單的words.txt,其內容很簡單如下所示:

Hello Edison Chou
Hello Hadoop RPC
Hello Wncud Chou
Hello Hadoop MapReduce
Hello Dick Gu

  通過Shell命令將其上傳到一個指定目錄中,這裏指定爲:/testdir/input

3.2 自定義Map函數

  在Hadoop 中, map 函數位於內置類org.apache.hadoop.mapreduce.Mapper<KEYIN,VALUEIN, KEYOUT, VALUEOUT>中,reduce 函數位於內置類org.apache.hadoop. mapreduce.Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT>中。

  我們要做的就是覆蓋map 函數和reduce 函數,首先我們來覆蓋map函數:繼承Mapper類並重寫map方法

複製代碼
    /**
     * @author Edison Chou
     * @version 1.0
     * @param KEYIN
     *            →k1 表示每一行的起始位置(偏移量offset)
     * @param VALUEIN
     *            →v1 表示每一行的文本內容
     * @param KEYOUT
     *            →k2 表示每一行中的每個單詞
     * @param VALUEOUT
     *            →v2 表示每一行中的每個單詞的出現次數,固定值爲1
     */
    public static class MyMapper extends
            Mapper<LongWritable, Text, Text, LongWritable> {
        protected void map(LongWritable key, Text value,
                Mapper<LongWritable, Text, Text, LongWritable>.Context context)
                throws java.io.IOException, InterruptedException {
            String[] spilted = value.toString().split(" ");
            for (String word : spilted) {
                context.write(new Text(word), new LongWritable(1L));
            }
        };
    }
複製代碼

  Mapper 類,有四個泛型,分別是KEYIN、VALUEIN、KEYOUT、VALUEOUT,前面兩個KEYIN、VALUEIN 指的是map 函數輸入的參數key、value 的類型;後面兩個KEYOUT、VALUEOUT 指的是map 函數輸出的key、value 的類型;

從代碼中可以看出,在Mapper類和Reducer類中都使用了Hadoop自帶的基本數據類型,例如String對應Text,long對應LongWritable,int對應IntWritable。這是因爲HDFS涉及到序列化的問題,Hadoop的基本數據類型都實現了一個Writable接口,而實現了這個接口的類型都支持序列化。

  這裏的map函數中通過空格符號來分割文本內容,並對其進行記錄;

3.3 自定義Reduce函數

  現在我們來覆蓋reduce函數:繼承Reducer類並重寫reduce方法

複製代碼
    /**
     * @author Edison Chou
     * @version 1.0
     * @param KEYIN
     *            →k2 表示每一行中的每個單詞
     * @param VALUEIN
     *            →v2 表示每一行中的每個單詞的出現次數,固定值爲1
     * @param KEYOUT
     *            →k3 表示每一行中的每個單詞
     * @param VALUEOUT
     *            →v3 表示每一行中的每個單詞的出現次數之和
     */
    public static class MyReducer extends
            Reducer<Text, LongWritable, Text, LongWritable> {
        protected void reduce(Text key,
                java.lang.Iterable<LongWritable> values,
                Reducer<Text, LongWritable, Text, LongWritable>.Context context)
                throws java.io.IOException, InterruptedException {
            long count = 0L;
            for (LongWritable value : values) {
                count += value.get();
            }
            context.write(key, new LongWritable(count));
        };
    }
複製代碼

  Reducer 類,也有四個泛型,同理,分別指的是reduce 函數輸入的key、value類型(這裏輸入的key、value類型通常和map的輸出key、value類型保持一致)和輸出的key、value 類型。

  這裏的reduce函數主要是將傳入的<k2,v2>進行最後的合併統計,形成最後的統計結果。

3.4 設置Main函數

  (1)設定輸入目錄,當然也可以作爲參數傳入

public static final String INPUT_PATH = "hdfs://hadoop-master:9000/testdir/input/words.txt";

  (2)設定輸出目錄(輸出目錄需要是空目錄),當然也可以作爲參數傳入

public static final String OUTPUT_PATH = "hdfs://hadoop-master:9000/testdir/output/wordcount";

  (3)Main函數的主要代碼

複製代碼
     public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();

        // 0.0:首先刪除輸出路徑的已有生成文件
        FileSystem fs = FileSystem.get(new URI(INPUT_PATH), conf);
        Path outPath = new Path(OUTPUT_PATH);
        if (fs.exists(outPath)) {
            fs.delete(outPath, true);
        }

        Job job = new Job(conf, "WordCount");
        job.setJarByClass(MyWordCountJob.class);

        // 1.0:指定輸入目錄
        FileInputFormat.setInputPaths(job, new Path(INPUT_PATH));
        // 1.1:指定對輸入數據進行格式化處理的類(可以省略)
        job.setInputFormatClass(TextInputFormat.class);
        // 1.2:指定自定義的Mapper類
        job.setMapperClass(MyMapper.class);
        // 1.3:指定map輸出的<K,V>類型(如果<k3,v3>的類型與<k2,v2>的類型一致則可以省略)
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(LongWritable.class);
        // 1.4:分區(可以省略)
        job.setPartitionerClass(HashPartitioner.class);
        // 1.5:設置要運行的Reducer的數量(可以省略)
        job.setNumReduceTasks(1);
        // 1.6:指定自定義的Reducer類
        job.setReducerClass(MyReducer.class);
        // 1.7:指定reduce輸出的<K,V>類型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(LongWritable.class);
        // 1.8:指定輸出目錄
        FileOutputFormat.setOutputPath(job, new Path(OUTPUT_PATH));
        // 1.9:指定對輸出數據進行格式化處理的類(可以省略)
        job.setOutputFormatClass(TextOutputFormat.class);
        // 2.0:提交作業
        boolean success = job.waitForCompletion(true);
        if (success) {
            System.out.println("Success");
            System.exit(0);
        } else {
            System.out.println("Failed");
            System.exit(1);
        }
    }
複製代碼

  在Main函數中,主要做了三件事:一是指定輸入、輸出目錄;二是指定自定義的Mapper類和Reducer類;三是提交作業;匆匆看下來,代碼有點多,但有些其實是可以省略的。

  (4)完整代碼如下所示

 View Code

3.5 運行吧小DEMO

  (1)調試查看控制檯狀態信息

  (2)通過Shell命令查看統計結果

四、使用ToolRunner類改寫WordCount

  Hadoop有個ToolRunner類,它是個好東西,簡單好用。無論在《Hadoop權威指南》還是Hadoop項目源碼自帶的example,都推薦使用ToolRunner。

4.1 最初的寫法

  下面我們看下src/example目錄下WordCount.java文件,它的代碼結構是這樣的:

複製代碼
public class WordCount {
    // 略...
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        String[] otherArgs = new GenericOptionsParser(conf, 
                                            args).getRemainingArgs();
        // 略...
        Job job = new Job(conf, "word count");
        // 略...
        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }
}
複製代碼

  WordCount.java中使用到了GenericOptionsParser這個類,它的作用是將命令行中參數自動設置到變量conf中。舉個例子,比如我希望通過命令行設置reduce task數量,就這麼寫:

bin/hadoop jar MyJob.jar com.xxx.MyJobDriver -Dmapred.reduce.tasks=5

  上面這樣就可以了,不需要將其硬編碼到java代碼中,很輕鬆就可以將參數與代碼分離開。

4.2 加入ToolRunner的寫法

  至此,我們還沒有說到ToolRunner,上面的代碼我們使用了GenericOptionsParser幫我們解析命令行參數,編寫ToolRunner的程序員更懶,它將 GenericOptionsParser調用隱藏到自身run方法,被自動執行了,修改後的代碼變成了這樣:

複製代碼
public class WordCount extends Configured implements Tool {
    @Override
    public int run(String[] arg0) throws Exception {
        Job job = new Job(getConf(), "word count");
        // 略...
        System.exit(job.waitForCompletion(true) ? 0 : 1);
        return 0;
    }

    public static void main(String[] args) throws Exception {
        int res = ToolRunner.run(new Configuration(), new WordCount(), args);
        System.exit(res);
    }
}
複製代碼

  看看這段代碼上有什麼不同:

  (1)讓WordCount繼承Configured並實現Tool接口

  (2)重寫Tool接口的run方法,run方法不是static類型,這很好。

  (3)在WordCount中我們將通過getConf()獲取Configuration對象

  可以看出,通過簡單的幾步,就可以實現代碼與配置隔離、上傳文件到DistributeCache等功能。修改MapReduce參數不需要修改java代碼、打包、部署,提高工作效率。

4.3 重寫WordCount程序

複製代碼
public class MyJob extends Configured implements Tool {
    public static class MyMapper extends
            Mapper<LongWritable, Text, Text, LongWritable> {
        protected void map(LongWritable key, Text value,
                Mapper<LongWritable, Text, Text, LongWritable>.Context context)
                throws java.io.IOException, InterruptedException {
                       ......
            }
        };
    }

    public static class MyReducer extends
            Reducer<Text, LongWritable, Text, LongWritable> {
        protected void reduce(Text key,
                java.lang.Iterable<LongWritable> values,
                Reducer<Text, LongWritable, Text, LongWritable>.Context context)
                throws java.io.IOException, InterruptedException {
                       ......
        };
    }

    // 輸入文件路徑
    public static final String INPUT_PATH = "hdfs://hadoop-master:9000/testdir/input/words.txt";
    // 輸出文件路徑
    public static final String OUTPUT_PATH = "hdfs://hadoop-master:9000/testdir/output/wordcount";

    @Override
    public int run(String[] args) throws Exception {
        // 首先刪除輸出路徑的已有生成文件
        FileSystem fs = FileSystem.get(new URI(INPUT_PATH), getConf());
        Path outPath = new Path(OUTPUT_PATH);
        if (fs.exists(outPath)) {
            fs.delete(outPath, true);
        }

        Job job = new Job(getConf(), "WordCount");
        // 設置輸入目錄
        FileInputFormat.setInputPaths(job, new Path(INPUT_PATH));
        // 設置自定義Mapper
        job.setMapperClass(MyMapper.class);
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(LongWritable.class);
        // 設置自定義Reducer
        job.setReducerClass(MyReducer.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(LongWritable.class);
        // 設置輸出目錄
        FileOutputFormat.setOutputPath(job, new Path(OUTPUT_PATH));

        System.exit(job.waitForCompletion(true) ? 0 : 1);
        return 0;
    }

    public static void main(String[] args) {
        Configuration conf = new Configuration();
        try {
            int res = ToolRunner.run(conf, new MyJob(), args);
            System.exit(res);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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