MapReduce基本原理(詳解!)

一、概念理解

MapReduce是面向大數據並行處理的計算模型、框架和平臺,它隱含了以下三層含義:
1)MapReduce是一個基於集羣的高性能並行計算平臺。它允許用市場上普通的商用服務器構成一個包含數十、數百至數千個節點的分佈和並行計算集羣。
2)MapReduce是一個並行計算與運行軟件框架。它提供了一個龐大但設計精良的並行計算軟件框架,能自動完成計算任務的並行化處理,自動劃分計算數據和計算任務,在集羣節點上自動分配和執行任務以及收集計算結果,將數據分佈存儲、數據通信、容錯處理等並行計算涉及到的很多系統底層的複雜細節交由系統負責處理,大大減少了軟件開發人員的負擔。
3)MapReduce是一個並行程序設計模型與方法。它藉助於函數式程序設計語言Lisp的設計思想,提供了一種簡便的並行程序設計方法,用Map和Reduce兩個函數編程實現基本的並行計算任務,提供了抽象的操作和並行編程接口,以簡單方便地完成大規模數據的編程和計算處理。

總結:MapReduce是一個基於集羣的計算平臺,是一個簡化分佈式編程的計算框架,是一個將分佈式計算抽象爲Map和Reduce兩個階段的編程模型。

MapReduce核心思想:分而治之

二、MapReduce計算模型

我們知道MapReduce計算模型主要由三個階段構成:Map、shuffle、Reduce。

Map是映射,負責數據的過濾分法,將原始數據轉化爲鍵值對;Reduce是合併,將具有相同key值的value進行處理後再輸出新的鍵值對作爲最終結果。爲了讓Reduce可以並行處理Map的結果,必須對Map的輸出進行一定的排序與分割,然後再交給對應的Reduce,而這個將Map輸出進行進一步整理並交給Reduce的過程就是Shuffle。整個MR的大致過程如下:

mapreduce運行原理圖解
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
Map和Reduce操作需要我們自己定義相應Map類和Reduce類,以完成我們所需要的化簡、合併操作,而shuffle則是系統自動幫我們實現的,瞭解shuffle的具體流程能幫助我們編寫出更加高效的Mapreduce程序。

Shuffle過程包含在Map和Reduce兩端,即Map shuffle和Reduce shuffle

三、Map shuffle

在Map端的shuffle過程是對Map的結果進行分區、排序、分割,然後將屬於同一劃分(分區)的輸出合併在一起並寫在磁盤上,最終得到一個分區有序的文件,分區有序的含義是map輸出的鍵值對按分區進行排列,具有相同partition值的鍵值對存儲在一起,每個分區裏面的鍵值對又按key值進行升序排列(默認),其流程大致如下:

在這裏插入圖片描述

(1)Partition

對於map輸出的每一個鍵值對,系統都會給定一個partition,partition值默認是通過計算key的hash值後對Reduce task的數量取模獲得。如果一個鍵值對的partition值爲1,意味着這個鍵值對會交給第一個Reducer處理。

我們知道每一個Reduce的輸出都是有序的,但是將所有Reduce的輸出合併到一起卻並非是全局有序的,如果要做到全局有序,我們該怎麼做呢?最簡單的方式,只設置一個Reduce task,但是這樣完全發揮不出集羣的優勢,而且能應對的數據量也很受限。最佳的方式是自己定義一個Partitioner,用輸入數據的最大值除以系統Reduce task數量的商作爲分割邊界,也就是說分割數據的邊界爲此商的1倍、2倍至numPartitions-1倍,這樣就能保證執行partition後的數據是整體有序的。

另一種需要我們自己定義一個Partitioner的情況是各個Reduce task處理的鍵值對數量極不平衡。對於某些數據集,由於很多不同的key的hash值都一樣,導致這些鍵值對都被分給同一個Reducer處理,而其他的Reducer處理的鍵值對很少,從而拖延整個任務的進度。當然,編寫自己的Partitioner必須要保證具有相同key值的鍵值對分發到同一個Reducer。

(2)Collector

Map的輸出結果是由collector處理的,每個Map任務不斷地將鍵值對輸出到在內存中構造的一個環形數據結構中。使用環形數據結構是爲了更有效地使用內存空間,在內存中放置儘可能多的數據。

這個數據結構其實就是個字節數組,叫Kvbuffer,名如其義,但是這裏面不光放置了數據,還放置了一些索引數據,給放置索引數據的區域起了一個Kvmeta的別名,在Kvbuffer的一塊區域上穿了一個IntBuffer(字節序採用的是平臺自身的字節序)的馬甲。數據區域和索引數據區域在Kvbuffer中是相鄰不重疊的兩個區域,用一個分界點來劃分兩者,分界點不是亙古不變的,而是每次Spill之後都會更新一次。初始的分界點是0,數據的存儲方向是向上增長,索引數據的存儲方向是向下增長,如圖所示:
在這裏插入圖片描述
Kvbuffer的存放指針bufindex是一直悶着頭地向上增長,比如bufindex初始值爲0,一個Int型的key寫完之後,bufindex增長爲4,一個Int型的value寫完之後,bufindex增長爲8。

索引是對在kvbuffer中的鍵值對的索引,是個四元組,包括:value的起始位置、key的起始位置、partition值、value的長度,佔用四個Int長度,Kvmeta的存放指針Kvindex每次都是向下跳四個“格子”,然後再向上一個格子一個格子地填充四元組的數據。比如Kvindex初始位置是-4,當第一個鍵值對寫完之後,(Kvindex+0)的位置存放value的起始位置、(Kvindex+1)的位置存放key的起始位置、(Kvindex+2)的位置存放partition的值、(Kvindex+3)的位置存放value的長度,然後Kvindex跳到-8位置,等第二個鍵值對和索引寫完之後,Kvindex跳到-12位置。

Kvbuffer的大小可以通過io.sort.mb設置,默認大小爲100M。但不管怎麼設置,Kvbuffer的容量都是有限的,鍵值對和索引不斷地增加,加着加着,Kvbuffer總有不夠用的那天,那怎麼辦?把數據從內存刷到磁盤上再接着往內存寫數據,把Kvbuffer中的數據刷到磁盤上的過程就叫Spill,多麼明瞭的叫法,內存中的數據滿了就自動地spill到具有更大空間的磁盤。

關於Spill觸發的條件,也就是Kvbuffer用到什麼程度開始Spill,還是要講究一下的。如果把Kvbuffer用得死死得,一點縫都不剩的時候再開始Spill,那Map任務就需要等Spill完成騰出空間之後才能繼續寫數據;如果Kvbuffer只是滿到一定程度,比如80%的時候就開始Spill,那在Spill的同時,Map任務還能繼續寫數據,如果Spill夠快,Map可能都不需要爲空閒空間而發愁。兩利相衡取其大,一般選擇後者。Spill的門限可以通過io.sort.spill.percent,默認是0.8。

Spill這個重要的過程是由Spill線程承擔,Spill線程從Map任務接到“命令”之後就開始正式幹活,乾的活叫SortAndSpill,原來不僅僅是Spill,在Spill之前還有個頗具爭議性的Sort。

(3)Sort

當Spill觸發後,SortAndSpill先把Kvbuffer中的數據按照partition值和key兩個關鍵字升序排序,移動的只是索引數據,排序結果是Kvmeta中數據按照partition爲單位聚集在一起,同一partition內的按照key有序。

(4)Spill

Spill線程爲這次Spill過程創建一個磁盤文件:從所有的本地目錄中輪訓查找能存儲這麼大空間的目錄,找到之後在其中創建一個類似於“spill12.out”的文件。Spill線程根據排過序的Kvmeta挨個partition的把數據吐到這個文件中,一個partition對應的數據吐完之後順序地吐下個partition,直到把所有的partition遍歷完。一個partition在文件中對應的數據也叫段(segment)。在這個過程中如果用戶配置了combiner類,那麼在寫之前會先調用combineAndSpill(),對結果進行進一步合併後再寫出。Combiner會優化MapReduce的中間結果,所以它在整個模型中會多次使用。那哪些場景才能使用Combiner呢?Combiner的輸出是Reducer的輸入,Combiner絕不能改變最終的計算結果。所以從我的想法來看,Combiner只應該用於那種Reduce的輸入key/value與輸出key/value類型完全一致,且不影響最終結果的場景。比如累加,最大值等。Combiner的使用一定得慎重,如果用好,它對job執行效率有幫助,反之會影響reduce的最終結果。

所有的partition對應的數據都放在這個文件裏,雖然是順序存放的,但是怎麼直接知道某個partition在這個文件中存放的起始位置呢?強大的索引又出場了。有一個三元組記錄某個partition對應的數據在這個文件中的索引:起始位置、原始數據長度、壓縮之後的數據長度,一個partition對應一個三元組。然後把這些索引信息存放在內存中,如果內存中放不下了,後續的索引信息就需要寫到磁盤文件中了:從所有的本地目錄中輪訓查找能存儲這麼大空間的目錄,找到之後在其中創建一個類似於“spill12.out.index”的文件,文件中不光存儲了索引數據,還存儲了crc32的校驗數據。spill12.out.index不一定在磁盤上創建,如果內存(默認1M空間)中能放得下就放在內存中,即使在磁盤上創建了,和spill12.out文件也不一定在同一個目錄下。每一次Spill過程就會最少生成一個out文件,有時還會生成index文件,Spill的次數也烙印在文件名中。索引文件和數據文件的對應關係如下圖所示:
在這裏插入圖片描述
在Spill線程如火如荼的進行SortAndSpill工作的同時,Map任務不會因此而停歇,而是一無既往地進行着數據輸出。Map還是把數據寫到kvbuffer中,那問題就來了:只顧着悶頭按照bufindex指針向上增長,kvmeta只顧着按照Kvindex向下增長,是保持指針起始位置不變繼續跑呢,還是另謀它路?如果保持指針起始位置不變,很快bufindex和Kvindex就碰頭了,碰頭之後再重新開始或者移動內存都比較麻煩,不可取。Map取kvbuffer中剩餘空間的中間位置,用這個位置設置爲新的分界點,bufindex指針移動到這個分界點,Kvindex移動到這個分界點的-16位置,然後兩者就可以和諧地按照自己既定的軌跡放置數據了,當Spill完成,空間騰出之後,不需要做任何改動繼續前進。分界點的轉換如下圖所示:
在這裏插入圖片描述
Map任務總要把輸出的數據寫到磁盤上,即使輸出數據量很小在內存中全部能裝得下,在最後也會把數據刷到磁盤上。

(5)Merge

Map任務如果輸出數據量很大,可能會進行好幾次Spill,out文件和Index文件會產生很多,分佈在不同的磁盤上。最後把這些文件進行合併的merge過程閃亮登場。

Merge過程怎麼知道產生的Spill文件都在哪了呢?從所有的本地目錄上掃描得到產生的Spill文件,然後把路徑存儲在一個數組裏。Merge過程又怎麼知道Spill的索引信息呢?沒錯,也是從所有的本地目錄上掃描得到Index文件,然後把索引信息存儲在一個列表裏。到這裏,又遇到了一個值得納悶的地方。在之前Spill過程中的時候爲什麼不直接把這些信息存儲在內存中呢,何必又多了這步掃描的操作?特別是Spill的索引數據,之前當內存超限之後就把數據寫到磁盤,現在又要從磁盤把這些數據讀出來,還是需要裝到更多的內存中。之所以多此一舉,是因爲這時kvbuffer這個內存大戶已經不再使用可以回收,有內存空間來裝這些數據了。(對於內存空間較大的土豪來說,用內存來省卻這兩個io步驟還是值得考慮的。)
在這裏插入圖片描述
然後爲merge過程創建一個叫file.out的文件和一個叫file.out.Index的文件用來存儲最終的輸出和索引,一個partition一個partition的進行合併輸出。對於某個partition來說,從索引列表中查詢這個partition對應的所有索引信息,每個對應一個段插入到段列表中。也就是這個partition對應一個段列表,記錄所有的Spill文件中對應的這個partition那段數據的文件名、起始位置、長度等等。

然後對這個partition對應的所有的segment進行合併,目標是合併成一個segment。當這個partition對應很多個segment時,會分批地進行合併:先從segment列表中把第一批取出來,以key爲關鍵字放置成最小堆,然後從最小堆中每次取出最小的輸出到一個臨時文件中,這樣就把這一批段合併成一個臨時的段,把它加回到segment列表中;再從segment列表中把第二批取出來合併輸出到一個臨時segment,把其加入到列表中;這樣往復執行,直到剩下的段是一批,輸出到最終的文件中。最終的索引數據仍然輸出到Index文件中。

四、Reduce shuffle

在Reduce端,shuffle主要分爲複製Map輸出、排序合併兩個階段。

(1)Copy

Reduce任務通過HTTP向各個Map任務拖取它所需要的數據。Map任務成功完成後,會通知父TaskTracker狀態已經更新,TaskTracker進而通知JobTracker(這些通知在心跳機制中進行)。所以,對於指定作業來說,JobTracker能記錄Map輸出和TaskTracker的映射關係。Reduce會定期向JobTracker獲取Map的輸出位置,一旦拿到輸出位置,Reduce任務就會從此輸出對應的TaskTracker上覆制輸出到本地,而不會等到所有的Map任務結束。

(2)Merge Sort

Copy過來的數據會先放入內存緩衝區中,如果內存緩衝區中能放得下這次數據的話就直接把數據寫到內存中,即內存到內存merge。Reduce要向每個Map去拖取數據,在內存中每個Map對應一塊數據,當內存緩存區中存儲的Map數據佔用空間達到一定程度的時候,開始啓動內存中merge,把內存中的數據merge輸出到磁盤上一個文件中,即內存到磁盤merge。在將buffer中多個map輸出合併寫入磁盤之前,如果設置了Combiner,則會化簡壓縮合並的map輸出。Reduce的內存緩衝區可通過mapred.job.shuffle.input.buffer.percent配置,默認是JVM的heap size的70%。內存到磁盤merge的啓動門限可以通過mapred.job.shuffle.merge.percent配置,默認是66%。

當屬於該reducer的map輸出全部拷貝完成,則會在reducer上生成多個文件(如果拖取的所有map數據總量都沒有內存緩衝區,則數據就只存在於內存中),這時開始執行合併操作,即磁盤到磁盤merge,Map的輸出數據已經是有序的,Merge進行一次合併排序,所謂Reduce端的sort過程就是這個合併的過程。一般Reduce是一邊copy一邊sort,即copy和sort兩個階段是重疊而不是完全分開的。最終Reduce shuffle過程會輸出一個整體有序的數據塊。

參考文章:


以上內容僅供參考學習,如有侵權請聯繫我刪除!
如果這篇文章對您有幫助,左下角的大拇指就是對博主最大的鼓勵。
您的鼓勵就是博主最大的動力!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章