純乾貨:通過WourdCount程式樣本:詳細講解MapReduce之Block+Split+Shuffle+Map+Reduce的區別及資料處理流程。
Shuffle過程是MapReduce的核心,集中了MR過程最關鍵的部分。要想瞭解MR,Shuffle是必須要理解的。瞭解Shuffle的過程,更有利於我們在對MapReduce job效能調優的工作有協助,以及進一步加深我們對MR內部機理的瞭解。Shuffle到底是什麼,自己在參考一位大牛兩年前的部落格,關於MR系列的文章中,才知道前輩什麼時候已經開始相應的工作,真實佩服。這裡通過對前輩的概念梳理,加上自己的見解,來儘可能的梳理清楚什麼是Shuffle過程,什麼是block,什麼是split,從此處揭開MR的神秘面紗。
在上篇部落格中簡單給出了Shuffle的概念,稍提了一下split,但沒有談block。在瞭解Shuffle之間我們要先瞭解一下block與split。Shuffle給出的定義是copy,copy一片資料,這裡的一片資料你可以理解成一個split資料。但資料上傳到HDFS中,資料被分塊,被分成一個個的block塊,這就引出了什麼是block,什麼是split,以及split和block的區別是什嗎?在解決block與split之後,就是重中之中,map和reduce過程,這就開始吧。
1、Block塊:
你把檔案上傳到HDFS中,第一步就是資料的劃分,這個是真實物理上的劃分,資料檔案上傳到HDFS後,要把檔案劃分成一塊一塊,每塊的大小可以有hadoop-default.xml裡配置選項進行劃分。這裡預設每塊64MB,一個檔案被分成多個64MB大小的小檔案,最後一個可能於64MB。注意:64MB只是預設,是可以更改的,下面會談到如何更改。
<property> <name>dfs.block.size</name> <value>67108864</value> <description>The default block size for new files.</description></property>
資料的劃分有冗餘,冗餘的概念來自哪兒?為了保證資料的安全,上傳的檔案是被複製成3份,當一份資料宕掉,其餘的可以即刻補上。當然這隻是預設。
<property> <name>dfs.replication</name> <value>3</value> <description> Default block replication.The actual number of replications can be specified when the file is created.The default is used if replication is not specified in create time. </description></property>
2、Split塊:
Hadoop中,有另一種關於資料的劃分。這裡定義了一個InputFormat介面,其中一個方法就是getSplits方法。這裡就談到了split。由於Hadoop版本更新換代很快,不同版本中的split的劃分是由不同的job任務來完成的。早早先的版本split是有JobTracker端彎沉的,後來的版本是由JobClient完成的,JobClient劃分好後,把split.file寫入HDFS中,到時候JobTracker端讀這個檔案,就知道split是怎樣劃分的了。這種資料的劃分其實只是一種邏輯上的劃分,目的是為了讓Map Task更好的擷取資料。
例如:
File1:Block11,Block12,Block13,Block14,Block15File2:Block21,Block22,Block23
如果使用者在程式中指定map tasks的個數,比如說是2,如果不指定maptasks個數預設是1,那麼在FileInputFormat(最常見的InputFormat實現)的getSplits方法中,首先會計算totalSize=8(源碼中定義,注意getSplits這個Function Compute的單位是Block個數,而不是Byte個數,後面有個變數叫bytesremaining表示剩餘的 Block個數,不要根據變數名就認為是度byte的字數),然後會計算goalSize=totalSize/numSplits=4,對於File1,計算一個Split 有多少個Block就是這樣計算的。
long splitSize = computeSplitSize(goalSize, minSize, blockSize);protected long computeSplitSize(long goalSize, long minSize, long blockSize) { return Math.max(minSize, Math.min(goalSize, blockSize));}
這裡minSize是1(說明了1個Split至少包含1個Block,不會出現一個Split包含零點幾個Block的情況),計算得出splitSize=4,所以接下來Split劃分是這樣分的:
Split 1: Block11, Block12, Block13,Block14Split 2: Block15Split 3: Block21, Block22, Block23
那使用者指定的map個數是2,出現了三個split怎麼辦?在JobInProgress裡其實maptasks的個數是根據Splits的長度來指定的,所以使用者指定的map個數只是個參考。可以參看裡的代碼:
JobInProgress: initTasks() try { splits = JobClient.readSplitFile(splitFile); } finally { splitFile.close(); } numMapTasks = splits.length; maps = new TaskInProgress[numMapTasks];
所以問題就很清晰了,如果使用者指定了20個map作業,那麼最後會有8個Split(每個Split一個Block),所以最後實際上就有8個MapTasks,也就是說maptask的個數是由splits的長度決定的。
幾個簡單的結論:
1)一個split大於等於1的整數個Block
2) 一個split不會包含兩個File的Block,不會跨越File邊界
3) split和Block的關係是一對多的關係,預設一對一
4) maptasks的個數最終決定於splits的長度
還有一點要說明:在FileSplit類中,有一項是private String[] hosts;看上去是說明這個FileSplit是放在哪些機器上的,實際上hosts裡只是儲存了一個Block的冗餘機器列表。比如上面例子中的Split 1: Block11, Block12, Block13,Block14,這個FileSplit中的hosts裡最終儲存的是Block11本身和其冗餘所在的機器列表,也就是說 Block12,Block13,Block14這些塊存在的那些機器上沒有在FileSplit中記錄,並包含Block與Split之間的關聯性記錄。
FileSplit中的這個屬性有利於調度作業時候的資料本地性問題(資料本地性:作用很大,更有利於MapReduce效能調優)。如果一個tasktracker前來索取task,jobtracker就會找個task給它,找到一個maptask,得先看這個task的輸入的FileSplit裡hosts是否包含tasktracker所在機器,也就是判斷和該tasktracker同時存在一個機器上的datanode是否擁有FileSplit中某個Block的備份。
簡單的說了一下Block與Split之間的聯絡與區別。其實問題不在這裡,網上又有新的解釋如:MapReduce中如何處理跨行Block和UnputSplit的問題。其實大致就是想說命Block與Split之間的區別,這裡不再說明。
回到今天的重點:Shuffle
3、Shuffle過程:
Shuffle,也稱Copy階段。Reduce Task從各個Map Task上遠程拷貝一片資料,並針對某一片資料,如果其大小超過一定閥值,則寫到磁碟上,否則直接放到記憶體中。其實就是打亂,如在java API Collections.shuffle(List);方法裡的隨機的打亂參數list裡的元素順序。
在官方給的關於Shuffle過程的描述,很模糊,建議大家不要看了。還是看看大牛們是怎麼總結Shuffle過程的吧。關於Shuffle,我們需要清楚Shuffle是怎樣把map task的輸出結果有效地傳送到reduce端的。其實Shuffle描述的就是資料從map task 的輸出到reduce task輸入的這段過程。
在Hadoop這樣的叢集環境中,大部分map task與reduce task的執行是在不同的節點上。當然很多情況下Reduce執行時需要跨節點去拉取其它節點上的map task結果。如果叢集正在啟動並執行job有很多,那麼task的正常執行對叢集內部的網路資源消耗會很嚴重。這種網路消耗是正常的,我們不能限制,能做的 是最大化地減少不必要的消耗。還有在節點內,相比於記憶體,磁碟IO對job完成時間的影響也是可觀的。從最基本的要求來說,我們對Shuffle過程的 期望可以有:
1)完整地從map task端拉取資料到Reduce端。
2)在跨節點拉取資料時,儘可能地減少對頻寬的不必要消耗。
3)減少磁碟IO對task執行的影響。
到這裡時,大家可以去想想,如果是自己來設計這段Shuffle過程,那麼你的設計目標是什麼。能最佳化的地方主要在於減少拉取資料的量及盡量使用記憶體而不是磁碟。
這雷根據博主指定的在Hadoop0.21.0的源碼中的Shuffle過程。以WordCount為例,並假設它有8個map task和3個reduce task。而Shuffle過程橫跨map與reduce兩端。
3.1、Map階段:
是某個map task的運行情況。圖中清楚的指出partition、sort與combiner到底作用在哪個階段。從這個圖,可以清晰的瞭解map資料輸出到map端所有資料准被好的全過程。
整個流程分為四步。簡單可這樣說,每個map task都有一個記憶體緩衝區,儲存著map的輸出結果,當緩衝區快滿的時候需要將緩衝區的資料以一個臨時檔案的方式存放到磁碟,當整個map task 結束後在對磁碟中這個map task 產生的所有臨時檔案合并,產生最終的正式輸出檔案,然後等待reduce task來拉資料。
其實每一步都包含著多個步驟與細節:
1、在map task執行時,它的輸入資料來源於HDFS的block,當然在MapReduce概念中,map task唯讀取split。Split與block的對應關係在上面我們已經說的很明白了。在WordCount例子裡,假設map的輸入資料都是像 “aaa”這樣的字串。
2、在經過mapper的運行後,我們得知mapper的輸出是這樣一個key/value對: key是“aaa”, value是數值1。因為當前map端只做加1的操作,在reduce task裡才去合并結果集。前面我們知道這個job有3個reduce task,到底當前的“aaa”應該交由哪個reduce去做呢,是需要現在決定的。
MapReduce提供Partitioner介面,它的作用就是根據key或value及reduce的數量來決定當前的這對輸出資料最終應該交由哪個 reduce task處理。預設對key hash後再以reduce task數量模數。預設的模數方式只是為了平均reduce的處理能力,如果使用者自己對Partitioner有需求,可以訂製並設定到job上。
在上面的例子中,“aaa”經過Partitioner後返回0,也就是這對值應當交由第一個reducer來處理。接下來,需要將資料寫入記憶體緩衝區中,緩衝區的作用是批量收集map結果,減少磁碟IO的影響。我們的key/value對以及Partition的結果都會被寫入緩衝區。當然寫入之 前,key與value值都會被序列化成位元組數組。 而整個記憶體緩衝區就是一個位元組數組。
3、這個記憶體緩衝區是有大小限制的,預設是100MB。當map task的輸出結果很多時,就可能會撐爆記憶體,所以需要在一定條件下將緩衝區中的資料臨時寫入磁碟,然後重新利用這塊緩衝區。這個從記憶體往磁碟寫資料的過程被稱為Spill,中文可譯為溢寫。這個溢寫是由單獨線程來完成,不影響往緩衝區寫map結果的線程。溢寫線程啟動時不應該阻止map 的結果輸出,所以整個緩衝區有個溢寫的比例spill.percent。這個比例預設是0.8,也就是當緩衝區的資料已經達到閾值(buffer size * spill percent = 100MB * 0.8 = 80MB),溢寫線程啟動,鎖定這80MB的記憶體,執行溢寫過程。Map task的輸出結果還可以往剩下的20MB記憶體中寫,互不影響。
當溢寫線程啟動後,需要對這80MB空間內的key做排序(Sort)。排序是MapReduce模型預設的行為,這裡的排序也是對序列化的位元組做的排序。
在這裡我們可以想想,因為map task的輸出是需要發送到不同的reduce端去,而記憶體緩衝區沒有對將發送到相同reduce端的資料做合并,那麼這種合并應該是體現是磁碟檔案中的。從官方圖上也可以看到寫到磁碟中的溢寫檔案是對不同的reduce端的數值做過合并。所以溢寫過程一個很重要的細節在於,如果有很多個 key/value對需要發送到某個reduce端去,那麼需要將這些key/value值拼接到一塊,減少與partition相關的索引記錄。
在針對每個reduce端而合并資料時,有些資料可能像這樣:“aaa”/1, “aaa”/1。對於Wordcount例子,就是簡單地統計單詞出現的次數,如果在同一個map task的結果中有很多個像“aaa”一樣出現多次的key,我們就應該把它們的值合并到一塊,這個過程叫reduce也叫combine。但 MapReduce的術語中,reduce只指reduce端執行從多個map task取資料做計算的過程。除reduce外,非正式地合并資料只能算做combine了。其實大家知道的,MapReduce中將Combiner等 同於Reducer。
如果client設定過Combiner,那麼現在就是使用Combiner的時候了。將有相同key的key/value對的value加起來,減少溢寫到磁碟的資料量。Combiner會最佳化MapReduce的中間結果,所以它在整個模型中會多次使用。那哪些情境才能使用Combiner呢?從這裡分析,Combiner的輸出是Reducer的輸入,Combiner絕不能改變最終的計算結果。所以從我的想法來看,Combiner只應該用於那種 Reduce的輸入key/value與輸出key/value類型完全一致,且不影響最終結果的情境。比如累加,最大值等。Combiner的使用一定得謹慎,如果用好,它對job執行效率有協助,反之會影響reduce的最終結果。
4、每次溢寫會在磁碟上產生一個溢寫檔案,如果map的輸出結果真的很大,有多次這樣的溢寫發生,磁碟上相應的就會有多個溢寫檔案存在。當map task真正完成時,記憶體緩衝區中的資料也全部溢寫到磁碟中形成一個溢寫檔案。最終磁碟中會至少有一個這樣的溢寫檔案存在(如果map的輸出結果很少,當 map執行完成時,只會產生一個溢寫檔案),因為最終的檔案只有一個,所以需要將這些溢寫檔案歸併到一起,這個過程就叫做Merge。Merge是怎樣的?如前面的例子,“aaa”從某個map task讀取過來時值是5,從另外一個map 讀取時值是8,因為它們有相同的key,所以得merge成group。什麼是group。對於“aaa”就是像這樣的:{“aaa”, [5, 8, 2, …]},數組中的值就是從不同溢寫檔案中讀取出來的,然後再把這些值加起來。請注意,因為merge是將多個溢寫檔案合并到一個檔案,所以可能也有相同的 key存在,在這個過程中如果client設定過Combiner,也會使用Combiner來合并相同的key。
至此,map端的所有工作都已結束,最終產生的這個檔案也存放在TaskTracker夠得著的某個本地目錄內。每個reduce task不斷地通過RPC從JobTracker那裡擷取map task是否完成的資訊,如果reduce task得到通知,獲知某台TaskTracker上的map task執行完成,Shuffle的後半段過程開始啟動。
3.2、Reduce階段:
簡單地說,reduce task在執行之前的工作就是不斷地拉取當前job裡每個map task的最終結果,然後對從不同地方拉取過來的資料不斷地做merge,也最終形成一個檔案作為reduce task的輸入檔案。
如map 端的細節圖,Shuffle在reduce端的過程也能用圖上標明的三點來概括。當前reduce copy資料的前提是它要從JobTracker獲得有哪些map task已執行結束,這段過程不詳述。Reducer在真正運行之前,所有的時間都是在拉取資料,做merge,且不斷重複地在做。如前面的方式一樣,下面我也分段地描述reduce 端的Shuffle細節:
1、Copy過程,簡單地拉取資料。Reduce進程啟動一些資料copy線程(Fetcher),通過HTTP方式請求map task所在的TaskTracker擷取map task的輸出檔案。因為map task早已結束,這些檔案就歸TaskTracker管理在本地磁碟中。
2、Merge階段。這裡的merge如map端的merge動作,只是數組中存放的是不同map端copy來的數值。Copy過來的資料會先放入記憶體緩衝區中,這裡的緩衝區大小要比map端的更為靈活,它基於JVM的heap size設定,因為Shuffle階段Reducer不運行,所以應該把絕大部分的記憶體都給Shuffle用。這裡需要強調的是,merge有三種形 式:1)記憶體到記憶體 2)記憶體到磁碟 3)磁碟到磁碟。預設情況下第一種形式不啟用。當記憶體中的資料量到達一定閾值,就啟動記憶體到磁碟的merge。與map 端類似,這也是溢寫的過程,這個過程中如果你設定有Combiner,也是會啟用的,然後在磁碟中產生了眾多的溢寫檔案。第二種merge方式一直在運行,直到沒有map端的資料時才結束,然後啟動第三種磁碟到磁碟的merge方式產生最終的那個檔案。
3、Reducer的輸入檔案。不斷地merge後,最後會產生一個“最終檔案”。為什麼加引號?因為這個檔案可能存在於磁碟上,也可能存在於記憶體中。對我們 來說,當然希望它存放於記憶體中,直接作為Reducer的輸入,但預設情況下,這個檔案是存放於磁碟中的。至於怎樣才能讓這個檔案出現在記憶體中,後面再說。當Reducer的輸入檔案已定,整個Shuffle才最終結束。然後就是Reducer執行,把結果放到HDFS上。
上面就是整個MapReduce中Shuffle的過程,從檔案上傳到HDFS中開始,檔案分塊Block,Split讀塊,到Map,到Reduce的過程,我們都梳理了一遍。上面的內容,主要參考大牛的部落格,自己手打一遍,包括map/reduce端流程自己再畫一遍。一、是為了自己明白整個流程內部機理,加深理解。二、是表示對兩年前博主就有如此深度的見解尊重。希望此篇部落格,能對後學習者有個指導作用,加深對MapReduce內部機理流程的瞭解。
感謝博主:http://langyu.iteye.com/blog/992916
CopyrightBUAA