思考問題
MapReduce總結
MapReduce
MapReduce的定義
MapReduce是一種編程模型, 用于大規模數據集(大于1TB)的并行運算。它將分布式磁盤讀寫的問題進行抽象,并轉換為對一個數據集(由鍵/值對組成)的計算,該計算由 map 和 reduce 兩部分組成。MapReduce編程模型流行的三個技術方面的原因:
- MapReduce采用無共享大規模集群系統。集群系統具有良好的性價比和可伸縮性。
- MapReduce模型簡單、易于理解、易于使用。它不僅用于處理大規模數據,而且能夠將很多繁瑣的細節隱藏起來(比如,自動化并行、負載均衡和災備管理等),極大地簡化了程序員的開發工作,而且大量數據處理問題,包括很多機器學習和數據挖掘算法,都可以使用MapReduce實現。
- 雖然基本的MapReduce模型只提供一個過程性的編程接口,但在海量數據環境、需要保證可伸縮性的前提下,通過使用合適的查詢優化和索引技術,MapReduce仍能夠提供很好的數據處理性能。
-
MapReduce和關系型數據庫的比較
MapReduce和關系型數據庫之間的另一個區別在于他們所操作的數據集的結構化的程度,這在第一篇的文章已經討論過。
需要強調的是,MapReduce輸入的鍵和值并不是數據固有的屬性,而是由分析數據的人員來選擇的。 MapReduce的特點:
- 軟件框架
- 并行處理
- 可靠且容錯
- 大規模集群
- 海量數據集
- Map 和 Reduce
Hadoop框架使用Mapper將數據處理成一個個的<key,value>鍵值對,在網絡節點間對其進行整理(shuffle),然后使用Reducer處理數據并進行最終輸出。
-
Map 映射的意思,簡單來說就是把一個輸入映射為一組(多個)全新的數據,而不去改變原始的數據。
Mapper負責的是分。把復雜的任務分解為若干個“簡單的任務”來處理。“簡單的任務”包含三層含義:- 數據或計算的規模相對原任務要大大縮小
- 就近計算原則,任務會分配到存放著所需數據的節點上進行計算
- 這些小任務可以并行計算彼此間幾乎沒有依賴關系
-
Reduce 化簡的意思,就是把通過Map得到的一組數據經過某些方法(化簡)歸一成想要的輸出值。
所以說,MapReduce的思想就是:分而治之
MapReduce的工作機制
下面,我們來剖析MapReduce作業的運行機制
- 作業的提交:客戶端通過JobClient.runJob()來提交一個作業到jobtracker,JobClient程序邏輯如下:
- 向Jobtracker請求一個新的job id (JobTracker.getNewJobId());
- 檢查作業的輸出說明,如已存在拋錯誤給客戶端;計算作業的輸入分片;
- 將運行作業所需要的資源(包括作業jar文件,配置文件和計算所得的輸入分片)復制到jobtracker的文件系統中以jobid命名的目錄下。作業jar副本較多(mapred.submit.replication = 10);
- 告知jobtracker作業準備執行 (submit job)。
簡單來說呢,就是
- 通過JobClient提交,與JobTracker通信得到一個jar的存儲路徑和JobId
- 檢查輸入輸出的路徑
- 計算分片的信息
- 將作于所需要的資源(jar,配置文件,計算所得的輸入分片)賦值到以作業ID命名的HDFS上
- 告知JobTracker作業準備執行
- 作業的初始化
- job tracker接收到對其submitJob()方法的調用后,將其放入內部隊列,交由job scheduler進行調度,并對其進行初始化,包括創建一個正在運行作業的對象(封裝任務和記錄信息)。
- 為了創建任務運行列表,job scheduler首先從共享文件系統中獲取JobClient已計算好的輸入分片信息,然后為每個分片創建一個map任務;創建的reduce任務數量由JobConf的mapred.reduce.task屬性決定,schedule創建相應數量的reduce任務。任務此時被指定ID。
簡單來說呢,就是
- JobTracker配置好Job需要的資源后,JobTracker就會初始化作業
- 初始化主要做的是將Job放入一個內部的隊列,讓配置好的作業調度器能調度到這個作業,作業調度器會初始化這個job。
- 初始化就是創建一個正在運行的job對象(封裝任務和記錄信息),以便JobTracker跟蹤job的狀態和進程。
- 初始化完畢后,作業調度器會獲取輸入分片信息(input split),每個分片創建一個map任務。
- 任務的分配
- jobtacker應該先選擇哪個job來運行?這個由job scheduler來決定
- jobtracker如何選擇tasktracker來運行選中作業的任務呢?看下面
- 每個tasktracker定期發送心跳給jobtracker,告知自己還活著,是否可以接受新的任務。
jobtracker以此來決定將任務分配給誰(仍然使用心跳的返回值與tasktracker通信)。
每個tasktracker會有固定數量的任務槽來處理map和reduce(比如2,表示tasktracker可以同時運行兩個map和reduce),由機器內核的數量和內存大小來決定。job tracker會先將tasktracker的map槽填滿,然后分配reduce任務到tasktracker。 - jobtracker選擇哪個tasktracker來運行map任務需要考慮網絡位置,它會選擇一個離輸入分片較近的tasktracker,優先級是數據本地化(data-local)–>機架本地化(rack-local)。
- 對于reduce任務,沒有什么標準來選擇哪個tasktracker,因為無法考慮數據的本地化。map的輸出始終是需要經過整理(切分排序合并)后通過網絡傳輸到reduce的,可能多個map的輸出會切分出一部分送給一個reduce,所以reduce任務沒有必要選擇和map相同或最近的機器上。
- 任務的執行
- tasktracker分配到一個任務后,首先從HDFS中把作業的jar文件復制到tasktracker所在的本地文件系統(jar本地化用來啟動JVM)。同時將應用程序所需要的全部文件從分布式緩存復制到本地磁盤。
- 接下來tasktracker為任務新建一個本地工作目錄,并把jar文件的內容解壓到這個文件夾下。
- tasktracker新建一個taskRunner實例來運行該任務。
TaskRunner啟動一個新的JVM來運行每個任務,以便客戶的map/reduce不會影響tasktracker守護進程。但在不同任務之間重用JVM還是可能的。
子進程通過umbilical接口與父進程進行通信。任務的子進程每隔幾秒便告知父進程的進度,直到任務完成。
- 進度和狀態的更新
- 一個作業和每個任務都有一個狀態信息,包括:作業或任務的運行狀態(running, successful, failed),map和reduce的進度,計數器值,狀態消息或描述。
- task會定期向tasktracker匯報執行情況,tasktracker會定期收集所在集群上的所有task信息,并想JobTracker匯報.JobTracker會根據所有tasktracker匯報上來的信息進行匯總。
- 這些信息通過一定的時間間隔由child JVM –> task tracker –> job tracker匯聚。job tracker將產生一個表明所有運行作業及其任務狀態的全局視圖。你可以通過Web UI查看。同時JobClient通過每秒查詢jobtracker來獲得最新狀態。
- 作業的完成
- JobTracker是在接收到最后一個任務完成后,才將任務標記為"成功"。并將數據結果寫入到HDFS上。
- 作業的失敗
- JobTracker失敗:JobTracker失敗這是最為嚴重的一種任務失敗,失敗機制--它是一個單節點故障,因此,作業注定失敗。(hadoop2.0解決了)。
- tasktracker失敗:tasktracker失敗崩潰了會停止向jobt發送心跳信息,并且JobTracker會將tasktracker從等待的任務池中移除,將該任務轉移到其他的地方執行.obTracker會將tasktracker加入到黑名單。
- task失敗:map或reduce運行失敗,會向tasktracker拋出異常,任務掛起.
MapReduce工作涉及到的4個對象
- 客戶端(client):編寫mapreduce程序,配置作業,提交作業,這就是程序員完成的工作。
- JobTracker:初始化作業,分配作業,與TaskTracker通信,協調整個作業的執行。
- TaskTracker:保持與JobTracker的通信,在分配的數據片段上執行Map或Reduce任務,TaskTracker和JobTracker的不同有個很重要的方面,就是在執行任務時候TaskTracker可以有n多個,JobTracker則只會有一個(JobTracker只能有一個就和hdfs里namenode一樣存在單點故障,我會在后面的mapreduce的相關問題里講到這個問題的)。
- HDFS:保存作業的數據、配置信息等等,最后的結果也是保存在hdfs上面
<small>jobtracker的單點故障:
jobtracker和hdfs的namenode一樣也存在單點故障,
單點故障一直是hadoop被人詬病的大問題,
為什么hadoop的做的文件系統和mapreduce計算框架都是高容錯的,但是最重要的管理節點的故障機制卻如此不好,我認為主要是namenode和jobtracker在實際運行中都是在內存操作,而做到內存的容錯就比較復雜了,只有當內存數據被持久化后容錯才好做,namenode和jobtracker都可以備份自己持久化的文件,但是這個持久化都會有延遲,因此真的出故障,任然不能整體恢復,另外hadoop框架里包含zookeeper框架,zookeeper可以結合jobtracker,用幾臺機器同時部署jobtracker,保證一臺出故障,有一臺馬上能補充上,不過這種方式也沒法恢復正在跑的mapreduce任務。</small>
MapRedece作業的處理流程
按照時間順序包括:
- 輸入分片(input split)
- map階段
- combiner階段
- shuffle階段和
- reduce階段。
- 輸入分片(input split):
在進行map計算之前,mapreduce會根據輸入文件計算輸入分片(input split),每個輸入分片(input split)針對一個map任務
輸入分片(input split)存儲的并非數據本身,而是一個分片長度和一個記錄數據的位置的數組,輸入分片(input split)往往和hdfs的block(塊)關系很密切
<small>假如我們設定hdfs的塊的大小是64mb,如果我們輸入有三個文件,大小分別是3mb、65mb和127mb,那么mapreduce會把3mb文件分為一個輸入分片(input split),65mb則是兩個輸入分片(input split)而127mb也是兩個輸入分片(input split)
即我們如果在map計算前做輸入分片調整,例如合并小文件,那么就會有5個map任務將執行,而且每個map執行的數據大小不均,這個也是mapreduce優化計算的一個關鍵點。
</small> - Map階段:
程序員編寫好的map函數了,因此map函數效率相對好控制,而且一般map操作都是本地化操作也就是在數據存儲節點上進行。 - Combiner階段:
combiner階段是程序員可以選擇的,combiner其實也是一種reduce操作。
Combiner是一個本地化的reduce操作,它是map運算的后續操作,主要是在map計算出中間文件前做一個簡單的合并重復key值的操作。
<small>例如我們對文件里的單詞頻率做統計,map計算時候如果碰到一個hadoop的單詞就會記錄為1,但是這篇文章里hadoop可能會出現n多次,那么map輸出文件冗余就會很多,因此在reduce計算前對相同的key做一個合并操作,那么文件會變小,這樣就提高了寬帶的傳輸效率,畢竟hadoop計算力寬帶資源往往是計算的瓶頸也是最為寶貴的資源,但是combiner操作是有風險的,使用它的原則是combiner的輸入不會影響到reduce計算的最終輸入,
例如:如果計算只是求總數,最大值,最小值可以使用combiner,但是做平均值計算使用combiner的話,最終的reduce計算結果就會出錯。</small> - shuffle階段:
將map的輸出作為reduce的輸入的過程就是shuffle了。 - reduce階段:
和map函數一樣也是程序員編寫的,最終結果是存儲在hdfs上的。
Combiner深入理解
在上述過程中,我們看到至少兩個性能瓶頸:
(1)如果我們有10億個數據,Mapper會生成10億個鍵值對在網絡間進行傳輸,但如果我們只是對數據求最大值,那么很明顯的Mapper只需要輸出它所知道的最大值即可。這樣做不僅可以減輕網絡壓力,同樣也可以大幅度提高程序效率。
總結:網絡帶寬嚴重被占降低程序效率;
(2)假設使用美國專利數據集中的國家一項來闡述數據傾斜這個定義,這樣的數據遠遠不是一致性的或者說平衡分布的,由于大多數專利的國家都屬于美國,這樣不僅Mapper中的鍵值對、中間階段(shuffle)的鍵值對等等,大多數的鍵值對最終會聚集于一個單一的Reducer之上,計算任務分配不夠均衡,從而大大降低程序的性能。
總結:單一節點承載過重降低程序性能;
在MapReduce編程模型中,在Mapper和Reducer之間有一個非常重要的組件,它解決了上述的性能瓶頸問題,它就是Combiner。
① 與mapper和reducer不同的是,combiner沒有默認的實現,需要顯式的設置在conf中才有作用。
② 并不是所有的job都適用combiner,只有操作滿足結合律的才可設置combiner。
combine操作類似于:opt(opt(1, 2, 3), opt(4, 5, 6))。如果opt為求和、求最大值的話,可以使用,但是如果是求平均值的話,則不適用。
因為每一個map都可能會產生大量的本地輸出,Combiner的作用就是對map端的輸出先做一次合并,以減少在map和reduce節點之間的數據傳輸量,以提高網絡IO性能。是MapReduce的一種優化手段。
Combiner總結:
在實際的Hadoop集群操作中,我們是由多臺主機一起進行MapReduce的,
如果加入規約(Combiner)操作,每一臺主機會在reduce之前進行一次對本機數據的規約,
然后在通過集群進行reduce操作,這樣就會大大節省reduce的時間,
從而加快MapReduce的處理速度
Partitioner理解
MapReduce的使用者通常會指定Reduce任務和Reduce任務輸出文件的數量(R)。
用戶在中間key上使用分區函數來對數據進行分區,之后在輸入到后續任務執行進程。一個默認的分區函數式使用hash方法(比如常見的:hash(key) mod R)進行分區。hash方法能夠產生非常平衡的分區。
默認的分區函數HashPartitioner
Partitioner小結:分區Partitioner主要作用在于以下兩點
- 根據業務需要,產生多個輸出文件
- 多個reduce任務并發運行,提高整體job的運行效率
Shuffle理解
Shuffle是什么
針對多個map任務的輸出按照不同的分區(Partition)通過網絡復制到不同的reduce任務節點上,這個過程就稱作為Shuffle。
Hadoop的shuffle過程就是從map端輸出到reduce端輸入之間的過程,這一段應該是Hadoop中最核心的部分,因為涉及到Hadoop中最珍貴的網絡資源,所以shuffle過程中會有很多可以調節的參數,也有很多策略可以研究
map過程的輸出是寫入本地磁盤而不是HDFS,但是一開始數據并不是直接寫入磁盤而是緩沖在內存中,緩存的好處就是減少磁盤I/O的開銷,提高合并和排序的速度。又因為默認的內存緩沖大小是100M(當然這個是可以配置的),所以在編寫map函數的時候要盡量減少內存的使用,為shuffle過程預留更多的內存,因為該過程是最耗時的過程。
再來詳細梳理下整個流程
- 在map端首先是InputSplit,在InputSplit中含有DataNode中的數據,每一個InputSplit都會分配一個Mapper任務,Mapper任務結束后產生<K2,V2>的輸出,這些輸出先存放在緩存中,每個map有一個環形內存緩沖區,用于存儲任務的輸出。默認大小100MB(io.sort.mb屬性),一旦達到閥值0.8(io.sort.spil l.percent),一個后臺線程就把內容寫到(spill)Linux本地磁盤中的指定目錄(mapred.local.dir)下的新建的一個溢出寫文件。
- 寫磁盤前,要進行partition、sort和combine等操作。通過分區,將不同類型的數據分開處理,之后對不同分區的數據進行排序,如果有Combiner,還要對排序后的數據進行combine。等最后記錄寫完,將全部溢出文件合并為一個分區且排序的文件。
- 最后將磁盤中的數據送到Reduce中,圖中Map輸出有三個分區,有一個分區數據被送到圖示的Reduce任務中,剩下的兩個分區被送到其他Reducer任務中。而圖示的Reducer任務的其他的三個輸入則來自其他節點的Map輸出。
- Copy階段:Reducer通過Http方式得到輸出文件的分區。
reduce端可能從n個map的結果中獲取數據,而這些map的執行速度不盡相同,當其中一個map運行結束時,reduce就會從JobTracker中獲取該信息。map運行結束后TaskTracker會得到消息,進而將消息匯報給JobTracker,reduce定時從JobTracker獲取該信息,reduce端默認有5個數據復制線程從map端復制數據 - Merge階段:如果形成多個磁盤文件會進行合并。
從map端復制來的數據首先寫到reduce端的緩存中,同樣緩存占用到達一定閾值后會將數據寫到磁盤中,同樣會進行partition、combine、排序等過程。如果形成了多個磁盤文件還會進行合并,最后一次合并的結果作為reduce的輸入而不是寫入到磁盤中
3.Reducer的參數:最后將合并后的結果作為輸入傳入Reduce任務中
總結:當Reducer的輸入文件確定后,整個Shuffle操作才最終結束。之后就是Reducer的執行了,最后Reducer會把結果存到HDFS上。
Hadoop的壓縮
Shuffle過程中看到,map端在寫磁盤的時候采用壓縮的方式將map的輸出結果進行壓縮是一個減少網絡開銷很有效的方法
MapReduce排序分組
排序: 在Hadoop默認的排序算法中,只會針對key值進行排序。
自定義排序:
public interface WritableComparable<T> extends Writable, Comparable<T> {
}
自定義類型MyNewKey實現了WritableComparable的接口,
該接口中有一個compareTo()方法,當對key進行比較時會調用該方法,而我們將其改為了我們自己定義的比較規則,從而實現我們想要的效果
分組:在Hadoop中的默認分組規則中,也是基于Key進行的,會將相同key的value放到一個集合中去。
自定義分組:
public interface RawComparator<T> extends Comparator<T> {
public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2);
}
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
}
自定義了一個分組比較器MyGroupingComparator,該類實現了RawComparator接口,而RawComparator接口又實現了Comparator接口,這兩個接口的定義:
Hadoop數據類型
Hadoop MapReduce操作的是鍵值對,但這些鍵值對并不是Integer、String等標準的Java類型。為了讓鍵值對可以在集群上移動,Hadoop提供了一些實現了WritableComparable接口的基本數據類型,以便用這些類型定義的數據可以被序列化進行網絡傳輸、文件存儲與大小比較。
- 值:僅會被簡單的傳遞,必須實現Writable或WritableComparable接口。
- 鍵:在Reduce階段排序時需要進行比較,故只能實現WritableComparable接口。
類 | 描述 |
---|---|
BooleanWritable | 標準布爾變量的封裝 |
ByteWritable | 單字節數的封裝 |
DoubleWritable | 雙字節數的封裝 |
FloatWritable | 浮點數的封裝 |
IntWritable | 整數的封裝 |
LongWritable | Long的封裝 |
NullWritable | 無鍵值時的占位符 |
Text | 使用UTF-8格式的文本封裝 |