前言
在上一篇文章中我們已經瞭解了HDFS的讀寫流程,HA高可用,聯邦和Sequence Files方案,簡單回顧一下HDFS的寫流程吧
Client呼叫Distributed FileSystem的create方法,這個過程是遠端呼叫了NameNode的create方法,此時NameNode就會做四件事情
-
檢查自己是否正常執行
-
判斷要寫進HDFS的檔案是否存在
-
檢查client是否具有建立許可權
-
對此次操作進行日誌記錄(edits log)
此時create方法會返回一個OutputStream,這個流還需啊喲和NameNode進行互動,呼叫NameNode的addBlock()方法,以得知這個block需要寫在哪些資料節點上。
開始寫資料時先寫在一個chuck上,附帶著一個4位元組的checkSum,總共516位元組,然後再把這些chuck寫在一個更大的結構package中,在package被多個chuck寫滿之後,把package放到一個叫做data queue的佇列中,之後所做的事情有兩個
-
data queue中的package往資料節點DataNode上傳輸,傳輸的順序按照NameNode的addBlock()方法返回的列表依次傳輸
-
往DataNode上傳輸的同時也往確認佇列ack queue上傳輸
-
針對DataNode中傳輸完成的資料做一個checkSum,並與原本打包前的checkSum做一個比較
-
校驗成功,就從確認佇列ack queue中刪除該package,否則該package重新置入data queue重傳
完成後通過心跳機制NameNode就可以得知副本已經建立完成,再呼叫addBlock()方法寫之後的檔案。
異常的情況就不再重新說明了,可以直接跳到第二篇進行檢視
一、MapReduce程式設計模型
MapReduce是採用一種分而治之思想設計出來的分散式計算框架
在計算複雜或者計算量大的任務,單臺伺服器無法勝任時,可將其切分成一個個小的任務,小任務分別在不同的伺服器上並行執行,最終再彙總每個小任務的結果即可
MapReduce由兩個階段組成,切分成小任務的Map階段和彙總小任務的Reduce階段,如下圖,需要注意,三個小任務是可以並行執行的
1.1 Map階段
map()函式的輸入時鍵值對,輸出的是一系列鍵值對,輸出的結果時寫入本地磁碟的
1.2 Reduce階段
reduce()函式的輸入時鍵值對(即map()函式的輸出),輸出是一系列鍵值對,最終寫入HDFS
大體邏輯在下面的圖非常清晰明瞭了,shuffle的過程之後再說明
二、MapReduce程式設計示例
永遠都逃不過的詞頻統計,統計一篇文章中,各個單詞出現的次數
2.1 原理圖分析
從左到右,有一個檔案,HDFS對它進行了分塊儲存,且每一個塊我們也可以視為是一個分片(split),然後它提供一個kv對(0,Dear Bear River)過來,key為什麼是0呢?那這裡的0其實是偏移量,這個偏移量是會隨著檔案中的資料位元組大小進行變化的。在當前例子中暫時我們還用不上,我們需要做的只是把作為value的Dear Bear River做一個拆分,然後進行統計,統計完成後開始讀第二行的Dear Car,同樣輸出即可。
之後這個檔案分成的3個塊都統計好之後,再按照同一個單詞彙聚到同一個節點進行統計的方式,得出結果即可
需要注意的問題
1.我們可以看到在上圖存在著 4 個單詞 4 個 reduce task,但是這個reduce task的個數是由開發人員自己決定的,只是一個SetReduceNum(4)的問題
2.為什麼reduce可以得知究竟有多少個單詞,提到shuffle時我們再說。
3.細心的你應該會發現shufflling過後的那些(Dear,1)有4個,可是key不應該只能存在一個麼,這也是shuffle的時候要說的
2.2 mapper程式碼
public class WordMap extends Mapper<LongWritable, Text, Text, IntWritable> {
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String[] words = value.toString().split(" ");
for (String word : words) {
// 每個單詞出現1次,作為中間結果輸出
context.write(new Text(word), new IntWritable(1));
}
}
}
複製程式碼
這裡的LongWritable對應Java裡面的Long型別,Text對應String型別,因為分散式框架中資料從一個節點到另一個節點時會存在序列化和反序列化的問題,所以Hadoop自身提供了一些帶有序列化功能的類供我們使用,也就是平時我們看到的鍵值對是(Long,String),在這裡就變成了(LongWritable,Text)而已。
之後就是覆寫map()方法,實現單詞分割,之後把每個單詞作為key,以(word,1)這種狀態輸出出去。
想要檢視這些API方法的話,可以去hadoop官網檢視,這裡我用的還是2.7.3,看過上一篇的同學應該也是知道了
這裡有兩個Mapper是因為第一個Mapper是老的Mapper,現在已經使用新的了。點選Method之後就可以看到剛剛使用的map()方法了
2.3 Reducer程式碼
public class WordReduce extends Reducer<Text, IntWritable, Text, IntWritable> {
/*
key: hello
value: List(1, 1, ...)
*/
protected void reduce(Text key, Iterable<IntWritable> values,
Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable count : values) {
sum = sum + count.get();
}
context.write(key, new IntWritable(sum));// 輸出最終結果
};
}
複製程式碼
有了上一個2.2的基礎,這個程式碼就不再展開說明了,就是把value進行累加,然後得出一個sum,key還是指單詞,之後以(word,sum)這種狀態輸出出去。
補充:當value中的列表非常大時,會選擇提高叢集記憶體或者設定一些讀句子時候的限制(自定義InputFormat類,MapReduce預設的是TextInputFormat)把資料大小給減少。
2.4 程式執行的main()方法
這裡的main方法基本每一個都是直接拷貝過來然後填填set方法的引數直接用的
public class WordMain {
public static void main(String[] args) throws IOException,
ClassNotFoundException, InterruptedException {
if (args.length != 2 || args == null) {
System.out.println("please input Path!");
System.exit(0);
}
Configuration configuration = new Configuration();
// 生成一個job例項
Job job = Job.getInstance(configuration, WordMain.class.getSimpleName());
// 打jar包之後,找程式入口用
job.setJarByClass(WordMain.class);
// 通過job設定輸入/輸出格式
// MR的預設輸入格式就是TextInputFormat,所以註釋掉也沒問題
//job.setInputFormatClass(TextInputFormat.class);
//job.setOutputFormatClass(TextOutputFormat.class);
// 設定輸入/輸出路徑
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 設定處理Map/Reduce階段的類
job.setMapperClass(WordMap.class);
job.setReducerClass(WordReduce.class);
//如果map、reduce的輸出的kv對型別一致,直接設定reduce的輸出的kv對就行;如果不一樣,需要分別設定map, reduce的輸出的kv型別
//job.setMapOutputKeyClass(.class)
// 設定最終輸出key/value的型別m
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 提交作業
job.waitForCompletion(true);
}
}
複製程式碼
執行的方式可以本地執行,可以叢集執行,可以maven打包執行也可以,執行結果可以通過yarn檢視,因為考慮到大家可能沒時間去搭建一個叢集玩這裡就不貼圖了,後面找機會分享一下簡答的3節點的叢集搭建。
2.5 combiner
map端的本地聚合,無論執行多少次combiner操作,都不會影響最終的結果
注意:不是所有MapReduce程式都適合使用,比如求average
WordCountMap與WordCountReduce程式碼不變
WordCountMain中,增加job.setCombinerClass(WordCountReduce.class);
複製程式碼
鍵值對一開始的時候是第一張圖的樣子,現在我們剛經過Mapping時會存在大量的鍵值對,它們會通過網路傳到對應的Reducing那,如果都是按照(word,1)的格式傳輸過去,傳輸的資料量就變得非常巨大,所以這時候最好的方案是先在本地對某一個單詞先做一個彙總,也就是combine操作,如圖,兩個(dear,1)變成了一個(Dear,2),2個(Car,1)變成了(Car,2)等···
2.6 shuffle過程
map task 輸出的時候會輸出到一個環形緩衝區中,每一個環形緩衝區是100M大小,隨著資料的不斷讀寫,讓環形緩衝區的記憶體達到80%,這時候會造成溢位寫磁碟,把這些檔案寫到磁碟中,而這個寫到磁碟的操作會經歷3個過程
首先是分割槽,預設情況下是利用key來進行分割槽操作,MapReduce框架專門提供了一個HashPartitioner用於進行分割槽操作
環形緩衝區的kv對在落入磁碟前都需要去呼叫一下getPartition()方法,此時我們可以看到,它使用了一個比較巧妙的方法:先是計算了一下這個key的hashcode,再模上一個reduce的個數,這種時候我們看上面的圖,reduce的個數是4,那我們一個數字去模4,結果只會是4個,也就是0,1,2,3,所以這四個結果就會對應不同的緩衝區
剩下的就是reduce task來進行拉取資料,剛開始時會放到記憶體當中,放不下的時候也會溢位寫到磁碟
當然如果一開始的時候有進行setCombine操作的話就會變成(Dear,4),在圖中因為我們是舉例說明,實際情況下每個分割槽都有很多不同的單詞,在reduce操作時就會進行合併操作,即相同的key放在一起,然後按照字母順序排序。
combine,merge,和最後的reduce task,這些功能都一樣,只不過作用的階段不同,方便提升效能。只要達到業務要求就行,有時候一個map就能解決需求,有時候需要map和reduce兩個階段。
之後每一個reduce task的結果都會寫到HDFS的一個檔案裡。當map task完成後,後面說yarn的時候會有一個appMaster,做一個輪詢的確認,確認完成後再通知reduce task從本地磁碟拉取,有比較多的具體知識需要後續跟進時才會在最後形成一個比較清晰的概念,這也是非常正常的。
2.7 二次排序
MapReduce中根據key進行分割槽排序和分組,如果現在需要自定義key型別,並自定義key的排序規則,如何實現(結合程式碼講解)
public class Person implements WritableComparable<Person> {
private String name;
private int age;
private int salary;
public Person() {
}
public Person(String name, int age, int salary) {
//super();
this.name = name;
this.age = age;
this.salary = salary;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
@Override
public String toString() {
return this.salary + " " + this.age + " " + this.name;
}
//先比較salary,高的排序在前;若相同,age小的在前
public int compareTo(Person o) {
int compareResult1= this.salary - o.salary;
if(compareResult1 != 0) {
return -compareResult1;
} else {
return this.age - o.age;
}
}
//序列化,將NewKey轉化成使用流傳送的二進位制
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeUTF(name);
dataOutput.writeInt(age);
dataOutput.writeInt(salary);
}
//使用in讀欄位的順序,要與write方法中寫的順序保持一致
public void readFields(DataInput dataInput) throws IOException {
//read string
this.name = dataInput.readUTF();
this.age = dataInput.readInt();
this.salary = dataInput.readInt();
}
}
複製程式碼
講解內容··
2.8 資料傾斜
資料傾斜是資料中的常見情況。資料中不可避免地會出現離群值(outlier),並導致資料傾斜。這些離群值會顯著地拖慢MapReduce的執行。常見的資料傾斜有以下幾類:
- 資料頻率傾斜——某一個區域的資料量要遠遠大於其他區域。(reduce傾斜)
- 資料大小傾斜——部分記錄的大小遠遠大於平均值。(map傾斜)
在map端和reduce端都有可能發生資料傾斜。在map端的資料傾斜會讓多樣化的資料集的處理效率更低。在reduce端的資料傾斜常常來源於MapReduce的預設分割槽器。
資料傾斜會導致map和reduce的任務執行時間大為延長,也會讓需要快取資料集的操作消耗更多的記憶體資源。
2.8.1 如何診斷是否存在資料傾斜
- 關注由map的輸出資料中的資料頻率傾斜的問題。
- 如何診斷map輸出中哪些鍵存在資料傾斜?
-
在reduce方法中加入記錄map輸出鍵的詳細情況的功能
-
在發現了傾斜資料的存在之後,就很有必要診斷造成資料傾斜的那些鍵。有一個簡便方法就是在程式碼裡實現追蹤每個鍵的最大值。為了減少追蹤量,可以設定資料量閥值,只追蹤那些資料量大於閥值的鍵,並輸出到日誌中。
-
8.2 減緩Reduce端資料傾斜
-
Reduce資料傾斜一般是指map的輸出資料中存在資料頻率傾斜的狀況,也就是部分輸出鍵的資料量遠遠大於其它的輸出鍵
-
如何減小reduce端資料傾斜的效能損失?
① 抽樣和範圍分割槽
Hadoop預設的分割槽器是基於map輸出鍵的雜湊值分割槽。這僅在資料分佈比較均勻時比較好。在有資料傾斜時就很有問題。
使用分割槽器需要首先了解資料的特性。**TotalOrderPartitioner**中,可以通過對原始資料進行抽樣得到的結果集來預設分割槽邊界值。TotalOrderPartitioner中的範圍分割槽器可以通過預設的分割槽邊界值進行分割槽。因此它也可以很好地用在矯正資料中的部分鍵的資料傾斜問題。
複製程式碼
② 自定義分割槽
另一個抽樣和範圍分割槽的替代方案是基於輸出鍵的背景知識進行自定義分割槽。例如,如果map輸出鍵的單詞來源於一本書。其中大部分必然是省略詞(stopword)。那麼就可以將自定義分割槽將這部分省略詞傳送給固定的一部分reduce例項。而將其他的都傳送給剩餘的reduce例項。
複製程式碼
③ Combine
使用Combine可以大量地減小資料頻率傾斜和資料大小傾斜。在可能的情況下,combine的目的就是聚合並精簡資料。在技術48種介紹了combine。
複製程式碼
④ Map端連線和半連線
如果連線的資料集太大而不能在map端的連線中使用。那麼可以考慮第4章和第7章中介紹的超大資料集的連線優化方案。
複製程式碼
⑤ 資料大小傾斜的自定義策略
在map端或reduce端的資料大小傾斜都會對快取造成較大的影響,乃至導致OutOfMemoryError異常。處理這種情況並不容易。可以參考以下方法。
- 設定mapred.linerecordreader.maxlength來限制RecordReader讀取的最大長度。RecordReader在TextInputFormat和KeyValueTextInputFormat類中使用。預設長度沒有上限。
- 通過org.apache.hadoop.contrib.utils.join設定快取的資料集的記錄數上限。在reduce中預設的快取記錄數上限是100條。
- 考慮使用有損資料結構壓縮資料,如Bloom過濾器。
複製程式碼
finally
MR的沒有分篇,篇幅很大,希望大家能夠耐心看完。
根據順序下一篇是Yarn,走完大資料的這個流程。