一、MapReduce2工作機制
1.1、MapReduce2的架構圖
1.2、MapReduce2運作步驟
說在前頭的話,上圖中有一個ResoureceManager,這是一個資源調度器,說白了就是管資源的,在MapReduce1時,所有的事情都是交給Jobtracker來做,包括資源調度,在MapReduce2的框架圖當中,進行了明確的分工,減少了各個組件的耦合性,當然優化不止這一點。在hadoop當中的三大組件之一yarn,其實就是一個資源調度框架,并且默認是集成的,只需要稍加配置就好,目前企業對hadoop的使用,基本都在用yarn做資源調度。
客戶端的配置信息mapreduce.framework.name為yarn時,客戶端會啟動YarnRunner(yarn的客戶端程序),并將mapreduce作業提交給yarn平臺處理。
以下是對MapReduce2框架圖的解釋:
1.2.1、提交job
Client調用job.waitForCompletion方法,向整個集群提交MapReduce作業(第1步)。新的作業ID(應用ID)由ResourceManager分配(第2步)。作業的client核實作業的輸出,計算輸入的split,將作業的資源(包括jar包、配置文件和split信息等)拷貝給HDFS(第3步)。最后,通過調用ResourceManager的submitApplication()來提交作業(第4步)。
1.2.2、作業初始化
當ResourceManager收到submitApplication()的請求時, 就將該請求發給調度器(scheduler), 調度器分配container, 然后ResourceManager在該container內啟動ApplicationMaster進程, 由NodeManager監控(第5a和5b步)。
MapReduce作業的ApplicationMaster是一個主類為MRAppMaster的Java應用。其通過創造一些bookkeeping對象來監控作業的進度, 得到任務的進度和完成報告(第6步)。MRAppMaster通過分布式文件系統得到由客戶端計算好的輸入split(第7步)。MRAppMaster為每個輸入split創建一個map任務, 根據mapreduce.job.reduces創建reduce任務對象。
1.2.3、任務分配
如果作業很小,ApplicationMaster會選擇在其自己的JVM中運行任務。如果不是小作業,那么ApplicationMaster向ResourceManager請求container來運行所有的map和reduce任務(第8步)。這些請求是通過心跳來傳輸的,包括每個map任務的數據位置,比如存放輸入split的主機名和機架(rack)。調度器利用這些信息來調度任務,盡量將任務分配給存儲數據的節點,或者退而分配給和存放輸入split的節點相同機架的節點。
1.2.4、任務運行
當一個任務由ResourceManager的調度分配給一個container后, ApplicationMaster通過聯系NodeManager來啟動container(第9a步和9b步)。任務由一個主類為YarnChild的Java應用執行。在運行任務之前首先本地化任務需要的資源,比如作業配置、JAR文件和分布式緩存的所有文件(第10步)。最后,運行map或reduce任務(第11步)。
1.2.5、進度和狀態更新
YARN中的任務將其進度和狀態(包括counter)返回給ApplicationMaster,客戶端每秒輪詢(通過mapreduce.client.progressmonitor.pollinterval設置)向ApplicationMaster請求進度更新,展示給用戶。
1.2.6、作業完成
除了向ApplicationMaster請求作業進度外,客戶端每5分鐘都會通過調用waitForCompletion()來檢查作業是否完成。時間間隔可以通過mapreduce.client.completion. pollinterval來設置。作業完成之后, ApplicationMaster和container會清理工作狀態, OutputCommiter的作業清理方法也會被調用。作業的信息會被作業歷史服務器存儲以備之后用戶核查。最后,ApplicationMaster向ResourceManager注銷并關閉自己。
二、mapTask的工作機制
先放出整個mapreduce的流程圖:
2.1、切片
在FileInputFormat中,計算切片大小的邏輯:Math.max(minSize, Math.min(maxSize, blockSize)),minSize的默認值是1,而maxSize的默認值是long類型的最大值,即可得切片的默認大小是blockSize(128M),maxSize參數如果調得比blocksize小,則會讓切片變小,而且就等于配置的這個參數的值,minSize參數調的比blockSize大,則可以讓切片變得比blocksize還大
,hadoop為每個分片構建一個map任務,可以并行處理多個分片上的數據,整個數據的處理過程將得到很好的負載均衡,因為一臺性能較強的計算機能處理更多的數據分片。分片也不能切得太小,否則多個map和reduce間數據的傳輸時間,管理分片,構建多個map任務的時間將決定整個作業的執行時間(導致大部分時間都不在計算上)。如果文件大小小于128M,則該文件不會被切片,不管文件多小都會是一個單獨的切片,交給一個maptask處理.如果有大量的小文件,將導致產生大量的maptask,大大降低集群性能。
大量小文件的優化策略:
(1) 在數據處理的前端就將小文件整合成大文件,再上傳到hdfs上,即避免了hdfs不適合存儲小文件的缺點,又避免了后期使用mapreduce處理大量小文件的問題。(最提倡的做法)
(2)小文件已經存在hdfs上了,可以使用另一種inputformat來做切片(CombineFileInputFormat),它的切片邏輯和FileInputFormat(默認)不同,它可以將多個小文件在邏輯上規劃到一個切片上,交給一個maptask處理。
2.2、環形緩存區
經過map函數的邏輯處理后的數據輸出之后,會通過OutPutCollector收集器將數據收集到環形緩存區保存。環形緩存區的大小默認為100M,當保存的數據達到80%時,就將緩存區的數據溢出到磁盤上保存。
2.3、溢出
環形緩存區的數據達到其容量的80%時就會溢出到磁盤上進行保存,在此過程中,程序會對數據進行分區(默認HashPartition)和排序(默認根據key進行快排),緩存區不斷溢出的數據形成多個小文件。其實溢出的過程當中,還會涉及排序,分區,combiner,這些后面詳細介紹。
2.4、合并
溢出的多個小文件各個區合并在一起(0區和0區合并成一個0區),形成大文件,通過歸并排序保證區內的數據有序。
2.5、輸出到運行mapTask的本地磁盤
形成的文件會存放在運行mapTask的本地磁盤上,所以map的結果并不會持久化到HDFS上去,只有reduceTask的結果會持久化到HDFS上。
三、reduceTask的工作機制
3.1、拉取數據階段
簡單地拉取數據。Reduce進程啟動一些數據copy線程(Fetcher),通過HTTP方式請求Map Task獲取屬于自己的文件。之前在mapTask中,從環形緩沖區溢出進文件時,會進行分區,分區決定了這個分區的數據應該被那個reduceTask處理。
3.2、Merge階段
這里的merge如map端的merge動作,只是數組中存放的是不同map端copy來的數值。Copy過來的數據會先放入內存緩沖區中,這里的緩沖區大小要比map端的更為靈活。merge有三種形式:內存到內存;內存到磁盤;磁盤到磁盤。默認情況下第一種形式不啟用。當內存中的數據量到達一定閾值,就啟動內存到磁盤的 merge。與map端類似,這也是溢寫的過程,這個過程中如果你設置有Combiner,也是會啟用的,然后在磁盤中生成了眾多的溢寫文件。第二種merge方式一直在運行,直到沒有map端的數據時才結束,然后啟動第三種磁盤到磁盤的merge方式生成最終的文件
3.3、reduce函數處理階段
把分散的數據合并成一個大的數據后,還會再對合并后的數據排序。對排序后的鍵值對調用reduce方法,鍵相等的鍵值對調用一次reduce方法,每次調用會產生零個或者多個鍵值對,最后把這些輸出的鍵值對寫入到HDFS文件中。
四、shuffle的工作機制
Shuffle 是 MapReduce 的核心,它分布在 MapReduce 的Map 階段和Reduce階段。一般把從 Map產生輸出開始到Reduce 取得數據 作為輸入之前的過程稱作Shuffle。如mapreduce的流程圖中的步驟2-》步驟7都屬于shuffle階段,即map任務和reduce任務之間的數據流稱為shuffle(混洗),而過程5最能體現出混洗這一概念。一般情況下,一個reduce任務的輸入數據來自與多個map任務,多個reduce任務的情況下就會出現如過程5所示的,每個reduce任務從map的輸出數據中獲取屬于自己的那個分區的數據。
(1)Collect 階段:將 Map Task的結果輸出到默認大小為 100M的環形緩沖區,保存的是 key/value,Partition 分區信息等。
(2)Spill 階段:當內存中的數據量達到一定的閥值的時候,就會將數據寫入本地磁盤,在將數據寫入磁盤之前需要對數據進行一次排序的操作,如果配置了Combiner,還會將有相同分區號和 key 的數據進行排序。
(3)Merge 階段:把所有溢出的臨時文件進行一次合并操作,以確保一個Map Task最終只產生一個中間數據文件。
(4)Copy 階段: Reduce Task 啟動 Fetcher 線程到已經完成 Map Task 的節點上復制一份屬于自己的數據,這些數據默認會保存在內存的緩沖區中,當內存的緩沖區達到一定的閥值的時候,就會將數據寫到磁盤之上。
(5)Merge 階段:在 Reduce Task 遠程復制數據的同時,會在后臺開啟兩個線程對內存到本地的數據文件進行合并操作。
(6)Sort 階段:在對數據進行合并的同時,會進行排序操作,由于 Map Task階段已經對數據進行了局部的排序,Reduce Task 只需保證 Copy 的數據的最終整體有效性即可。Shuffle 中的緩沖區大小會影響到MapReduce 程序的執行效率,原則上說,緩沖區越大,磁盤 io 的次數越少,執行速度就越快。緩沖區的大小可以通過參數調整, 參數:io.sort.mb 默認 100M。
五、mapreduce代碼實現
package com.ibeifeng.mapreduce;
import java.io.IOException;
import java.util.StringTokenizer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
public class MyWordCountMapReduce extends Configured implements Tool{
//定義map處理類模板
public static class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable>{
private Text outKey = new Text();
private IntWritable outValues = new IntWritable(1);
@Override
public void map(LongWritable key, Text values, Context context)
throws IOException, InterruptedException {
//書寫map處理業務邏輯代碼
//1.將單詞進行分割
String str = values.toString();
// String[] split = str.split(" ");
// for(String s : split) {
// outKey.set(s);
// context.write(outKey, outValues);
// }
StringTokenizer stringtokenizer = new StringTokenizer(str);
while((stringtokenizer.hasMoreTokens())) {
outKey.set(stringtokenizer.nextToken());
context.write(outKey, outValues);
System.out.println("<"+outKey+outValues+">");
}
}
}
//定義combiner處理模塊
public static class Combiner extends Reducer<Text, IntWritable, Text, IntWritable>{
private IntWritable outValues = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values,
Context context) throws IOException, InterruptedException {
//書寫reduce處理業務邏輯代碼
//1.由于需要累加所以需要定義一個變量做記錄
int sum = 0;
for(IntWritable i : values) {
sum+=i.get();
}
outValues.set(sum);
context.write(key, outValues);
System.out.println("<"+key+outValues+">");
}
}
//定義reduce處理類模板
public static class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable>{
private IntWritable outValues = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values,
Context context) throws IOException, InterruptedException {
//書寫reduce處理業務邏輯代碼
//1.由于需要累加所以需要定義一個變量做記錄
int sum = 0;
for(IntWritable i : values) {
sum+=i.get();
}
outValues.set(sum);
context.write(key, outValues);
System.out.println("<"+key+outValues+">");
}
}
//配置Driver模塊
public int run(String[] args) {
//1.獲取配置配置文件對象
Configuration configuration = new Configuration();
//2.創建給mapreduce處理的任務
Job job = null;
try {
job = Job.getInstance(configuration,this.getClass().getSimpleName());
} catch (IOException e) {
e.printStackTrace();
}
try {
//3.創建輸入路徑
Path source_path = new Path(args[0]);
FileInputFormat.addInputPath(job, source_path);
//4.創建輸出路徑
Path des_path = new Path(args[1]);
FileOutputFormat.setOutputPath(job, des_path);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
//設置讓任務打包jar運行
job.setJarByClass(MyWordCountMapReduce.class);
//5.設置map
job.setMapperClass(WordCountMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//================shuffle========================
//1.分區,主要用于自定義分區使用
// job.setPartitionerClass(cls);
//2.排序,主要用于自定義排序使用
// job.setSortComparatorClass(cls);
//3.分組,主要用于自定義分組使用
// job.setGroupingComparatorClass(cls);
//4.可選項,設置combiner,相當于map過程的reduce處理,優化選項,可節省map和reduce之間IO傳輸量
job.setCombinerClass(Combiner.class);
//================shuffle========================
//6.設置reduce
job.setReducerClass(WordCountReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//7.提交job到yarn組件上
boolean isSuccess = false;
try {
isSuccess = job.waitForCompletion(true);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return isSuccess?0:1;
}
//書寫主函數
public static void main(String[] args) {
Configuration configuration = new Configuration();
//1.書寫輸入和輸出路徑
String[] args1 = new String[] {
"hdfs://hadoop-senior01.ibeifeng.com:8020/user/beifeng/wordcount/input",
"hdfs://hadoop-senior01.ibeifeng.com:8020/user/beifeng/wordcount/output"
};
//2.設置系統以什么用戶執行job任務
System.setProperty("HADOOP_USER_NAME", "beifeng");
//3.運行job任務
int status = 0;
try {
status = ToolRunner.run(configuration, new MyWordCountMapReduce(), args1);
} catch (Exception e) {
e.printStackTrace();
}
// int status = new MyWordCountMapReduce().run(args1);
//4.退出系統
System.exit(status);
}
}
六、mapreduce幾個重要概念理解
6.1、partition分區
數據從環形緩存區溢出到文件的過程中會根據用戶自定義的partition函數進行分區,如果用戶沒有自定義該函數,程序會用默認的partitioner通過哈希函數來分區,hash partition 的好處是比較彈性,跟數據類型無關,實現簡單,只需要設置reducetask的個數。分區的目的是將整個大數據塊分成多個數據塊,通過多個reducetask處理后,輸出多個文件。通常在輸出數據需要有所區分的情況下使用自定義分區,如在上述的流量統計的案例里,如果需要最后的輸出數據再根據手機號碼的省份分成幾個文件來存儲,則需要自定義partition函數,并在驅動程序里設置reduce任務數等于分區數(job.setNumReduceTasks(5);)和指明自己定義的partition(job.setPartitionerClass(ProvincePartitioner.class))。在需要獲取統一的輸出結果的情況下,不需要自定義partition也不用設置reducetask的數量(默認1個)。
自定義的分區函數有時會導致數據傾斜的問題,即有的分區數據量極大,各個分區數據量不均勻,這會導致整個作業時間取決于處理時間最長的那個reduce,應盡量避免這種情況發生。
6.2、combiner(map端的reduce)
集群的帶寬限制了mapreduce作業的數量,因此應該盡量避免map和reduce任務之間的數據傳輸。hadoop允許用戶對map的輸出數據進行處理,用戶可自定義combiner函數(如同map函數和reduce函數一般),其邏輯一般和reduce函數一樣,combiner的輸入是map的輸出,combiner的輸出作為reduce的輸入,很多情況下可以直接將reduce函數作為conbiner函數來使用。combiner屬于優化方案,所以無法確定combiner函數會調用多少次,可以在環形緩存區溢出文件時調用combiner函數,也可以在溢出的小文件合并成大文件時調用combiner。但要保證不管調用幾次combiner函數都不會影響最終的結果,所以不是所有處理邏輯都可以使用combiner組件,有些邏輯如果在使用了combiner函數后會改變最后rerduce的輸出結果(如求幾個數的平均值,就不能先用combiner求一次各個map輸出結果的平均值,再求這些平均值的平均值,這將導致結果錯誤)。combiner的意義就是對每一個maptask的輸出進行局部匯總,以減小網絡傳輸量。(原先傳給reduce的數據是(a,(1,1,1,1,1,1...)),使用combiner后傳給reduce的數據變為(a,(4,2,3,5...)))
6.3、分組
分組和上面提到的partition(分區)不同,分組發生在reduce端,reduce的輸入數據,會根據key是否相等而分為一組,如果key相等的,則這些key所對應的value值會作為一個迭代器對象傳給reduce函數。以單詞統計為例,reduce輸入的數據就如:第一組:(a,(1,3,5,3,1))第二組:(b,(6,2,3,1,5))。上述例子也可以看出在map端是執行過combiner函數的,否則reduce獲得的輸入數據是:第一組:(a,(1,1,1,1,1,...))第二組:(b,(1,1,1,1,1...))。對每一組數據調用一次reduce函數。
6.4、排序
在整個mapreduce過程中涉及到多處對數據的排序,環形緩存區溢出的文件,溢出的小文件合并成大文件,reduce端多個分區數據合并成一個大的分區數據等都需要排序,而這排序規則是根據key的compareTo方法來的。map端輸出的數據的順序不一定是reduce端輸入數據的順序,因為在這兩者之間數據經過了排序,但reduce端輸出到文件上顯示的順序就是reduce函數的寫出順序。在沒有reduce函數的情況下,顯示地在驅動函數里將reduce的數量設置為0(設置為0后表示沒有reduce階段,也就沒有shuffle階段,也就不會對數據進行各種排序分組),否則雖然沒有reduce邏輯,但是還是會有shuffle階段,map端處理完數據后將數據保存在文件上的順序也不是map函數的寫出順序,而是經過shuffle分組排序過后的順序