spark Shuffle相關

破棉襖發表於2015-09-30

 

Shuffle 相關

Shuffle操作大概是對Spark效能影響最大的步驟之一(因為可能涉及到排序,磁碟IO,網路IO等眾多CPU或IO密集的操作),這也是為什麼在Spark 1.1的程式碼中對整個Shuffle框架程式碼進行了重構,將Shuffle相關讀寫操作抽象封裝到Pluggable的Shuffle Manager中,便於試驗和實現不同的Shuffle功能模組。例如為了解決Hash Based的Shuffle Manager在檔案讀寫效率方面的問題而實現的Sort Base的Shuffle Manager。

spark.shuffle.manager

用來配置所使用的Shuffle Manager,目前可選的Shuffle Manager包括預設的org.apache.spark.shuffle.sort.HashShuffleManager(配置引數值為hash)和新的org.apache.spark.shuffle.sort.SortShuffleManager(配置引數值為sort)。

這兩個ShuffleManager如何選擇呢,首先需要了解他們在實現方式上的區別。

HashShuffleManager,故名思義也就是在Shuffle的過程中寫資料時不做排序操作,只是將資料根據Hash的結果,將各個Reduce分割槽的資料寫到各自的磁碟檔案中。帶來的問題就是如果Reduce分割槽的數量比較大的話,將會產生大量的磁碟檔案。如果檔案數量特別巨大,對檔案讀寫的效能會帶來比較大的影響,此外由於同時開啟的檔案控制程式碼數量眾多,序列化,以及壓縮等操作需要分配的臨時記憶體空間也可能會迅速膨脹到無法接受的地步,對記憶體的使用和GC帶來很大的壓力,在Executor記憶體比較小的情況下尤為突出,例如Spark on Yarn模式。

SortShuffleManager,是1.1版本之後實現的一個試驗性(也就是一些功能和介面還在開發演變中)的ShuffleManager,它在寫入分割槽資料的時候,首先會根據實際情況對資料採用不同的方式進行排序操作,底線是至少按照Reduce分割槽Partition進行排序,這樣來至於同一個Map任務Shuffle到不同的Reduce分割槽中去的所有資料都可以寫入到同一個外部磁碟檔案中去,用簡單的Offset標誌不同Reduce分割槽的資料在這個檔案中的偏移量。這樣一個Map任務就只需要生成一個shuffle檔案,從而避免了上述HashShuffleManager可能遇到的檔案數量巨大的問題

兩者的效能比較,取決於記憶體,排序,檔案操作等因素的綜合影響。

對於不需要進行排序的Shuffle操作來說,如repartition等,如果檔案數量不是特別巨大,HashShuffleManager面臨的記憶體問題不大,而SortShuffleManager需要額外的根據Partition進行排序,顯然HashShuffleManager的效率會更高。

而對於本來就需要在Map端進行排序的Shuffle操作來說,如ReduceByKey等,使用HashShuffleManager雖然在寫資料時不排序,但在其它的步驟中仍然需要排序,而SortShuffleManager則可以將寫資料和排序兩個工作合併在一起執行,因此即使不考慮HashShuffleManager的記憶體使用問題,SortShuffleManager依舊可能更快。

spark.shuffle.sort.bypassMergeThreshold

這個引數僅適用於SortShuffleManager,如前所述,SortShuffleManager在處理不需要排序的Shuffle操作時,由於排序帶來效能的下降。這個引數決定了在這種情況下,當Reduce分割槽的數量小於多少的時候,在SortShuffleManager內部不使用Merge Sort的方式處理資料,而是與Hash Shuffle類似,直接將分割槽檔案寫入單獨的檔案,不同的是,在最後一步還是會將這些檔案合併成一個單獨的檔案。這樣透過去除Sort步驟來加快處理速度,代價是需要併發開啟多個檔案,所以記憶體消耗量增加,本質上是相對HashShuffleMananger一個折衷方案。 這個引數的預設值是200個分割槽,如果記憶體GC問題嚴重,可以降低這個值。

spark.shuffle.consolidateFiles

這個配置引數僅適用於HashShuffleMananger的實現,同樣是為了解決生成過多檔案的問題,採用的方式是在不同批次執行的Map任務之間重用Shuffle輸出檔案,也就是說合並的是不同批次的Map任務的輸出資料,但是每個Map任務所需要的檔案還是取決於Reduce分割槽的數量,因此,它並不減少同時開啟的輸出檔案的數量,因此對記憶體使用量的減少並沒有幫助。只是HashShuffleManager裡的一個折中的解決方案。

需要注意的是,這部分的程式碼實現儘管原理上說很簡單,但是涉及到底層具體的檔案系統的實現和限制等因素,例如在併發訪問等方面,需要處理的細節很多,因此一直存在著這樣那樣的bug或者問題,導致在例如EXT3上使用時,特定情況下效能反而可能下降,因此從Spark 0.8的程式碼開始,一直到Spark 1.1的程式碼為止也還沒有被標誌為Stable,不是預設採用的方式。此外因為並不減少同時開啟的輸出檔案的數量,因此對效能具體能帶來多大的改善也取決於具體的檔案數量的情況。所以即使你面臨著Shuffle檔案數量巨大的問題,這個配置引數是否使用,在什麼版本中可以使用,也最好還是實際測試以後再決定。

spark.shuffle.spill

shuffle的過程中,如果涉及到排序,聚合等操作,勢必會需要在記憶體中維護一些資料結構,進而佔用額外的記憶體。如果記憶體不夠用怎麼辦,那只有兩條路可以走,一就是out of memory 出錯了,二就是將部分資料臨時寫到外部儲存裝置中去,最後再合併到最終的Shuffle輸出檔案中去。

這裡spark.shuffle.spill 決定是否Spill到外部儲存裝置(預設開啟),如果你的記憶體足夠使用,或者資料集足夠小,當然也就不需要Spill,畢竟Spill帶來了額外的磁碟操作。

spark.shuffle.memoryFraction / spark.shuffle.safetyFraction

在啟用Spill的情況下,spark.shuffle.memoryFraction(1.1後預設為0.2)決定了當Shuffle過程中使用的記憶體達到總記憶體多少比例的時候開始Spill。

透過spark.shuffle.memoryFraction可以調整Spill的觸發條件,即Shuffle佔用記憶體的大小,進而調整Spill的頻率和GC的行為。總的來說,如果Spill太過頻繁,可以適當增加spark.shuffle.memoryFraction的大小,增加用於Shuffle的記憶體,減少Spill的次數。當然這樣一來為了避免記憶體溢位,對應的可能需要減少RDD cache佔用的記憶體,即減小spark.storage.memoryFraction的值,這樣RDD cache的容量減少,有可能帶來效能影響,因此需要綜合考慮。

由於Shuffle資料的大小是估算出來的,一來為了降低開銷,並不是每增加一個資料項都完整的估算一次,二來估算也會有誤差,所以實際暫用的記憶體可能比估算值要大,這裡spark.shuffle.safetyFraction(預設為0.8)用來作為一個保險係數,降低實際Shuffle使用的記憶體閥值,增加一定的緩衝,降低實際記憶體佔用超過使用者配置值的機率。

spark.shuffle.spill.compress / spark.shuffle.compress

這兩個配置引數都是用來設定Shuffle過程中是否使用壓縮演算法對Shuffle資料進行壓縮,前者針對Spill的中間資料,後者針對最終的shuffle輸出檔案,預設都是True

理論上說,spark.shuffle.compress設定為True通常都是合理的,因為如果使用千兆以下的網路卡,網路頻寬往往最容易成為瓶頸。此外,目前的Spark任務排程實現中,以Shuffle劃分Stage,下一個Stage的任務是要等待上一個Stage的任務全部完成以後才能開始執行,所以shuffle資料的傳輸和CPU計算任務之間通常不會重疊,這樣Shuffle資料傳輸量的大小和所需的時間就直接影響到了整個任務的完成速度。但是壓縮也是要消耗大量的CPU資源的,所以開啟壓縮選項會增加Map任務的執行時間,因此如果在CPU負載的影響遠大於磁碟和網路頻寬的影響的場合下,也可能將spark.shuffle.compress 設定為False才是最佳的方案

對於spark.shuffle.spill.compress而言,情況類似,但是spill資料不會被髮送到網路中,僅僅是臨時寫入本地磁碟,而且在一個任務中同時需要執行壓縮和解壓縮兩個步驟,所以對CPU負載的影響會更大一些,而磁碟頻寬(如果標配12HDD的話)可能往往不會成為Spark應用的主要問題,所以這個引數相對而言,或許更有機會需要設定為False。

總之,Shuffle過程中資料是否應該壓縮,取決於CPU/DISK/NETWORK的實際能力和負載,應該綜合考慮。

spark.locality.wait

spark.locality.wait和spark.locality.wait.process,spark.locality.wait.node, spark.locality.wait.rack這幾個引數影響了任務分配時的本地性策略的相關細節。

Spark中任務的處理需要考慮所涉及的資料的本地性的場合,基本就兩種,一是資料的來源是HadoopRDD; 二是RDD的資料來源來自於RDD Cache(即由CacheManager從BlockManager中讀取,或者Streaming資料來源RDD)。其它情況下,如果不涉及shuffle操作的RDD,不構成劃分Stage和Task的基準,不存在判斷Locality本地性的問題,而如果是ShuffleRDD,其本地性始終為No Prefer,因此其實也無所謂Locality。

在理想的情況下,任務當然是分配在可以從本地讀取資料的節點上時(同一個JVM內部或同一臺物理機器內部)的執行時效能最佳。但是每個任務的執行速度無法準確估計,所以很難在事先獲得全域性最優的執行策略,當Spark應用得到一個計算資源的時候,如果沒有可以滿足最佳本地性需求的任務可以執行時,是退而求其次,執行一個本地性條件稍差一點的任務呢,還是繼續等待下一個可用的計算資源已期望它能更好的匹配任務的本地性呢?

這幾個引數一起決定了Spark任務排程在得到分配任務時,選擇暫時不分配任務,而是等待獲得滿足程式內部/節點內部/機架內部這樣的不同層次的本地性資源的最長等待時間。預設都是3000毫秒。

基本上,如果你的任務數量較大和單個任務執行時間比較長的情況下,單個任務是否在資料本地執行,代價區別可能比較顯著,如果資料本地性不理想,那麼調大這些引數對於效能最佳化可能會有一定的好處。反之如果等待的代價超過帶來的收益,那就不要考慮了。

特別值得注意的是:在處理應用剛啟動後提交的第一批任務時,由於當作業排程模組開始工作時,處理任務的Executors可能還沒有完全註冊完畢,因此一部分的任務會被放置到No Prefer的佇列中,這部分任務的優先順序僅次於資料本地性滿足Process級別的任務,從而被優先分配到非本地節點執行,如果的確沒有Executors在對應的節點上執行,或者的確是No Prefer的任務(如shuffleRDD),這樣做確實是比較最佳化的選擇,但是這裡的實際情況只是這部分Executors還沒來得及註冊上而已。這種情況下,即使加大本節中這幾個引數的數值也沒有幫助。針對這個情況,有一些已經完成的和正在進行中的PR透過例如動態調整No Prefer佇列,監控節點註冊比例等等方式試圖來給出更加智慧的解決方案。不過,你也可以根據自身叢集的啟動情況,透過在建立SparkContext之後,主動Sleep幾秒的方式來簡單的解決這個問題。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/29754888/viewspace-1812412/,如需轉載,請註明出處,否則將追究法律責任。

相關文章