目錄
一、MapReduce概述
1.MapReduce定義
MapReduce 是一個分佈式運算程序的編程框架,是用戶開發“基於Hadoop的數據分析應用”的核心框架。
MapReduce核心功能是將用戶編寫的業務邏輯代碼和自帶默認組件整合成一個完整的分佈式運算程序,並運行在一個Hadoop集羣上。
2.MapReduce優缺點
2.1優點
- MapReduce易於編程
它簡單的實現一些接口,就可以完成一個分佈式程序,這個分佈式程序可以分佈到大量廉價的PC機器上運行。也就是說你寫一個分佈式程序,跟寫一個簡單的串行程序是一模一樣的。這個特點是MapReduce編程變得非常流行。 - 良好的擴展性
當你的計算資源不能得到滿足的時候,你可以通過簡單的增加機器來擴展它的計算能力。 - 高容錯性
MapReduce設計的初衷就是使程序能夠部署在廉價的PC機器上,這就要求它具有很高的容錯性。比如其中一臺機器掛了,它可以把上面的計算任務轉移到另外一個節點上運行,不至於這個任務運行失敗,而這個過程不需要人工參與,而是完全有Hadoop內部完成。 - 適合PB級以上海量數據的離線處理
- 可以實現上千臺服務器集羣併發工作,提高數據處理能力。
2.2缺點
- 不擅長實時計算
MapReduce無法像MySQL一樣,在毫秒或者秒級內返回結果。 - 不擅長流式計算
流式計算的話輸入數據是動態的,而MapReduce的輸入數據集是靜態的,不能動態變化。這是因爲MapReduce自身的設計特點決定了數據源必須是靜態的。 - 不擅長有向圖(DAG)計算
多個應用程序存在依賴關係,後一個程序的輸入爲前一個的輸出。在這種情況下,MapReduce並不是不能做,而是使用後,每個MapReduce作業的輸出結果都會寫到磁盤,會造成大量的磁盤IO,導致性能非常的低下。
3.MapReduce核心思想
1)分佈式的運行程序往往需要分成兩個階段
(1)第一個階段的MapTask併發實例,完全並行運行,互不相干。
(2)第二個階段的ReduceTask併發實例,互不相干,但是他們的數據依賴上一個階段的所有MapTask併發實力的輸出。
2)MapReduce編程模型只能包含一個Map階段和一個Reduce階段,如果用戶的業務邏輯非常複雜,那就只能多個MapReduce程序,串行運行。
對上圖進行分析:
需求是統計每個單詞出現的次數,a-p開頭的單詞輸出結果到一個文件中,q - z開頭的單詞輸出到一個文件中。
(1)多個MapTask併發執行,每個Task負責一部分的單詞處理。Map階段的每個Task對文件進行讀數據,一般是按行獲取,然後把一行單詞切割成多個單詞,然後得到結果爲(單詞,1)的鍵值對的方式,還要對結果根據單詞首字母進行判斷,分成兩個區。
(2)Reduce階段通過Map的輸出結果彙總。Map階段的輸出就是Reduce階段的輸入,也就是Reduce拿到(單詞,1)這樣的格式的輸入,通過相同的key也就是單詞,進行value的累加,就可以得到每個單詞的總數,比如Map輸出的結果有(hadoop,1),(hadoop,1),(hadoop,1),然後Reduce階段判斷hadoop都是相同的key,對value進行累加,最後輸出的結果就是(hadoop,3)在寫到文件中。
4.MapReduce進程
一個完整的MapReduce程序在分佈式運行時有三類實例進程:
- MrAppMaster:負責整個程序的過程調度及狀態協調。
- MapTask:負責Map階段的整個數據處理流程。
- ReduceTask:負責Reduce階段的整個數據處理流程。
5.常用數據序列化類型
常用的數據類型對應Hadoop數據序列化類型:
6.MapReduce編程規範
用戶編寫的程序分成三個部分:Mapper、Reduce 和 Driver
-
Mapper階段
(1)用戶自定義的Mapper要繼承自己的父類
(2)Mapper的輸入數據是KV對的形式(KV的類型可自定義)
(3)Mapper中的業務邏輯寫在map()方法中
(4)Mapper的輸出數據是KV對的形式(KV的類型可自定義)
(5)map()方法(MapTask進程)對每一個<K,V>調用一次。 -
Reducer階段
(1)用戶自定義的Reducer要繼承自己的父類
(2)Reducer的輸入數據類型對應Mapper的輸出數據類型,也是KV
(3)Reducer的業務邏輯寫在reduce()方法中
(4)ReduceTask進程對每一組相同<k,v>組調用一次reduce()方法。 -
Driver階段
相當於YARN集羣的客戶端,用於提交我們整個程序到YARN集羣,提交的是封裝MapReduce程序相關運行參數的job對象。
7.WordCount案例實操
需求:
在給定的文本文件中統計輸出每一個單詞出現的總次數。
單詞準備:
- 創建Maven工程,並添加依賴:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>2.7.2</version>
</dependency>
</dependencies>
- 在項目的src/main/resources下創建名爲log4j.properties的文件,並在文件中添加:
(作用:可以查看代碼執行後產生的日誌)
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
- 編寫程序:
(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;
/**
* Map階段
*
* 需要繼承Mapper,並重寫map方法,完成自定義的Mapper,
*
* KEYIN:輸入的Key的類型
* VALUEIN:輸入的Value的類型
*
* KEYOUT:輸出的Key的類型
* VALUEOUT:輸出的Value的類型
*/
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
Text k = new Text();
IntWritable v = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1. 獲取一行數據,例如: hadoop hadoop
String line = value.toString();
//2. 切割數據
String[] split = line.split(" ");
//3. 寫出數據
for (String word : split) {
k.set(word);
System.out.println("k=====:" + k + "v=========" + v);
context.write(k,v);
}
}
}
(2)編寫Reduce類
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.util.Arrays;
/**
* Reduce 階段
* <p>
* 通過繼承Reduce,重寫 reduce方法,完成自定義的Reducer
*/
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
IntWritable v = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
//1. 將相同key的value進行彙總。
//hadoop 1
//hadoop 1
int sum = 0;
for (IntWritable value : values) {
sum += value.get();
}
v.set(sum);
//2. 寫出
context.write(key,v);
}
}
(3)編寫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;
public class WordCountDriver {
public static void main(String[] args) throws Exception {
//1. 獲取Job對象 ==> 一個MapReduce程序實際上就是一個job
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//2. 關聯jar
job.setJarByClass(WordCountDriver.class);
//3.關聯當前Job對應的Mapper 和 Reducer
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
//4. 設置Mapper 輸出的key 和 value的類型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//5. 設置最終輸出的key 和 value的類型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//6. 設置文件的輸入 和 結果的輸出位置
FileInputFormat.setInputPaths(job,new Path("E:/file/test/intout/inputWord/word"));
FileOutputFormat.setOutputPath(job,new Path("E:/file/test/intout/inputWord/word/outputWord"));
//7. 提交job
boolean result = job.waitForCompletion(true);
System.exit(result?0:1);
}
}
- 本地測試:
(1)這裏使用的win10的電腦,將win10的hadoop jar包解壓到非中文路徑,並在Windows環境上配置HADOOP_HOME 環境變量。
Path:
(2)運行程序:
日誌部分截圖:
結果:
打開文件:
5. 集羣上測試
(1)用maven打jar包,需要添加的打包插件依賴。
有一行需要改成自己主類工程
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin </artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.fseast.mr.wordcount.WordCountDriver</mainClass> <!-- 此處需修改 -->
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
(2)將程序打包成jar包,然後拷貝到Hadoop集羣中。(爲了方便操作,把名字修改爲wc.jar)
(3)啓動hadoop集羣
(4)執行WordCount程序
hadoop jar wc.jar com.fseast.mr.wordcount.WordcountDriver /user/fseast/input /user/fseast/output
二、Hadoop序列化
1.序列化概述
1.1什麼是序列化
序列化就是把內存中的對象,轉換成字節序列(或其他數據傳輸協議)以便存儲到磁盤(持久化)和網絡傳輸。
反序列化就是將收到字節序列(或其他數據傳輸協議)或者是磁盤的持久化數據,轉換成內存中的對象。
1.2爲什麼要序列化
一般來說,“活的”對象只生存在內存裏,關機斷電就沒有了。而且“活的”對象只能有本地的進程使用,不能被髮送到網絡上的另一臺計算機。然後序列化可以存儲“活的”對象,可以將“活的”對象發送到遠程計算機。
1.3爲什麼不使用Java的序列化
Java的序列化是一個重量級序列化框架(Serializable),一個對象被序列化後,會附帶很多額外的信息(各種校驗信息,Header,繼承體系等),不便於在網絡中高效傳輸。所以,Hadoop自己開發了一套序列化機制(Writable)
1.4Hadoop序列化特點
- 緊湊:高效實用存儲空間。
- 快速: 讀寫數據的額外開銷小。
- 可擴展:隨着通信協議的升級而可升級。
- 互操作:支持多語言的交互。
2.自定義bean對象實現序列化接口(Writable)
實際開發過程中往往常用的基本序列化類型不能滿足所有需求,比如在Hadoop框架內部傳遞一個bean對象,那麼該對象就需要實現序列化接口。
具體實現bean對象序列化步驟有如下7步:
(1)必須實現Writable接口
(2)反序列化時,需要反射調用空參構造函數,所以必須有空參構造器。
public FlowBean(){
super();
}
(3)重寫序列化方法
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
(4)重寫反序列化方法
@Override
public void readFields(DataInput in) throws IOException {
upFlow = in.readLong();
downFlow = in.readLong();
sumFlow = in.readLong();
}
(5)注意反序列化的順序和序列化的順序完全一致。
(6)要想把結果顯示在文件中,需要重寫toString,可用“\t”分開,方便後續切割。
(7)如果需要將自定義的bean放在key中傳輸,則還需要實現Comparable接口,因爲MapReduce框中的Shuffle過程要求對key必須能排序。
@Override
public int compareTo(FlowBean o) {
// 倒序排列,從大到小
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
3.序列化案例實操
- 需求
統計每個手機號耗費的總上行流量、下行流量、總流量
(1)原始數據如圖:
(2)輸入數據格式:
(3)要求輸出數據格式:
- 需求分析
Map階段:
(1)讀取一行數據,切分字段
(2)從字段中抽取出手機號、上行流量、下行流量。
(3)以手機號爲key,bean對象爲value輸出。
(4) bean對象要想能夠傳輸,必須實現序列化接口
Reduce階段:
累加上行流量和下行流量得到總流量。
- 編寫MapReduce程序
(1)編寫流量統計的Bean對象
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* 描述 上行流量 下行流量 總流量
*
* 因爲要寫到磁盤,因此該類要實現Hadoop的序列化接口
*/
public class FlowBean implements Writable {
private long upFlow;
private long downFlow;
private long sumFlow;
/**
* 反序列化時,需要反射調用空參構造器,所以必須有
*/
public FlowBean(){
super();
}
/**
* 序列化方法
*/
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeLong(upFlow);
dataOutput.writeLong(downFlow);
dataOutput.writeLong(sumFlow);
}
/**
* 反序列化方法
*
* 注意:反序列化的順序要與序列化的順序一致。
*/
@Override
public void readFields(DataInput dataInput) throws IOException {
upFlow = dataInput.readLong();
downFlow = dataInput.readLong();
sumFlow = dataInput.readLong();
}
public long getUpFlow() {
return upFlow;
}
public void setUpFlow(long upFlow) {
this.upFlow = upFlow;
}
public long getDownFlow() {
return downFlow;
}
public void setDownFlow(long dowanFlow) {
this.downFlow = dowanFlow;
}
public long getSumFlow() {
return sumFlow;
}
public void setSumFlow(long sumFlow) {
this.sumFlow = sumFlow;
}
//編寫toString方法,方便後續切割或使用
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFlow;
}
//爲了方便統計總流量
public void set(long upFlow,long dowanFlow){
this.upFlow = upFlow;
this.downFlow = dowanFlow;
this.sumFlow = upFlow + dowanFlow;
}
}
(2)編寫Mapper類
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class FlowCountMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
Text outK = new Text();
FlowBean outV = new FlowBean();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1. 獲取一行,
String line = value.toString();
//2. 切割字段
String[] split = line.split("\t");
//處理數據
//取出手機號
String k = split[1];
outK.set(k);
//取出上行流量和下行流量
String upFlow = split[split.length - 3];
String downFlow = split[split.length - 2];
outV.set(Long.parseLong(upFlow),Long.parseLong(downFlow));
//寫出
context.write(outK,outV);
}
}
(3)編寫Reducer類
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class FlowCountReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
FlowBean v = new FlowBean();
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {
//彙總
int totalUpFlow = 0;
int totalDownFlow = 0;
int totalSumFlow = 0;
//遍歷所有bean,將其中的上行流量,下行流量,總流量分別累加
for (FlowBean flowBean : values) {
totalUpFlow += flowBean.getUpFlow();
totalDownFlow += flowBean.getDownFlow();
totalSumFlow += flowBean.getSumFlow();
}
//封裝對象
v.setUpFlow(totalUpFlow);
v.setDownFlow(totalDownFlow);
v.setSumFlow(totalSumFlow);
//寫出
context.write(key, v);
}
}
(4)編寫Driver驅動類
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 FlowCountDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
args = new String[] {"e:/file/test/intout/inputWord","e:/file/test/intout/output"};
//1. 獲取Job
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//2. 指定本程序的jar包所在的本地路徑
job.setJarByClass(FlowCountDriver.class);
//3. 關聯Mapper 和 Reducer
job.setMapperClass(FlowCountMapper.class);
job.setReducerClass(FlowCountReducer.class);
//4. 設置Map輸出的key和value的類型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
//5. 設置最終輸出的key和value的類型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//6. 設置輸入和輸出路徑
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//7. 提交Job中配置的相關參數等
job.waitForCompletion(true);
}
}
(5)運行:
部分運行結果截圖: