Hadoop(7) MapReduce的介紹以及編程案例


Hadoop(7)
MapReduce的介紹以及編程案例

MapReduce介紹

MapReduce是一個分佈式運算程序的 編程框架,是用戶基於Hadoop開發數據分析應用的和新框架.MapReduce核心工能是將用戶編寫的業務邏輯代碼和自帶默認的組件整合成一個完整的分佈式運算程序,併發運行在一個Hadoop集羣上.

MapReduce的特點

優點

  1. 易於編程

    MapReduce編程只需要實現幾個接口,完成一個串行化的程序就可以

  2. 良好的擴展性

    MapReduce程序可以運行在大量廉價的機器上運行,當計算資源得不到滿足時,可以簡單的通過增加機器數量來擴展集羣的計算能力

  3. 高容錯性

    如果一臺節點宕機,這臺機器上的計算任務就可以自動轉移到另外一臺節點上運行

  4. 適合PB級以上海量數據的離線處理

    MapReduce可以實現上千臺機器併發工作,提供數據處理了的能力

缺點

  1. 不擅長實時計算

    MapReduce無法在短時間內返回結果

  2. 不擅長流式計算

    MapReduce的輸入是靜態的,而流式計算是動態的


MapReduce 核心過程

在這裏插入圖片描述

Map階段

  1. 讀取數據,並按行處理
  2. 對行進行切分(按照指定的字符,如空格)
  3. 整理成鍵值對
  4. 將鍵值對結果寫入到磁盤

注意

1. Map階段的MapTask完全併發執行,互不干擾

2. Map階段對於每一個<K,V>鍵值對都會調用一次map()方法

Reduce階段

  1. 讀取Map階段的鍵值對結果
  2. 對Map的結果進行歸納整理(計數)
  3. 將統計結果輸出到磁盤(文件中)

注意

1. Reduce階段的ReduceTask也是併發執行,互補干擾

2. Reduce階段對於每一個<K,V>鍵值對都會調用一次reduce()方法

3. MapReduce編程模型只能包含一個Map階段和一個Reduce階段,如果用戶的業務邏輯比較複雜,那就只能多個MapReduce程序串行執行


Hadoop序列化

Hadoop序列化介紹

Java序列化是一個重量級框架,一個對象被序列化後會附帶很多額外信息,如果在的網絡中傳輸會佔用很大的資源,所以Hadoop自己有一套序列化機制 Writable

Hadoop序列化特點

  1. 緊湊: 高效使用空間
  2. 快速: 讀寫數據的額外開銷小
  3. 可擴展: 隨着通信協議的升級可升級
  4. 互操作: 支持多語言的交互

Hadoop序列化類型對比

Java類型 Hadoop Writable類型
Boolean BooleanWritable
Byte ByteWritable
Int IntWritable
Float FloatWritable
Long LongWritable
Double DoubleWritable
String Text
Map MapWritable
Array ArrayWritable

MapReduce編程主要流程

用戶編寫的程序分成三個部分:Mapper、Reducer和Driver

Mapper

  1. 自定義一個類,繼承Mapper類(org.apache.hadoop.mapreduce包下的)
public class MyMapper extends Mapper<K1, V1, K2, V2> {

}
  1. 定義泛型類型(mapper的輸入輸出類型)

說明

這裏要特別說明一下Mapper的泛型類型,四個類型分是:
<當前文件的偏移量,當前行的內容,map輸出的key的類型,map輸出的value的類型>
當前文件的偏移量: 是指相對於文件的開始,讀取到哪裏了,這裏偏移量是一字節爲單位,在實際編程中,這裏直接使用LongWritable就可以了,不需要特別關注

當前行的內容: MapReduce對於文件是按行讀取的,這裏的偏移量和當前行的內容就組成了一個輸入到mapper的鍵值對

map輸出的key的類型: map處理當前行之後輸出的鍵值對中的“鍵”

map輸出的value的類型: map處理當前行之後輸出的鍵值對中的“值”

如果白底黑字還是不理解,直接上圖!

假設Mapper的四個泛型分別是<K1,V1,K2,V2>

在這裏插入圖片描述

  1. 重寫map方法,將map階段的業務邏輯在map方法中實現
@Override
protected void map(K1, V1, Context context) throws IOException, InterruptedException {
    //map的方法體
}

說明

這裏map方法的參數類型,前兩個其實就是上面Mapper泛型的<K1,V1>,表示從文件輸入到Mapper的鍵值對,因爲之前說過,每個鍵值對都會調用一次map方法,所以這裏的map方法參數類型就表示輸入的鍵值對

第三個參數Context表示MapReduce的上下文,可以理解爲MapReduce的運行環境,Mapper的輸出(K2,V2)就是輸出到Context中,一般來說第三個參數是固定的,直接寫上Context就好

  1. 將鍵值對輸出
context.write(K2,V2 );

Reducer

  1. 自定義一個類繼承Reducer類(org.apache.hadoop.mapreduce包下的)
public class WcReducer extends Reducer<K1, V1, K2, V2> {

}
  1. 定義泛型類型(reducer的輸入輸出類型)

說明

Reducer的泛型類型和Mapper的泛型類型作用是一樣的,表示對於Reducer的輸入和輸出

其中K1,V1表示輸入,K2,V2表示輸出

因爲Mapper的輸出結果就是Reducer的輸入,所以Reduce的輸入類型和Mapper的輸出類型是一致的,也就是說,Reducer的K1,V1等於Mapper的K2.V2

  1. 重寫reduce方法,將Reduce階段的業務邏輯在reduce方法中實現
@Override
protected void reduce(K1, V1', Context context) throws IOException, InterruptedException {
        
}

說明

這裏注意的是reduce方法的參數,同map方法一樣,前兩個參數表示Reducer的輸入類型,但是這裏不同於map的是,因爲map輸出的不止是一個,而Key相同的鍵值對要匯聚到一個Reduce處理,所以reduce接收的應該是N多個鍵值對,所以reduce的第二個參數應該是一個迭代器,用於迭代遍歷這些鍵值對

第三個參數同map,默認寫Context就可以

  1. 輸出結果
//輸出統計結果
context.write(K2, V2);

Driver

定義該MapReduce程序的一些配置,相當於Yarn集羣的客戶端,用於提交該MapReduce程序到Yarn集羣

  1. 獲取Job實例
  2. 配置Driver類路徑
  3. 配置Map和Reduce
  4. 配置Mapper和Reduce的輸出的鍵值對類型
  5. 配置該MapReduce程序的輸入和輸出路徑
  6. 提交job,返回job執行結果

MapReduce編程舉例

MapReduce在Maven環境下的配置

  1. 首先新建一個Maven工程
  2. 在POM文件中引入MapReduce的依賴

注意除了MapReduce的依賴,還需要引入log4j日誌的依賴,因爲如果不配置日誌,我們就無法在控制檯查看程序運行的日誌,如果出現異常也就沒辦法通過控制檯查看

<dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-common</artifactId>
    <version>2.7.7</version>
</dependency>
<dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-client</artifactId>
    <version>2.7.7</version>
</dependency>
<dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-hdfs</artifactId>
    <version>2.7.7</version>
</dependency>
<!-- 這個是日誌的依賴-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.8.2</version>
</dependency>
  1. 配置log4j
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n

完成以上幾步,就可以愉快的開始MapReduce編程了!

編程舉例

舉例 1 官方舉例:統計文件中每個單詞出現的個數

  1. Mapper
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;


//Mapper的四個泛型分別是: 偏移量,當前行的內容,輸出的key,輸出的value
//注意這裏所有的類型都是用Hadoop的序列化類型
public class WcMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
    //創建需要的對象,word是單詞,one表示1次
    private Text word = new Text();
    private IntWritable one = new IntWritable(1);

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        //獲取這一行的數據
        String line=value.toString();

        //按照空格切分
        String[] words=line.split(" ");

        //遍歷數組,把單詞變成(word,1)的形式
        for(String word:words){
            //給變量賦值
            this.word.set(word);
            //將數據交給context,其實就是Mapper類中泛型中的後兩個類型
            context.write(this.word, this.one);
        }

    }
}
  1. Reducer
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

//四個泛型分別是:輸入的key,輸入的value,輸出的key,輸出的value
public class WcReducer extends Reducer<Text, IntWritable,Text,IntWritable> {
    //創建變量
    private IntWritable total = new IntWritable();

    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        //累加
        int sum=0;
        for (IntWritable value : values) {
            sum += value.get();
        }
        //將累加的結果轉變類型
        total.set(sum);
        //輸出統計結果
        context.write(key, total);
    }
}
  1. Driver
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 WcDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        //獲取一個Job實力
        Job job=Job.getInstance(new Configuration());

        //配置類路徑(classpath)
        job.setJarByClass(WcDriver.class);

        //配置Mapper和Reducer
        job.setMapperClass(WcMapper.class);
        job.setReducerClass(WcReducer.class);

        //配置Mapper的輸出類型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);

        //配置Reducer的輸出類型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        //配置輸入輸出數據路徑,分別是命令行輸入的第一個和第二個參數
        //注意導入的是org.apache.hadoop.mapreduce.lib包下的類
        FileInputFormat.setInputPaths(job,new Path(args[0]));
        FileOutputFormat.setOutputPath(job,new Path(args[1]));

        //提交job, 如果成功,返回0.失敗返回1
        boolean b = job.waitForCompletion(true);
        System.exit(b?0:1);

    }

}

舉例 2 流量使用統計

  1. 首先我們先模擬創建一個流量日誌文件,大體格式如下:

<手機號> <ip地址> <訪問網址> <上行流量> <下行流量>

我們需要從日誌中提取出 手機號 上行流量 下行流量,並計算上行流量和下行流量的和

17813913318 113.30.95.76 www.qq.com 1594 1541
15673789605 38.15.216.174 www.baidu.com 2647 1109
18353471332 85.185.214.83 www.qq.com 422 1450
15850131583 8.244.97.252 www.qq.com 774 2832
15824801037 179.120.142.17 www.taobao.com 612 60
15850702081 141.41.46.161 www.qq.com 400 2754
15672111580 120.170.51.153 www.baidu.com 2647 846
18367704639 99.50.51.234 www.qq.com 505 46
15661383493 52.191.212.206  2076 789
15889011264 80.124.192.178 www.taobao.com 143 2569
18319773861 209.143.170.216  15 149
19994848048 254.129.53.14  1043 2198
17834296412 180.222.63.18 www.taobao.com 395 392
  1. Bean

由於這裏的鍵值對,不只是單單的字符串,而是幾個字段,所以我們需要創建一個Bean來存儲這幾個字段

上面我們介紹過Hadoop的序列化機制,所以我們就需要序列化我們的Bean

import org.apache.hadoop.io.Writable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

/**
 * phoneflow
 * description: 存儲流量上行和下行的類
 */

public class FlowBean implements Writable {
    //創建手機流量的屬性,上行,下行和總流量
    private long upFlow;
    private long downFlow;
    private long sumFlow;

    //set/get方法
    public long getUpFlow() {
        return upFlow;
    }

    public void setUpFlow(long upFlow) {
        this.upFlow = upFlow;
    }

    public long getDownFlow() {
        return downFlow;
    }

    public void setDownFlow(long downFlow) {
        this.downFlow = downFlow;
    }

    public long getSumFlow() {
        return sumFlow;
    }

    public void setSumFlow(long sumFlow) {
        this.sumFlow = sumFlow;
    }

    /**
     * 方便起見,直接定義一個方法計算總流量,到時候直接調用這個方法就可以了
     * @param upFlow
     * @param downFlow
     */
    public void set(long upFlow, long downFlow) {
        this.upFlow=upFlow;
        this.downFlow=downFlow;
        this.sumFlow=upFlow+downFlow;
    }

    /**
     *	重寫toString
     */
    @Override
    public String toString() {
        return upFlow+" "+downFlow+" "+sumFlow;
    }


    /*
    重寫Writable的方法,
    注意,這裏的序列化方法和飯序列化方法中,屬性的順序一定要一致
    因爲一個對象怎麼序列化,就要怎麼反序列化
     */

    /**
     * 序列化方法
     * @param dataOutput    框架提供的數據出口
     * @throws IOException
     */
    @Override
    public void write(DataOutput dataOutput) throws IOException {
        dataOutput.writeLong(upFlow);
        dataOutput.writeLong(downFlow);
        dataOutput.writeLong(sumFlow);
    }

    /**
     *  反序列化方法
     * @param dataInput 框架提供的數據來源
     * @throws IOException
     */
    @Override
    public void readFields(DataInput dataInput) throws IOException {
        upFlow=dataInput.readLong();
        downFlow=dataInput.readLong();
        sumFlow=dataInput.readLong();
    }

}
  1. Mapper

根據第一步介紹的日誌格式,如果以空格將各個字段分開,手機好是第一個字段,上行流量和下行流量是倒數第一,二個字段,由於日誌中有些網址的內容是空的,所以如果上下行流量正着取值的話,會出現錯誤,如:

18367704639 99.50.51.234 www.qq.com 505 46
15661383493 52.191.212.206   2076 789

按空格分離字段,第一行內容上行流量是第4個字段,而在第二行內容中,上行流量是第三個字段

所以我們取上下行流量時,倒着去就不會出現這樣的問題,即fields.length - n

代碼如下:

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

/**
 * phoneflow
 * description:
 */

public class FlowMapper extends Mapper<LongWritable, Text,Text,FlowBean> {
    private Text phone = new Text();
    private FlowBean flow=new FlowBean();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        //輸入的內容分開
        String[] fields = value.toString().split(" ");

        //獲取手機號
        phone.set(fields[0]);
        //獲取上下行的流量值,因爲URI字段有可能爲空,所以需要倒數獲取該字段的值
        long upFlow = Long.parseLong((fields[fields.length - 2]));
        long downFlow = Long.parseLong(fields[fields.length - 1]);

        //獲取總流量
        flow.set(upFlow,downFlow);
        context.write(phone,flow);
    }
}
  1. Reducer
package phoneflow;

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

public class FlowReducer extends Reducer<Text,FlowBean,Text,FlowBean> {
    private FlowBean sumFlow = new FlowBean();

    @Override
    protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {
        //定義上行和下行流量
        long sumUpFlow=0;
        long sumDownFlow=0;

        //計算上下行流量
        for (FlowBean value : values) {
            sumUpFlow+=value.getUpFlow();
            sumDownFlow+=value.getDownFlow();
        }

        //計算總流量
        sumFlow.set(sumUpFlow, sumDownFlow);

        context.write(key, sumFlow);

    }
}
  1. Reducer
package phoneflow;

import org.apache.hadoop.conf.Configuration;
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.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

public class FlowDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        //獲取job實例
        Job job = Job.getInstance(new Configuration());

        //設置類路徑
        job.setJarByClass(FlowDriver.class);

        //設置Mapper和Reducer
        job.setMapperClass(FlowMapper.class);
        job.setReducerClass(FlowReducer.class);

        //設置輸入輸出類型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(FlowBean.class);

        //設置輸入輸出路徑
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        //提交
        boolean b = job.waitForCompletion(true);
        System.exit(b ? 0 : 1);
    }
}

測試和打包上傳

在IDE中運行

這裏用IDE是IDEA,如果用Eclipse的同學可以自己百度一下怎麼添加參數,這裏就不詳細做介紹了

下面介紹一下如何在IDEA上運行MapReduce程序:

  1. 編輯FlowDriver類的運行環境

在這裏插入圖片描述

  1. 配置輸入的文件加和輸出的文件夾,注意將自己模擬創建的流量日誌文件放在輸入文件夾裏,而且輸出文件夾不能存在!!!如果指定路徑有輸出文件夾,會報錯!

我這裏是直接使用的相對路徑(默認根目錄是該工程的根目錄)下的input目錄,然後輸出文件夾是output目錄

在這裏插入圖片描述

  1. 配置完成之後就可以運行main函數了,如果運行成功,成功會返回Process finished with exit code 0表示執行成功,這時候會自動生成指定好的output目錄,打開裏面的part-r-xxxxx就可以查看MapReduce的輸出內容

在這裏插入圖片描述

打jar包上傳到Hadoop服務器運行

  1. 首先打開Maven的窗口(一般在右側)

在這裏插入圖片描述

  1. 點開當前的project或者module–>Lifecycle–>package,就可以打成一個jar包,默認jar在target目錄裏

在這裏插入圖片描述

  1. 然後上傳jar包和流量日誌文件到Hadoop服務器,然後執行即可

具體執行jar包的語句介紹見Hadoop(2) 單節點模式(本地模式) 中的單節點官方測試部分

<Hadoop安裝目錄>/bin/hadoop jar <jar包路徑> <主類名稱> <輸入目錄路徑(流量日誌文件路徑)> <輸出路徑> 

ain函數了,如果運行成功,成功會返回Process finished with exit code 0表示執行成功,這時候會自動生成指定好的output目錄,打開裏面的part-r-xxxxx就可以查看MapReduce的輸出內容

[外鏈圖片轉存中…(img-6l99pnYf-1579752950996)].assets/1579684928176.png)

打jar包上傳到Hadoop服務器運行

  1. 首先打開Maven的窗口(一般在右側)

[外鏈圖片轉存中…(img-PY3go50c-1579752950998)].assets/1579752414644.png)

  1. 點開當前的project或者module–>Lifecycle–>package,就可以打成一個jar包,默認jar在target目錄裏

[外鏈圖片轉存中…(img-Cj6JID4B-1579752950998)].assets/1579752541686.png)

  1. 然後上傳jar包和流量日誌文件到Hadoop服務器,然後執行即可

具體執行jar包的語句介紹見 Hadoop(2) 單節點模式(本地模式) 中的單節點官方測試部分

<Hadoop安裝目錄>/bin/hadoop jar <jar包路徑> <主類名稱> <輸入目錄路徑(流量日誌文件路徑)> <輸出路徑> 
發佈了29 篇原創文章 · 獲贊 3 · 訪問量 1881
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章