MapReduce Shuffle和Sort的運行機制

MapReduce 保證對每個reduce的輸入都是已排序的,系統執行排序的過程——傳輸map的輸出到reduce作爲輸入——被稱作“shuffle”(譯爲“洗牌”)。在許多方面,Shuffle是MapReduce的心臟和發生“神奇”的地方。

The Map Side

在map函數開始產生輸出時,並不是簡單的寫到磁盤上,出於效率的原因而是先寫到內存的緩衝區,並做一些預排序處理,最後才寫到磁盤。下圖展示了到底發生了什麼:


每一個map task都有一個環形的內存緩衝區,用於存儲map的輸出。緩衝區的大小默認爲100MB(這個大小可以通過改變mapreduce.task.io.sort.mb屬性來調整),當緩衝區中的內容達到指定閾值的大小(由mapreduce.map.sort.spill.percent屬性設置,默認爲0.8或80%),一個後臺進程就開始將緩衝區中的內容溢出(spill)到磁盤上。當spill發生時,map的輸出就可以繼續寫到緩衝區中,但是在這期間如果緩衝區被填滿了,該 map將被鎖定,直到spill完成。Spill是以循環(round-robin)的方式將溢出的內容寫到由mapreduce.cluster.local.dir屬性指定的目錄中,該目錄爲當前job的一個子目錄。

在寫到磁盤之前,線程先將數據分區,每個分區對應於它們最終被髮送到的reducer。在每個分區內,後臺進程會在內存中按key排序,如果有combiner函數,它將在已排序的輸出上運行。運行combiner函數可以使map的輸出更加緊湊,所以,將有更少的數據寫到本地磁盤上和傳遞給reducer 函數。

內存緩衝區每次達到spill的閾值,都會創建一個spill文件,所以在map task寫完最後的輸出記錄後,可能會存在幾個spill 文件。在task結束之前,這些spill文件將被合併成一個已分區和排序好的輸出文件。mapreduce.task.io.sort.factor配置屬性控制着每次合併spill文件的最大數量,默認爲10。

如果有至少三個spill文件(由mapreduce.map.combine.minspills屬性設置),在寫入輸出文件之前combiner函數將會再次運行。combiner函數對輸入反覆運行,並不影響最終的結果。如果僅有1個或2個spill文件,再調用combiner函數來減少map 輸出的大小並不值得,所以它不會再運行map的輸出。

在map輸出寫到磁盤時,對其進行壓縮通常是比較好的主意,因爲這樣做可以使map輸出較快的寫到磁盤上,節省存儲空間,並且減少傳遞個reduce函數的數據量。默認,map輸出是不壓縮的,但是可以通過mapreduce.map.output.compress屬性設置爲true來啓用壓縮,使用的壓縮庫是由mapreduce.map.output.compress.codec屬性指定的。

輸出文件的分區是通過HTTP提供給reduce函數的。用於文件分區的最大工作線程的數量是由mapreduce.shuffle.max.threads屬性控制的,此設置針對的是每個node manager,而不是map task。默認爲0,意味着工作線程的最大數量爲機器處理器數量的2倍。

The Reduce Side

轉到reduce處理部分。運行map task的輸出文件存放在本地的磁盤上(注意雖然map的輸出總是寫到磁盤上,但reduce輸出也許不是),但現在需要在其分區文件上運行reduce task。並且,reduce task所需的特定分區來自於集羣中的若干map task的輸出。每個map task的完成時間不同,所以只要有map task完成,reduce task就開始複製它們的輸出。這就是所謂的reduce task的copy階段。reduce task有少量的copy線程,所以它可以並行提取map的輸出,默認爲5個線程,這個數字可以通過設置mapreduce.reduce.shuffle.parallelcopies屬性來改變。

注意
reducer怎麼知道要從哪臺機器獲取map輸出的?

當map task完成時,它會使用心跳機制通知application master,因此,對於一個給定的job,application master知道map 輸出和主機間的映射關係。reducer的一個線程定期向application master詢問map輸出的主機,直到得到所有輸出。
主機並沒有在第一個reducer獲取map輸出後就立即刪除它們,因爲reducer隨後可能會失敗。反而,它們會等待直到application master通知它們可以刪除,這是在job完成後才執行的。

如果map輸出比較小,它們將被複制到reduce task JVM的內存中(緩衝區的大小是由mapreduce.reduce.shuffle.input.buffer.percent屬性控制的,它指定了使用堆大小的比例);否則,它們將被拷貝到磁盤上。當內存緩衝區大小達到一個閾值(由mapreduce.reduce.shuffle.merge.percent屬性控制)或者達到map輸出的閾值(由mapreduce.reduce.merge.inmem.threshold控制)時,將會被合併和溢出(spill)到磁盤上。如果指定了combiner,在merge期間它將會運行,以減少寫到磁盤上的數據量。

隨着複製的累積,一個後臺進程會把它們合併成若干較大的、已排序的文件,這爲之後的merge操作節省了時間。需要注意的是對於壓縮的map輸出都必須在內存中解壓縮,以便於merge它們。

當所有的map輸出複製完成,reduce task就進入sort階段(恰當的說應該稱之爲merge階段,因爲排序已經在map端完成),這個階段將合併map的輸出,並保持它們的排序順序,這將循環進行。例如,如果有50個map輸出,合併係數爲10(默認爲10,由mapreduce.task.io.sort.factor屬性設置,與map的合併類似),那麼將會合並5輪,每輪將有10個文件合併成1個文件,所以最後將有5箇中間文件。

而不是還有最後一輪,將這5個文件合併成一個已排序的文件,而是直接傳遞給reduce函數,這樣做可以節省一次磁盤的訪問。這就是最後的階段:reduce階段。最後的合併可能來自於內存和磁盤的混合。

注意
事實上,每輪合併的文件數比本例展示的更加微妙。最後一輪的目標是合併的最小文件數量要匹配合並係數。所以如果有40個文件,合併係數爲10,並不是4輪合併每輪合併10個文件最終得到4個文件,而是第一輪僅合併4個文件,之後的3輪每輪將合併完整的10文件,那現在有4個合併後的文件和6個未合併的文件總共10個文件最爲最後一輪。注意,這並沒有改變輪的次數,它只是一個優化,以最大限度的減少寫到磁盤的數據量,因爲最後一輪總是直接合併到reduce。處理過程如下圖所示。


在reduce階段,將對已排序map輸出中的每個key調用reduce函數,這個階段的輸出被直接寫到文件系統上,通常爲HDFS,對於HDFS來說,因爲node manager也正在運行一個datanode,所以第一個塊的副本將被直接寫到本地磁盤上。
Configuration Tuning

現在我們能夠更好的理解怎樣調整shuffle,以提高MapReduce的性能。相關的設置,能夠適用於每個job(除非另有說明),如下兩表的總結,對於每個配置的默認值,能夠較好的適用於一般的job。

表1,map端可調整的屬性

Property name

Type 

Default value 

Description

mapreduce.task.io.sort.mb 

 int

100

對map輸出進行排序的內存緩衝區的大小,單位MB。

mapreduce.map.sort.spill.percent 

 float 

0.80 

map輸出佔用內存緩衝區的比例,如果達到此比例,將會寫到磁盤上。

 mapreduce.task.io.sort.factor

 int

10 

 在排序文件時,每次合併文件的最大數量。這個屬性同樣也用於reduce,通常將將該值增加到100。

 mapreduce.map.combine.minspills 

 int 

 3

 運行combiner函數需要spill文件的最小數量(如果指定了combiner)

 mapreduce.map.output.compress

 boolean

 false

  是否壓縮map的輸出

 mapreduce.map.output.compress.codec 

 Class name   

 org.apache.hadoop.io.
compress.DefaultCodec     

 壓縮map 輸出用的編解碼器

 mapreduce.shuffle.max.threads

 int

 0

 在shuffle階段,每個節點用於處理map輸出到reducer的工作線程數。這是集羣範圍的設置,不能針對單個job設置。設置爲0意味着將使用Netty默認的兩倍於可用的處理進程。

表2,reduce端可調整的屬性

Property name

Type

Default value

Description

 mapreduce.reduce.shuffle.parallelcopies

 int

 5 

 用於將map輸出複製到reduer的線程數。

 mapreduce.reduce.shuffle.maxfetchfailures

 int

 10

 在報告錯誤前,一個reducer獲取map輸出的嘗試次數。

 mapreduce.task.io.sort.factor

 int

 10

 在排序文件時,每次合併流的最大數量,這個屬性也應用於map。

 mapreduce.reduce.shuffle.input.buffer.percent 

 float

 0.70 

 在shuffle的copy階段,分配給map 輸出緩衝區的比例。

 mapreduce.reduce.shuffle.merge.percent

 float

 0.66

 map輸出緩衝區的使用比例(由mapred.job.shuffle.input.buffer.percent定義)的閾值。當達到這個值時,就開始合併map輸出並溢出到磁盤上。

 mapreduce.reduce.merge.inmem.threshold

 int

 1000

 處理合並map輸出和溢出到磁盤的線程數。該值爲0意味着沒有限制,則spill的行爲僅有mapreduce.reduce.shuffle.merge.percent屬性控制。

 mapreduce.reduce.input.buffer.percent                                                                                                                                 

 float                     

 0.0                                  

 在reduce過程中,在內存中保留map輸出的大小佔整個堆空間大小的比例。在reduce階段開始時,map輸出在內存中的大小不能超過次大小。默認在reduce開始之前,爲了給reduce儘可能多的內存空間,所有的map輸出是在磁盤進行合併的。如果你的reducer需要的內存較少,可以增加此值,以減少寫入磁盤的次數

總的原則是給shuffle儘可能多的內存。但有一個折中,因爲還需要確保有足夠的內存提供給map和reduce函數。這就是爲什麼在編寫map和reduce時要儘可能的少用內存——當然它們不能無限制的使用內存(例如,避免map輸出的累計)。

爲運行map和reduce的JVM分配內存是由mapred.child.java.opts屬性設置的。在task 節點你應該試着使這個值儘可能的大。

在map端,可以通過降低溢出到磁盤的次數來獲得更好的性能。如果你能夠估算出map輸出的大小,你可以適當的調整mapreduce.task.io.sort.*屬性值來使溢出的數量最小。特別是,你可以適當的增加mapreduce.task.io.sort.mb屬性值。在整個job運行過程中,有一個MapReduce計數器,用於統計溢出到磁盤上的總記錄數,這有助於優化,需要注意的是,這個計數器包括map和reduce的溢出。

在reduce端,獲得最佳的性能是在所有的中間數據都駐留在內存中,但這種情況通常不會發生,因爲一般情況下所有的內存是留給reduce函數的。但是,如果你的reduce函數使用較少的內存就可以,你可以設置apreduce.reduce.merge.inmem.threshold屬性值爲0 和 mapreduce.reduce.input.buffer.percent屬性值爲1.0(或較低的值,見表2),也許會帶來性能的提升。

一般,Hadoop 緩衝區的大小默認爲4KB,這是比較低的,因此應該在集羣中增加這個值(通過設置io.file.buffer.size屬性)

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