目錄
2.13 爲什麼不使用java 的序列化Serializable
2.3 自定義bean對象實現序列化接口(Writable)
3.2.3 CombineTextInputFormat案例
1 MapReduce入門
1.1 MapReduce定義
Mapreduce是一個分佈式運算程序的編程框架,是用戶開發“基於hadoop的數據分析應用”的核心框架。
Mapreduce核心功能是將用戶編寫的業務邏輯代碼和自帶默認組件整合成一個完整的分佈式運算程序,併發運行在一個hadoop集羣上。
1.2 MapReduce的優缺點
1. 優點
1.MapReduce 易於編程
它簡單的實現一些接口,就可以完成一個分佈式程序,這個分佈式程序可以分佈到大量廉價的PC機器上運行。就是因爲這個特點使得MapReduce編程變得非常流行。
2.良好的擴展性
當你的計算資源不能得到滿足的時候,你可以通過簡單的增加機器來擴展它的計算能力。
3.高容錯性
MapReduce設計的初衷就是使程序能夠部署在廉價的PC機器上,這就要求它具有很高的容錯性。比如其中一臺機器掛了,它可以把上面的計算任務轉移到另外一個節點上運行,不至於這個任務運行失敗,而且這個過程不需要人工參與,而完全是由Hadoop內部完成的。
4.適合PB級以上海量數據的離線處理
它適合離線處理而不適合在線處理。比如像毫秒級別的返回一個結果,MapReduce很難做到。
2. 缺點
MapReduce不擅長做實時計算、流式計算、DAG(有向圖)計算。
1. 實時計算
MapReduce無法像Mysql一樣,在毫秒或者秒級內返回結果。
2. 流式計算
流式計算的輸入數據是動態的,而MapReduce的輸入數據集是靜態的,不能動態變化。這是因爲MapReduce自身的設計特點決定了數據源必須是靜態的。
3. DAG(有向圖)計算
多個應用程序存在依賴關係,後一個應用程序的輸入爲前一個的輸出。在這種情況下,MapReduce並不是不能做,而是使用後,每個MapReduce作業的輸出結果都會寫入到磁盤,會造成大量的磁盤IO,導致性能非常的低下。
1.3 MapReduce核心思想
- 分佈式的運算程序往往至少需要分爲兩個階段
- 第一階段的maptask併發實例,完全並行運行,互不相干
- 第二階段的reudcetask併發實例互不相干,但是他們的數據依賴於上一個階段所有maptask併發實例的輸出
- MapReudce編程模型只能包含一個map階段和reduce階段,如果業務邏輯特別複雜,那就只能多個mapreduce程序串行執行
1.4 MapReduce進程(MR)
一個完整的mapreduce程序在分佈式運行時有三類實例進程:
-
MrAppMaster:負責整個程序的過程調度及狀態協調。
-
MapTask:負責map階段的整個數據處理流程。
-
ReduceTask:負責reduce階段的整個數據處理流程。
1.5 MapReduce編程規範
用戶編寫的程序分爲三個部分:Mapper、Reducer和Driver
- Map階段:
- 用戶自定義的Mapper要繼承自己的父類
- Mapper的輸入數據時KV對的形式(KV的數據類型可自定義)
- Mapper中的業務邏輯寫在map()方法中
- Mapper的輸出數據是KV對的形式(KV的數據類型可自定義)
- map()方法(maptask進程)對每一個<K,V>調用一次
- Reduce階段:
- 用戶自定義的Reducer要繼承自己的父類
- Reducer的輸入數據類型要對應Mapper的輸出數據類型,也是KV格式的
- Reducer的業務邏輯寫在reduce()方法中
- reducetask進程對每一組相同K的<K,V>組調用一次reduce方法
- Driver階段(關聯Mapper和Reducer,並且提交任務到集羣)
相當於yarn集羣的客戶端,用於提交我們整個程序到yarn集羣,提交的是封裝了mapreduce程序相關運行參數的job對象
1.6 WordCount案例
1. 需求:dui下面給定的數中統計每一個單詞出現的總次數
2. 需求分析:按照mapreduce規範,分別編寫Mapper、Reducer、Driver
3. 準備工作
導入下面的依賴,配置文件
<dependencies> <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-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>
配置文件:
log4j.rootLogger=debug, 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
4. 編寫程序
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;
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.獲取一行的數據
String line = value.toString();
//2.切割
String[] strings = line.split(" ");
//3.輸出
for (String string : strings) {
k.set(string);
context.write(k,v);
}
}
}
2.Reducer類
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class WordcountReducer extends Reducer<Text, IntWritable, Text, IntWritable>{
int sum;
IntWritable v = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> value,
Context context) throws IOException, InterruptedException {
// 1 累加求和
sum = 0;
for (IntWritable count : value) {
sum += count.get();
}
// 2 輸出
v.set(sum);
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.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class WordCountDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//1.獲取配置信息及封裝任務
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
//2.設置jar加載路徑
job.setJarByClass(WordCountDriver.class);
//3.設置map和reduce類
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordcountReducer.class);
//4.設置map輸出
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//5.設置reduce輸出
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//6.設置輸入輸出的路徑
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//7.提交
job.waitForCompletion(true);
}
}
4. 本地測試
需要在window上配置HADOOP_HPME的環境變量,然後再IDEA上運行
5. 集羣測試
- 將程序打jar包,上傳到hadoop集羣
- 啓動hadoop集羣,運行wordcount程序
[hadoop@hadoop101 software]$ hadoop jar wordcount.jar com.bigdata.wordcount.WordcountDriver /user/hadoop/input /user/bigdata/output1 //上傳的jar的名稱,驅動類的包名+類名,輸入路徑,輸出路徑
2 Hadoop序列化
2.1 序列化概述
2.1.1 什麼是序列化
序列話就是將內存中的對象,轉換成字節序列(或者其他的傳輸協議)以便於存儲(持久化)和網絡傳輸
反序列化就是將收到的字節序列(或其他的傳輸協議)或者磁盤上持久化的數據。轉換爲內存中的對象
2.1.2 爲什麼要序列化
一般來說,“活的”對象只生存在內存裏,關機斷電就沒有了。而且“活的”對象只能由本地的進程使用,不能被髮送到網絡上的另外一臺計算機。 然而序列化可以存儲“活的”對象,可以將“活的”對象發送到遠程計算機。
2.13 爲什麼不使用java 的序列化Serializable
java的序列化是一個重量級序列化框架,一個對象被序列化後,會附帶很多額外的信息(各種校驗,herder,繼承體系等),不便於在網絡中高效的傳輸。所以hadoop自己開發了一套序列化機制(Writable),有以下特點:
緊湊:緊湊的格式能讓我們充分利用網絡帶寬,而網絡帶寬是數據中心最稀缺的資源
快速:進程通信形成了分佈式系統的骨架,所以需要儘量減少序列化和反序列化的性能開銷
互操作:能支持不同語言寫的客戶端和服務端進行交互
2.2 常用的數據序列化類型
常用的數據類型對應的hadoop數據序列化類型
Java類型 |
Hadoop Writable類型 |
boolean |
BooleanWritable |
byte |
ByteWritable |
int |
IntWritable |
float |
FloatWritable |
long |
LongWritable |
double |
DoubleWritable |
String |
Text |
map |
MapWritable |
array |
ArrayWritable |
2.3 自定義bean對象實現序列化接口(Writable)
自定義bean對象要想序列化傳輸,必須實現序列化接口,必須注意以下事項:
- 必須實現Writable接口
- 反序列化時,需要反射調用空參構造函數,所以必須有空參構造
- 重寫序列化方法
- 重寫反序列化方法
- 注意反序列化的順序要和序列化的順序一致
- 要想把結果顯示在文件中,需要重寫toString()方法,可用 “\t” 分開,方便後續調用
2.4 序列化案例
1. 需求: 統計每一個手機號耗費的總上行流量、下行流量、總流量
輸入數據格式: 1363157993055 13560436666 C4-17-FE-BA-DE-D9:CMCC 120.196.100.99 18 15 1116 954 200 手機號碼 上行流量 下行流量 |
輸出數據格式 1356·0436666 1116 954 2070 手機號碼 總上行流量 總下行流量 總流量 |
數據: 1363157985066 13726230503 00-FD-07-A4-72-B8:CMCC 120.196.100.82 i02.c.aliimg.com 24 27 2481 24681 200 |
2. 分析
Map階段:
(1)讀取一行數據,切分字段
(2)抽取手機號、上行流量、下行流量
(3)以手機號爲key,bean對象爲value輸出,即context.write(手機號,bean);
Reduce階段:
(1)累加上行流量和下行流量得到總流量。
(2)實現自定義的bean來封裝流量信息,並將bean作爲map輸出的key來傳輸
(3) MR程序在處理數據的過程中會對數據排序(map輸出的kv對傳輸到reduce之前,會排序),排序的依據是map輸出的key
3. 編寫程序
流量統計的bean對象
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableComparable;
// 1 實現writable接口
public class FlowBean implements WritableComparable{
private long upFlow ;
private long downFlow;
private long sumFlow;
//2 反序列化時,需要反射調用空參構造函數,所以必須有
public FlowBean() {
super();
}
public FlowBean(long upFlow, long downFlow) {
super();
this.upFlow = upFlow;
this.downFlow = downFlow;
this.sumFlow = upFlow + downFlow;
}
//3 寫序列化方法
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
//4 反序列化方法
//5 反序列化方法讀順序必須和寫序列化方法的寫順序必須一致
@Override
public void readFields(DataInput in) throws IOException {
this.upFlow = in.readLong();
this.downFlow = in.readLong();
this.sumFlow = in.readLong();
}
// 6 編寫toString方法,方便後續打印到文本
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFlow;
}
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;
}
}
Mapper類
import java.io.IOException;
import com.bigdata.mapreduce.flow.FlowBean;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
public class FlowCountMapper extends Mapper<LongWritable, Text, Text, FlowBean>{
FlowBean v = new FlowBean();
Text k = new Text();
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
// 1 獲取一行
String line = value.toString();
// 2 切割字段
String[] fields = line.split("\t");
// 3 封裝對象
// 取出手機號碼
String phoneNum = fields[1];
// 取出上行流量和下行流量
long upFlow = Long.parseLong(fields[fields.length - 3]);
long downFlow = Long.parseLong(fields[fields.length - 2]);
v.setUpFlow(upFlow);
v.setDownFlow(downFlow);
k.set(phoneNum);
// 4 寫出
context.write(k,v);
}
}
Reducer類
import java.io.IOException;
import com.bigdata.mapreduce.flow.FlowBean;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
public class FlowCountReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Context context)
throws IOException, InterruptedException {
long sum_upFlow = 0;
long sum_downFlow = 0;
// 1 遍歷所用bean,將其中的上行流量,下行流量分別累加
for (FlowBean flowBean : values) {
sum_upFlow += flowBean.getUpFlow();
sum_downFlow += flowBean.getDownFlow();
}
// 2 封裝對象
FlowBean resultBean = new FlowBean(sum_upFlow, sum_downFlow);
// 3 寫出
context.write(key, resultBean);
}
}
Driver類
import java.io.IOException;
import com.bigdata.mapreduce.flow.FlowBean;
import com.bigdata.mapreduce.flow.FlowCountMapper;
import com.bigdata.mapreduce.flow.FlowCountReducer;
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;
public class FlowsumDriver {
public static void main(String[] args) throws IllegalArgumentException, IOException, ClassNotFoundException, InterruptedException {
// 1 獲取配置信息,或者job對象實例
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 6 指定本程序的jar包所在的本地路徑
job.setJarByClass(FlowsumDriver.class);
// 2 指定本業務job要使用的mapper/Reducer業務類
job.setMapperClass(FlowCountMapper.class);
job.setReducerClass(FlowCountReducer.class);
// 3 指定mapper輸出數據的kv類型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
// 4 指定最終輸出的數據的kv類型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
// 5 指定job的輸入原始文件所在目錄
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 7 將job中配置的相關參數,以及job所用的java類所在的jar包, 提交給yarn去運行
boolean result = job.waitForCompletion(true);
}
}
3 MapReduce框架原理
3.1 MapReduce工作流程
流程:
上面的流程是整個mapreduce整個工作流程,shuffle過程需要詳細介紹,下面是具體的shuffle過程:
- maptask手機我們的map()方法輸出的kv對,放到環形內存緩衝區;
- 內存中的容量達到一定的閾值,不斷的溢寫到本地磁盤,可能會溢寫出多個文件
- 多個小文件會被合併爲大的溢出文件
- 在溢出過程及合併的工程中,都要調用partitione進行分區和針對key進行排序
- reducetask根據自己的分區號,去各個maptask進程節點上獲取相應的分區數據
- reducetask獲取到多個maptask結果文件,將這些文件再次進行合併(歸併排序)
- 合併成一個大文件後,shuffle的過程也就結束了,後面進入reducetask的邏輯運算過程
注意:
shuffle中的環形緩衝區的大小會影響到MR程序的執行效率,原則上說,緩衝區越大,進行磁盤IO的次數越少,執行速度就越快
緩衝區的大小可以通過參數調整,參數:io.sort.mb 默認100M
3.2 InputFormat數據輸入
3.2.1 FileInputFormat操作流程
- 找到數據所在目錄;
- 開始遍歷處理(規劃切片)目錄下的每一個文件
- 遍歷第一個文件xx.txt(假設300M)
- 獲取文件的大小fs.sizeOf(xx.txt)
- 默認切片大小=blocksize(128M)
- 開始切片,形成第1個切片信息:xx.txt-0~128M 第2個切片信息:xx.txt-128M~256M 第3個切片信息:xx.txt-256M~300M(每次切片時,都要判斷切完剩下的部分是否大於塊的1.1倍,不大於1.1倍就劃分一塊切片,比如說剩下部分大於128m但是小於140m(大概是這個區間))
- 將切片信息寫到一個切片規劃文件中
- 數據切片只是邏輯上對輸入數據進行分片,並不會在磁盤上將文件切分成分片文件進行存儲。使用InputSplit只記錄了分片的元數據信息,比如某一個切片文件的起始位置、長度以及所在節點等
- block是HDFS物理上存儲的數據,切片是對數據邏輯上的劃分
- 提交切片規劃到yarn上,yarn上的MrAppMaster就可以根據切片規劃文件計算開啓maptask的個數
3.2.2 FileInputFormat切片機制
1. FileInputFormat中默認的切片機制(底層使用textInputFormat)
- 簡單的按照文件的內容長度進行切片
- 切片大小默認等於block大小
- 切片時不考慮數據集整體,逐個針對每一個文件單獨切片
比如待處理有兩個文件
file1.txt 320M
file2.txt 10M
經過FileInputFormat的切片機制運算後,形成的切片信息如下:
file1.txt.split1-- 0~128
file1.txt.split2-- 128~256
file1.txt.split3-- 256~320
file2.txt.split1-- 0~10M
2. CombineTextInputFormat切片機制
針對大量小文件的優化策略
默認情況下TextIuptFormat對任務的切片機制時候按文件規劃切片,不管文件多小,都會是一個單獨的切片,都會交給一個maptask,這樣如果有大量小文件,就會產生大量的maptask,處理效率及其低下
優化策略
- 最好的辦法,在數據處理系統的最前端(預處理),將小文件先合併成大文件,在上傳HDFS做後續分析;
- 補救措施:如果已經是大量的小文件在HDFS上了,可以使用另一種CombineTextInputFormat來做切片,它的切片邏輯可以將多個小文件從邏輯上規劃爲一個切片中,這樣多個小文件就交給一個maptask進行處理
- 優先滿足最小切片大小,不超過最大切片大小
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304); // 4m
CombineTextInputFormat.setMinInputSplitSize(job, 2097152); // 2m
舉例:0.5m+1m+0.3m+5m=2m + 4.8m=2m + 4m + 0.8m 這樣最後就是三個分區
具體實現(需要首先在Driver中進行註冊)
// 如果不設置InputFormat,它默認用的是TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class)
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
CombineTextInputFormat.setMinInputSplitSize(job, 2097152);// 2m
3.2.3 CombineTextInputFormat案例
1. 需求:將輸入的大量小文件合併成成以一個切片統一處理
2. 輸入數據:準備五個小文件
3. 實現過程
未作任何處理,在最初的wordcount程序中,觀察切片個數爲5
在WordCountDriver中增加下面的代碼,運行程序,觀察切片信息
// 如果不設置InputFormat,它默認用的是TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
CombineTextInputFormat.setMinInputSplitSize(job, 2097152);// 2m
3.3 MapTask工作機制
3.3.1 並行度決定機制
1. 問題引出
maptask的並行度決定map階段的任務處理併發度,進而影響到整個job的處理速度。那麼maptask的並行任務是否越多越好呢?
2. MapTask並行度決定機制
一個job的map階段MapTask並行度(個數),由客戶端提交job時的切片個數決定
下面兩個圖解釋了爲什麼分片要和block塊的大小一致,切片大小跟hdfs存儲block大小不一致會導致,數據傳輸的問題,在大數據中,寧可移動計算,也不要移動數據
3.3.2 MapTask工作機制
(1)Read階段:Map Task通過用戶編寫的RecordReader,從輸入InputSplit中解析出一個個key/value。
(2)Map階段:該節點主要是將解析出的key/value交給用戶編寫map()函數處理,併產生一系列新的key/value。
(3)Collect收集階段:在用戶編寫map()函數中,當數據處理完成後,一般會調用context.write,context.write底層 OutputCollector.collect()輸出結果。在該函數內部,它會將生成的key/value分區(調用Partitioner),並寫入一個環形內存緩衝區中。
(4)Spill階段:即“溢寫”,當環形緩衝區滿後,MapReduce會將數據寫到本地磁盤上,生成一個臨時文件。需要注意的是,將數據寫入本地磁盤之前,先要對數據進行一次本地排序,並在必要時對數據進行合併等操作。
(5)Combine階段:當所有數據處理完成後,MapTask對所有臨時文件進行一次合併,以確保最終只會生成一個數據文件。
在進行文件合併過程中,MapTask以分區爲單位進行合併。對於某個分區,它將採用多輪遞歸合併的方式。每輪合併io.sort.factor(默認100)個文件,並將產生的文件重新加入待合併列表中,對文件排序後,重複以上過程,直到最終得到一個大文件。
3.4 Shuffle機制
3.4.1 shuffle機制
Mapreduce確保每個reducer的輸入都是按key排序的。系統執行排序的過程(即將mapper輸出作爲輸入傳給reducer)稱爲shuffle
3.4.2 Partition分區
分區:把數據扎堆存放
問題引出:要求將統計結果按照條件輸出到不同文件中(分區)。比如:將統計結果按照手機歸屬地不同省份輸出到不同文件中(分區)
1. 默認partition分區 hello-->hash%reducetask數量
public class HashPartitioner<K, V> extends Partitioner<K, V> {
public int getPartition(K key, V value, int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
默認分區是根據key的hashcode對reducetask的個數取模得到的,用戶無法控制那個key存儲到哪個分區
2. 自定義partition步驟
1. 自定義類繼承Partitioner,重寫getPartition()方法
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
import org.junit.Test;
public class ProvincePartition extends Partitioner<Text,FlowBean> {
@Override
public int getPartition(Text text, FlowBean flowBean, int i) {
String preNum = text.toString().substring(0,3);
int partition = 4;
if ("136".equals(preNum)) {
partition = 0;
}else if ("137".equals(preNum)) {
partition = 1;
}else if ("138".equals(preNum)) {
partition = 2;
}else if ("139".equals(preNum)) {
partition = 3;
}
return partition;
}
}
2. 在job驅動類中,註冊自定義分區類
job.setPartitionerClass(CustomPartitioner.class);
3. 自定義partition後,根據自定義partition的邏輯設置相應數量的reducetask
job.setNumReduceTasks(5);
3. 注意
reduceTask的個數決定了有幾個文件!!
如果reduceTask的數量 > getPartition的結果數,則會多產生幾個空的輸出文件part-r-000xx;
如果1< reduceTask的數量 < getPartition的結果數,則有一部分分區數據無處安放,會Exception;
如果reduceTask的數量 = 1,則不管mapTask端輸出多少個分區文件,最終結果都交給這一個reduceTask,最終也就只會產生一個結果文件 part-r-00000;
例如:假設自定義分區數爲5,則
(1)job.setNumReduceTasks(1);會正常運行,只不過會產生一個輸出文件
(2)job.setNumReduceTasks(2);會報錯
(3)job.setNumReduceTasks(6);大於5,程序會正常運行,會產生空文件
3.4.3 partition分區案例
1. 需求:將統計結果按照手機歸屬地不同省份輸出到不同文件中(分區)
2. 數據準備:使用流量統計案例中的數據
3. 分析
(1)Mapreduce中會將map輸出的kv對,按照相同key分組,然後分發給不同的reducetask。默認的分發規則爲:根據key的hashcode%reducetask數來分發
(2)如果要按照我們自己的需求進行分組,則需要改寫數據分發(分組)組件Partitioner,自定義一個CustomPartitioner繼承抽象類:Partitioner
(3)在job驅動中,設置自定義partitioner: job.setPartitionerClass(CustomPartitioner.class)
4. 在流量統計案例基礎上,增加一個自定義分區類
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
import org.junit.Test;
public class ProvincePartition extends Partitioner<Text,FlowBean> {
@Override
public int getPartition(Text text, FlowBean flowBean, int i) {
String preNum = text.toString().substring(0,3);
int partition = 4;
if ("136".equals(preNum)) {
partition = 0;
}else if ("137".equals(preNum)) {
partition = 1;
}else if ("138".equals(preNum)) {
partition = 2;
}else if ("139".equals(preNum)) {
partition = 3;
}
return partition;
}
}
5. 在驅動類中將自定義的分區類註冊並設置reducetask的數量,在Driver類基礎上增加下面內容
// 8 將自定義數據分區註冊
job.setPartitionerClass(ProvincePartitioner.class);
// 9 設置相應數量的reduce task
job.setNumReduceTasks(5);
3.4.4 WritableComparable排序
排序是MapReduce框架中最重要的操作之一。Map Task和Reduce Task均會對數據(按照key)進行排序。該操作屬於Hadoop的默認行爲。任何應用程序中的數據均會被排序,而不管邏輯上是否需要。默認排序是按照字典順序排序。
對於Map Task,它會將處理的結果暫時放到一個緩衝區中,當緩衝區使用率達到一定閾值後,再對緩衝區中的數據進行一次排序,並將這些有序數據寫到磁盤上,而當數據處理完畢後,它會對磁盤上所有文件進行一次合併,以將這些文件合併成一個大的有序文件。
對於Reduce Task,它從每個Map Task上遠程拷貝相應的數據文件,如果文件大小超過一定閾值,則放到磁盤上,否則放到內存中。如果磁盤上文件數目達到一定閾值,則進行一次合併以生成一個更大文件;如果內存中文件大小或者數目超過一定閾值,則進行一次合併後將數據寫到磁盤上。當所有數據拷貝完畢後,Reduce Task統一對內存和磁盤上的所有數據進行一次合併。
每個階段的默認排序
1. 排序的分類
(1)部分排序:
MapReduce根據輸入記錄的鍵對數據集排序。保證輸出的每個文件內部排序。
(2)全排序:
如何用Hadoop產生一個全局排序的文件?最簡單的方法是使用一個分區。但該方法在處理大型文件時效率極低,因爲一臺機器必須處理所有輸出文件,從而完全喪失了MapReduce所提供的並行架構。
替代方案:首先創建一系列排好序的文件;其次,串聯這些文件;最後,生成一個全局排序的文件。主要思路是使用一個分區來描述輸出的全局排序。例如:可以爲上述文件創建3個分區,在第一分區中,記錄的單詞首字母a-g,第二分區記錄單詞首字母h-n, 第三分區記錄單詞首字母o-z。
2. 自定義排序WritableComparable
bean對象實現WritableComparable接口重寫compareTo方法,就可以實現排序
@Override
public int compareTo(FlowBean o) {
// 倒序排列,從大到小
return this.sumFlow > o.getSumFlow() ? -1 : 1;
if(this.sumFlow==o.getSumFlow()){
This.downFlow>o.getDownFlow() ? -1 :1
}
}
3.4.5 WritableComparable排序案例
案例一
1. 需求: 根據流量統計的結果再次對總流量進行排序
2. 代碼實現
(1)在FlowBean基礎上增加了比較功能,在原先代碼基礎上增加該方法即可
@Override
public int compareTo(Object o) {
FlowBean f = (FlowBean)o;
return this.sumFlow > f.getSumFlow()? -1 : 1;
}
(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 FlowSortMapper extends Mapper<LongWritable, Text, FlowBean, Text> {
FlowBean flowBean = new FlowBean();
Text k = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1 獲取一行
String line = value.toString();
// 2 切割字段
String[] fields = line.split("\t");
// 3 封裝對象
// 取出手機號碼
String phoneNum = fields[0];
long upFlow = Long.parseLong(fields[1]);
long downFlow = Long.parseLong(fields[2]);
long sumFlow = Long.parseLong(fields[3]);
flowBean.setUpFlow(upFlow);
flowBean.setDownFlow(downFlow);
flowBean.setSumFlow(sumFlow);
k.set(phoneNum);
// 4 寫出
context.write(flowBean, k);
}
}
(3)reducer類
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class FlowSortReducer extends Reducer<FlowBean, Text, Text, FlowBean> {
@Override
protected void reduce(FlowBean key, Iterable<Text> values, Context context)
throws IOException, InterruptedException {
// 1 遍歷所用bean,將其中的上行流量,下行流量分別累加
for (Text value : values) {
context.write(value,key);
}
}
}
(4)Driver類
import java.io.IOException;
import com.bigdata.mapreduce.flow.FlowBean;
import com.bigdata.mapreduce.flow.FlowCountMapper;
import com.bigdata.mapreduce.flow.FlowCountReducer;
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;
public class FlowsumDriver {
public static void main(String[] args) throws IllegalArgumentException, IOException, ClassNotFoundException, InterruptedException {
// 1 獲取配置信息,或者job對象實例
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 6 指定本程序的jar包所在的本地路徑
job.setJarByClass(FlowsumDriver.class);
// 2 指定本業務job要使用的mapper/Reducer業務類
job.setMapperClass(FlowCountMapper.class);
job.setReducerClass(FlowCountReducer.class);
// 3 指定mapper輸出數據的kv類型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
// 4 指定最終輸出的數據的kv類型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
// 5 指定job的輸入原始文件所在目錄
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 7 將job中配置的相關參數,以及job所用的java類所在的jar包, 提交給yarn去運行
boolean result = job.waitForCompletion(true);
}
}
案例二
1. 需求:要求每個省份手機號輸出的文件中按照總流量內部排序。
2. 分析:基於前一個需求,增加自定義分區類即可。
(1)增加自定義分區類
package com.bigdata.mapreduce.sort;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
public class ProvincePartitioner extends Partitioner<FlowBean, Text> {
@Override
public int getPartition(FlowBean key, Text value, int numPartitions) {
// 1 獲取手機號碼前三位
String preNum = value.toString().substring(0, 3);
int partition = 4;
// 2 根據手機號歸屬地設置分區
if ("136".equals(preNum)) {
partition = 0;
}else if ("137".equals(preNum)) {
partition = 1;
}else if ("138".equals(preNum)) {
partition = 2;
}else if ("139".equals(preNum)) {
partition = 3;
}
return partition;
}
}
(2)在驅動類中註冊分區類
// 加載自定義分區類
job.setPartitionerClass(FlowSortPartitioner.class);
// 設置Reducetask個數
job.setNumReduceTasks(5);
3.4.6 Combiner合併
比如:<b,1> <b,1> == <b,2>
-
combiner是MR程序中Mapper和Reducer之外的一種組件。
-
combiner組件的父類就是Reducer。
-
combiner和reducer的區別在於運行的位置:
-
Combiner是在每一個maptask所在的節點運行;
-
Reducer是接收全局所有Mapper的輸出結果;
-
-
combiner的意義就是對每一個maptask的輸出進行局部彙總,以減小網絡傳輸量。
-
combiner能夠應用的前提是不能影響最終的業務邏輯,而且,combiner的輸出kv應該跟reducer的輸入kv類型要對應起來
-
自定義Combiner實現步驟
(1)自定一個combiner繼承Reducer,重寫reduce方法
package com.bigdata.mr.combiner;
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
public class WordcountCombiner extends Reducer<Text, IntWritable, Text, IntWritable>{
@Override
protected void reduce(Text key, Iterable<IntWritable> values,
Context context) throws IOException, InterruptedException {
// 1 彙總
int count = 0;
for(IntWritable v :values){
count += v.get();
}
// 2 寫出
context.write(key, new IntWritable(count));
}
}
(2)在驅動類中指定combiner
// 指定需要使用combiner,以及用哪個類作爲combiner的邏輯
job.setCombinerClass(WordcountCombiner.class);
運行程序,如圖所示
3.5 ReduceTask工作機制
1.設置ReduceTask並行度(個數)
reducetask的並行度同樣影響整個job的執行併發度和執行效率,但與maptask的併發數由切片數決定不同,Reducetask數量的決定是可以直接手動設置:
//默認值是1,手動設置爲4
job.setNumReduceTasks(4);
2.注意
- reducetask=0 ,表示沒有reduce階段,輸出文件個數和map個數一致。
- reducetask默認值就是1,所以輸出文件個數爲一個。
- 如果數據分佈不均勻,就有可能在reduce階段產生數據傾斜
- reducetask數量並不是任意設置,還要考慮業務邏輯需求,有些情況下,需要計算全局彙總結果,就只能有1個reducetask。
- 具體多少個reducetask,需要根據集羣性能而定。
- 如果分區數不是1,但是reducetask爲1,是否執行分區過程。答案是:不執行分區過程。因爲在maptask的源碼中,執行分區的前提是先判斷reduceNum個數是否大於1。不大於1肯定不執行。
4.ReduceTask工作機制
- (1)Copy階段:ReduceTask從各個MapTask上遠程拷貝一片數據,並針對某一片數據,如果其大小超過一定閾值,則寫到磁盤上,否則直接放到內存中。
- (2)Merge階段:在遠程拷貝數據的同時,ReduceTask啓動了兩個後臺線程對內存和磁盤上的文件進行合併,以防止內存使用過多或磁盤上文件過多。
- (3)Sort階段:按照MapReduce語義,用戶編寫reduce()函數輸入數據是按key進行聚集的一組數據。爲了將key相同的數據聚在一起,Hadoop採用了基於排序的策略。由於各個MapTask已經實現對自己的處理結果進行了局部排序,因此,ReduceTask只需對所有數據進行一次歸併排序即可。
- (4)Reduce階段:reduce()函數將計算結果寫到HDFS上。
3.6 MapReduce Join(關聯)
3.6.1 Reduce Join
1. 原理
Map端的主要工作:爲來自不同表(文件)的key/value對打標籤以區別不同來源的記錄。然後用連接字段作爲key,其餘部分和新加的標誌作爲value,最後進行輸出。
Reduce端的主要工作:在reduce端以連接字段作爲key的分組已經完成,我們只需要在每一個分組當中將那些來源於不同文件的記錄(在map階段已經打標誌)分開,最後進行合併就ok了。
3.6.2 Reduce join案例
1. 需求:將商品信息表中數據根據商品pid合併到訂單數據表中。
現在我們通過MapReduce的方式實現,通過關聯條件作爲map輸出的key,將兩個表滿足join條件的數據(包含數據來源於哪一個文件的標識),發往統一個reduce task,在reduce中進行數據的串聯
1. 創建商品和訂單合併後的bean類
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class TableBean implements WritableComparable<TableBean> {
private String order_id; // 訂單id
private String pid; // 商品id
private int account; // 商品數量
private String pname; // 商品名稱
private String flag; // 標記位,標記該bean來自於哪裏,0代表訂單,1代表商品
public TableBean() {
}
public TableBean(String order_id, String pid, int account, String pname, String flag) {
this.order_id = order_id;
this.pid = pid;
this.account = account;
this.pname = pname;
this.flag = flag;
}
public String getOrder_id() {
return order_id;
}
public void setOrder_id(String order_id) {
this.order_id = order_id;
}
public String getPid() {
return pid;
}
public void setPid(String pid) {
this.pid = pid;
}
public int getAccount() {
return account;
}
public void setAccount(int account) {
this.account = account;
}
public String getPname() {
return pname;
}
public void setPname(String pname) {
this.pname = pname;
}
public String getFlag() {
return flag;
}
public void setFlag(String flag) {
this.flag = flag;
}
@Override
public String toString() {
return order_id + '\t' + pname + '\t'+ account ;
}
// 序列化:寫字符串使用writeUTF
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(order_id);
out.writeUTF(pid);
out.writeInt(account);
out.writeUTF(pname);
out.writeUTF(flag);
}
//反序列化
@Override
public void readFields(DataInput in) throws IOException {
order_id = in.readUTF();
pid = in.readUTF();
account = in.readInt();
pname = in.readUTF();
flag = in.readUTF();
}
@Override
public int compareTo(TableBean o) {
return 1;
}
}
2. Mapper類
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
public class TableMapper extends Mapper<LongWritable, Text, Text, TableBean> {
TableBean bean = new TableBean();
Text k = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1.獲取文件輸入類型
FileSplit split = (FileSplit) context.getInputSplit();
String name = split.getPath().getName();
//2.獲取輸入數據
String line = value.toString();
//3.不同文件分別處理
if (name.startsWith("order")) { //訂單表處理
// 切割
String[] strings = line.split("\t");
//封裝對象
bean.setOrder_id(strings[0]);
bean.setPid(strings[1]);
bean.setAccount(Integer.parseInt(strings[2]));
bean.setPname("");
bean.setFlag("0");
k.set(strings[1]);
} else { //商品表處理
// 切割
String[] strings = line.split("\t");
//封裝
bean.setPid(strings[0]);
bean.setPname(strings[1]);
bean.setFlag("1");
bean.setAccount(0);
bean.setOrder_id("");
k.set(strings[0]);
}
context.write(k, bean);
}
}
3. Reducer類
import javafx.scene.control.Tab;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import org.codehaus.jackson.map.util.BeanUtil;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
public class TableReducer extends Reducer<Text, TableBean, TableBean, NullWritable> {
@Override
protected void reduce(Text key, Iterable<TableBean> values, Context context) throws IOException, InterruptedException {
//1.準備存儲訂單的集合
ArrayList<TableBean> ordersBean = new ArrayList<>();
//2.準備bean對象
TableBean pbBean = new TableBean();
for (TableBean value : values) {
if ("0".equals(value.getFlag())) {
// 拷貝傳遞過來的每條訂單數據到集合中
TableBean orderBean = new TableBean();
try {
BeanUtils.copyProperties(orderBean,value);
} catch (Exception e) {
e.printStackTrace();
}
ordersBean.add(orderBean);
} else {
try {
BeanUtils.copyProperties(pbBean,value);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
//3.表的拼接
for (TableBean tableBean : ordersBean) {
tableBean.setPname(pbBean.getPname());
//4.將數據寫出去
context.write(tableBean, NullWritable.get());
}
}
}
4. Driver類
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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 TableDriver {
public static void main(String[] args) throws Exception {
// 1 獲取配置信息,或者job對象實例
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 2 指定本程序的jar包所在的本地路徑
job.setJarByClass(TableDriver.class);
// 3 指定本業務job要使用的mapper/Reducer業務類
job.setMapperClass(TableMapper.class);
job.setReducerClass(TableReducer.class);
// 4 指定mapper輸出數據的kv類型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(TableBean.class);
// 5 指定最終輸出的數據的kv類型
job.setOutputKeyClass(TableBean.class);
job.setOutputValueClass(NullWritable.class);
// 6 指定job的輸入原始文件所在目錄
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 7 將job中配置的相關參數,以及job所用的java類所在的jar包, 提交給yarn去運行
boolean result = job.waitForCompletion(true);
}
}
5. 運行查看結果
1001 小米 1 1001 小米 1 1002 華爲 2 1002 華爲 2 1003 格力 3 1003 格力 3 |
缺點:這種方式中,合併操作是在reduce階段完成,reduce端的壓力太大,map節點的運算的負載則很低,資源率不高,並且在reduce階段內極易產生數據傾斜(某個reduce接收到的數據量特別大)
我們可以採用在map端進行數據合併來解決這個問題
3.6.3 Map join
適用於的場景:一張表特別大,而另一張表很小
在這種情況下,在map端緩存多張表,提前處理業務邏輯,這樣增加map端業務,減少reduce端數據的壓力,儘可能的減少數據傾斜
1. 具體實現
可以採用distributedcache,將小表提前加載到緩存集合中,mapper在setup的時候將小表架子啊到本地內存,在本地對地自己讀到的大表數據進程業務邏輯合併並輸出結果,可以大大提高合併操作二點併發度,加快處理速度
對於這個案例,在map端進行join操作後就不需要reduce階段了,直接設置reducetask 的數量爲0即可
1. Driver類: 現在驅動類中添加緩存文件(第6)
import java.net.URI;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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 DistributedCacheDriver {
public static void main(String[] args) throws Exception {
// 1 獲取job信息
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 2 設置加載jar包路徑
job.setJarByClass(DistributedCacheDriver.class);
// 3 關聯map
job.setMapperClass(DistributedCacheMapper.class);
// 4 設置最終輸出數據類型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// 5 設置輸入輸出路徑
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 6 加載緩存數據
job.addCacheFile(new URI("file:///e:/inputcache/pd.txt"));
// 7 map端join的邏輯不需要reduce階段,設置reducetask數量爲0
job.setNumReduceTasks(0);
// 8 提交
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
2. Mapper類:讀取緩存集合中的數據
import com.bigdata.mapreduce.table.TableBean;
import org.apache.commons.io.input.BOMInputStream;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.hdfs.util.EnumCounters;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
public class MapJoinMapper extends Mapper<LongWritable,Text,TableBean,NullWritable>{
//用來存儲讀取到的緩存數據
Map<String,String> pdMap = new HashMap<>();
// 讀取緩存文件,轉換成我們方便使用的數據結構備用
@Override
protected void setup(Context context) throws IOException, InterruptedException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new BOMInputStream(new FileInputStream("pd.txt"))));
String line;
while(StringUtils.isNotEmpty(line=bufferedReader.readLine())) {
String[] fields = line.split("\t");
pdMap.put(fields[0],fields[1]);
}
bufferedReader.close();
}
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
TableBean tableBean = new TableBean();
String line = value.toString();
String[] fields = line.split("\t");
String order_id = fields[0];
String pid = fields[1];
int account = Integer.parseInt(fields[2]);
tableBean.setOrder_id(order_id);
tableBean.setPid(pid);
tableBean.setAccount(account);
tableBean.setPname(pdMap.get(pid)); // 直接從緩存中取出商品名稱
tableBean.setFlag("");
context.write(tableBean,NullWritable.get());
}
}