開源組件系列(11):批處理引擎MapReduce

目錄

(一)MapReduce設計目標

(二)MapReduce編程思想

(三)MapReduce模塊

(四)MapReduce數據傾斜場景

(一)MapReduce設計目標

        MapReduce誕生於搜索領域,主要解決搜索引擎面臨的海量數據處理擴展性差的問題,很大程度上借鑑了Google開源的論文思想,包括了簡化編程接口、提高系統容錯性等特徵。如果我們總計一下MapReduce的設計目標,主要有以下幾個:

  • 簡化編程接口:傳統的分佈式程序設計非常複雜,用戶需要關注的細節非常多,例如數據分片、傳輸、通信等問題,而MapReduce將以上過程極大的簡化了,抽象成幾個獨立的公共模塊,將底層交給系統實現,用戶只需要關心自己的實現邏輯即可;
  • 良好的擴展性:MapReduce支持通過增加機器的方式實現現行擴展集羣的能力;
  • 極高的容錯性:MapReduce底層基於HDFS,運行依賴於Yarn,因而遇到常見的磁盤損壞、機器宕機、通信失敗等軟硬件故障時,能夠依賴於其他框架實現比較好的容錯性;
  • 不錯的吞吐率:MapReduce通過分佈式並行技術,能夠利用多臺機器的資源,一次性讀取和寫入海量數據。

(二)MapReduce編程思想

        MapReduce模型是針對大規模分佈式處理問題而進行的抽象和總結,核心思想是:【分而治之】,即將一個分佈式計算過程拆解成兩個階段:

  • 第一階段:Map階段。由多個可並行執行的Map Task構成,主要功能是,對前一階段中各任務產生的結果進行規約,並得到最終結果。
  • 第二階段:Reduce階段。由多個可並行執行的Reduce Task構成,主要功能是,對前一階段個任務產生的結果進行規約,並得到最終結果。

 

 

 

Map階段:

        首先mapreduce會根據要運行的大文件來進行split,每個輸入分片(input split)針對一個map任務,輸入分片(input split)存儲的並非數據本身,而是一個分片長度和一個記錄數據位置的數組。輸入分片(input split)往往和HDFS的block(塊)關係很密切,假如我們設定HDFS的塊的大小是64MB,我們運行的大文件是64x10M,mapreduce會分爲10個map任務,每個map任務都存在於它所要計算block(塊)的DataNode上。如果文件大小小於64M,則該文件不會被切片,不管文件多小都會是一個單獨的切片,交給一個maptask處理。如果有大量的小文件,將導致產生大量的maptask,大大降低集羣性能。

        經過map函數的邏輯處理後的數據輸出之後,會通過OutPutCollector收集器將數據收集到環形緩存區保存。環形緩存區的大小默認爲100M,當保存的數據達到80%時,就將緩存區的數據溢出到磁盤上保存。程序會對數據進行分區(默認HashPartition)和排序(默認根據key進行快排)。

        值得注意的是,對於數據分片,如果嚴格按照物理切分,會出現一條記錄分成兩部分的情況,因此第一個Split會讀到該Record結束,第二個Split會從下一個Record開始讀,因此Split並不是嚴格意義上數據塊大小一致的。

        每個Mapper通過map()方法,讀取自己的分片,生成鍵值對<Key, Value的形式>。

 

Shuffle階段:

        MapReduce中的Shuffle更像是洗牌的逆過程,把一組無規則的數據儘量轉換成一組具有一定規則的數據。我們都知道MapReduce計算模型一般包括兩個重要的階段:Map是映射,負責數據的過濾分發;Reduce是規約,負責數據的計算歸併。從Map輸出到Reduce輸入的整個過程可以廣義地稱爲Shuffle。Spill過程包括輸出、排序、溢寫、合併等步驟,如圖所示:

 

 

        先把Kvbuffer中的數據按照partition值和key兩個關鍵字升序排序,移動的只是索引數據,排序結果是Kvmeta中數據按照partition爲單位聚集在一起,同一partition內的按照key有序。Spill線程爲這次Spill過程創建一個磁盤文件:從所有的本地目錄中輪訓查找能存儲這麼大空間的目錄,找到之後在其中創建一個類似於“spill12.out”的文件。Spill線程根據排過序的Kvmeta挨個partition的把數據吐到這個文件中,一個partition對應的數據吐完之後順序地吐下個partition,直到把所有的partition遍歷完。一個partition在文件中對應的數據也叫段(segment)。Map任務如果輸出數據量很大,可能會進行好幾次Spill,out文件和Index文件會產生很多,分佈在不同的磁盤上。最後把這些文件進行合併的merge過程閃亮登場。然後爲merge過程創建一個叫file.out的文件和一個叫file.out.Index的文件用來存儲最終的輸出和索引。

        這裏使用的Merge和Map端使用的Merge過程一樣。Map的輸出數據已經是有序的,Merge進行一次合併排序,所謂Reduce端的sort過程就是這個合併的過程。一般Reduce是一邊copy一邊sort,即copy和sort兩個階段是重疊而不是完全分開的。

 

Reduce階段:

        reduce節點從各個map節點拉取存在磁盤上的數據放到Memory Buffer(內存緩衝區),同理將各個map的數據進行合併並存到磁盤,最終磁盤的數據和緩衝區剩下的20%合併傳給reduce階段。reduce對shuffle階段傳來的數據進行最後的整理合並。

        在Reduce端,輸入可能來自不同的map輸出,不同map任務的完成時間不同,只要有一個任務完成,reduce就會拷貝其輸出,這個過程稱之爲“複製階段”。和map端類似,每個reduce也持有一個內存緩衝區,如果map輸出較小,會寫到內存中,在溢出寫到磁盤,如果map輸出很大,會直接寫臨時文件。後臺會維護一個線程,當臨時文件太多時,會對磁盤文件進行合併,合併的結果文件可能是多個。

 

(三)MapReduce模塊

        按照map/reduce執行流程中各個任務的時間順序詳細敘述map/reduce的各個任務模塊,包括:輸入分片(input split)、map階段、combiner階段、shuffle階段和reduce階段。

 

1:input & split

        input可以是本地文件,也可以是數據庫的表, 提供數據源輸入。

        Split是一個單獨的Map任務需要處理的數據塊,通常一個split就是一個block,這樣做的好處是使得Map任務可以在存儲有當前數據的節點上運行本地的任務,而不需要通過網絡進行跨節點的任務調度。

        set odps.sql.mapper.split.size=512,可以控制block大小,一般默認是256M

 

2:Mapper

        Map是一類將輸入記錄集轉換爲中間格式記錄集的獨立任務,主要是讀取InputSplit的每一個Key,Value對並進行處理.

        maps的數量通常取決於輸入大小,也即輸入文件的block數,對於那種有大量小文件輸入的的作業來說,一個map處理多個文件會更有效率。如果輸入的是打文件,那麼一種提高效率的方式是增加block的大小(比如512M)

 

3:Shuffle

        一般把從map任務輸出到reducer任務輸入之間的map/reduce框架所做的工作叫做shuffle。這部分也是map/reduce框架最重要的部分。

        當Map程序開始產生結果的時候,並不是直接寫到文件的,而是寫到一個內存緩衝區(環形內存緩衝區)。

        每個map任務都有一個內存緩衝區,存儲着map的輸出結果,這個內存緩衝區是有大小限制的,默認是100MB(可以通過屬性參數配置)。當map task的輸出結果很多時,就可能會超過100MB內存的限制,所以需要在一定條件下將緩衝區中的數據臨時寫入磁盤,然後重新利用這塊緩衝區。這個從內存往磁盤寫數據的過程被稱爲“spill”,中文可譯爲溢寫。

        在把map()輸出數據寫入內存緩衝區之前會先進行Partitioner操作。Partitioner用於劃分鍵值空間(key space)。MapReduce提供Partitioner接口,它的作用就是根據key或value及reduce的數量來決定當前的這對輸出數據最終應該交由哪個reduce task處理。默認對key hash後再以reduce task數量取模。

        Partitioner操作得到的分區元數據也會被存儲到內存緩衝區中。當數據達到溢出的條件時,讀取緩存中的數據和分區元數據,然後把屬與同一分區的數據合併到一起。對於每一個分區,都會在內存中根據map輸出的key進行排序(排序是MapReduce模型默認的行爲,這裏的排序也是對序列化的字節做的排序。最後實現溢出的文件內是分區的,且分區內是有序的。

        Combiner最主要的好處在於減少了shuffle過程從map端到reduce端的傳輸數據量。combiner階段是程序員可以選擇的,combiner其實也是一種reduce操作。Combiner是一個本地化的reduce操作,它是map運算的後續操作,主要是在map計算出中間文件前做一個簡單的合併重複key值的操作

        每次spill操作也就是寫入磁盤操作時候就會寫一個溢出文件,也就是說在做map輸出有幾次spill就會產生多少個溢出文件,等map輸出全部做完後,map會合並這些輸出文件生成最終的正式輸出文件,然後等待reduce任務來拉數據。將這些溢寫文件歸併到一起的過程叫做Merge。

 

4:Reduce

        reduce任務在執行之前的工作就是不斷地拉取每個map任務的最終結果,然後對從不同地方拉取過來的數據不斷地做merge,也最終形成一個文件作爲reduce任務的輸入文件。reduce的運行可以分成copy、merge、reduce三個階段。

        由於job的每一個map都會根據reduce(n)數將數據分成map 輸出結果分成n個partition,所以map的中間結果中是有可能包含每一個reduce需要處理的部分數據的。所以,爲了優化reduce的執行時間,hadoop中是等job的第一個map結束後,所有的reduce就開始嘗試從完成的map中下載該reduce對應的partition部分數據,因此map和reduce是交叉進行的。

        這裏的merge如map端的merge動作類似,只是數組中存放的是不同map端copy來的數值。Copy過來的數據會先放入內存緩衝區中,然後當使用內存達到一定量的時候才刷入磁盤。

        當reduce將所有的map上對應自己partition的數據下載完成後,就會開始真正的reduce計算階段。Reducer的輸出是沒有排序的。

(四)MapReduce數據傾斜場景

Map端:

        在Map 讀數據階段,可以通過“ set odps.mapper.split.size=256 ”來調節Map Instance 的個數,提高數據讀人的效率,同時也可以通過“ set odps.mapper.merg e.limit.size=64 ”來控制Map Instance 讀取文件的個數。

        在寫人磁盤之前,線程首先根據Reduce Instance 的個數劃分分區,數據將會根據Key 值Hash 到不同的分區上,一個Reduce Instance 對應一個分區的數據。Map 端也會做部分聚合操作,以減少輸入Reduce 端的數據量。由於數據是根據Hash 分配的,因此也會導致有些Reduce Instance 會分配到大量數據,而有些Reduce Instance 卻分配到很少數據,甚至沒有分配到數據。

        在Map端讀數據時,由於讀人數據的文件大小分佈不均勻,因此會導致有些Map Instance 讀取並且處理的數據特別多,而有些Map Instance 處理的數據特別少,造成Map端長尾。以下兩種情況可能會導致Map端長尾:

        1:上游表文件的大小特別不均勻,並且小文件特別多,導致當前表Map端讀取的數據分佈不均勻,引起長尾。

        2:Map端做聚合時,由於某些Map Instance讀取文件的某個值特別多而引起長尾,主要是指Count Distinct操作。

        解決方案:

        第一種情況導致的Map 端長尾,可通過對上游合併小文件,同時調節本節點的小文件的參數來進行優化。

        第二種情況可以使用“ distribute by rand ()”來打亂數據分佈,使數據儘可能分佈均勻。

        Map 端長尾的根本原因是由於讀人的文件塊的數據分佈不均勻,再加上UDF 函數性能、Join 、聚合操作等,導致讀人數據量大的Map lnstance 耗時較長。在開發過程中如果遇到Map 端長尾的情況,首先考慮如何讓Map Instance 讀取的數據量足夠均勻,然後判斷是哪些操作導致Map Instance 比較慢,最後考慮這些操作是否必須在Map 端完成,在其他階段是否會做得更好。

 

Join端:

        SQL在Join 執行階段會將Join Key相同的數據分發到同一個執行Instance上處理。如果某個Key 上的數據量比較大,則會導致該Instance 執行時間較長。其表現爲:在執行日誌中該Join Task 的大部分Instance 都已執行完成,但少數幾個Instance 一直處於執行中(這種現象稱之爲長尾)。

        這裏主要講述三種常見的傾斜場景:

        1:Join的某路輸入比較小,可以採用Map Join,避免分發引起長尾(Map Join的原理是將Join操作提前到Map 端執行,將小表讀人內存,順序掃描大表完成Join,這樣可以避免因爲分發key不均勻導致數據傾斜,MapJoin 的使用方法非常簡單,在代碼中select 後加上“/*+mapjoin(a) */”即可,其中a 代表小表的別名)。

        2:Join的每路輸入都較大,且長尾是空值導致的,可以將空值處理成隨機值,避免聚集。

        3:Join的每路輸入都較大,且長尾是熱點值導致的,可以對熱點值和非熱點值分別進行處理,再合併數據。

 

Reduce端:

        Reduce端負責的是對Map端梳理後的有序key-value鍵值對進行聚合,即進行Count、Sum 、Avg等聚合操作,得到最終聚合的結果。Distinct是MaxCompute SQL中支持的語法,用於對字段去重。比如計算在某個時間段內支付買家數、訪問UV等,都是需要用Distinct進行去重的。MaxCompute中Distinct的執行原理是將需要去重的宇段以及Group By字段聯合作爲key 將數據分發到Reduce端。

        1:因爲Distinct 操作,數據無法在Map 端的Shuffle階段根據Group By先做一次聚合操作,以減少傳輸的數據量,而是將所有的數據都傳輸到Reduce端,當key的數據分發不均勻時,就會導致Reduce端長尾。Reduce端產生長尾的主要原因就是key的數據分佈不均勻。比如有些Reduce任務Instance處理的數據記錄多,有些處理的數據記錄少,造成Reduce端長尾。如下幾種情況會造成Reduce 端長尾:

        2:對同一個表按照維度對不同的列進行Count Distinct操作,造成Map端數據膨脹,從而使得下游的Join和Reduce出現鏈路上的長尾。

        3:Map端直接做聚合時出現key值分佈不均勻,造成Reduce端長尾。

        4:動態分區數過多時可能造成小文件過多,從而引起Reduce端長尾。

        5:多個Distinct同時出現在一段SQL代碼中時,數據會被分發多次,不僅會造成數據膨脹N倍,還會把長尾現象放大N倍。

 

解決方案:

        1:嵌套編寫,先Group By再Count(*);group by維度過小:採用sum() group by的方式來替換count(distinct)完成計算。

        2:可以對熱點key進行單獨處理,然後通過“ Union All ”合併;

        3:可以把符合不同條件的數據放到不同的分區,避免通過多次“Insert Overwrite”,寫人表中,特別是分區數比較多時,能夠很好地簡化代碼;

        4:在把不同指標Join 在一起之前, 一定要確保指標的粒度是原始表的數據粒度;當代碼比較膝腫時,也可以將上述子查詢落到中間表裏。

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