Spark Shuffle系列之Shuffle介紹&演進過程

Shuffle是什麼

spark源碼分析之stage生成中,我們講到Spark在DAGSchduler階段會將一個Job劃分爲多個Stage,在上游Stage做map工作,下游Stage做reduce工作,其本質上還是MapReduce計算框架。Shuffle是連接map和reduce之間的橋樑,它將map的輸出對應到reduce輸入中,這期間涉及到序列化反序列化、跨節點網絡IO以及磁盤讀寫IO等,所以說Shuffle是整個應用程序運行過程中非常昂貴的一個階段,理解Spark Shuffle原理有助於優化Spark應用程序。

Shuffle&Stage

Stage劃分&與Shuffle關係

Spark Stage是根據對parent rdd的依賴的種類進行劃分的,如下圖所示:
在這裏插入圖片描述

  1. 窄依賴是指父RDD的每個分區只被子RDD的一個分區所使用,子RDD分區通常對應常數個父RDD分區,如上圖中的C->D; D->F; E->F
  2. 寬依賴(Shuffle依賴)是指父RDD的每個分區可能會被下游RDD的一個或者多個分區所使用,如上圖中的A->B, B,F->G。

寬依賴是劃分Stage的重要標誌,劃分出來的兩個Stage,上一個Stage執行的是Shuffle的Map操作,下一個Stage執行的是Shffule的Reduce操作,所以上面依賴的整個執行過程如下圖所示:
在這裏插入圖片描述
總結起來:寬依賴劃分了Stage,中間涉及了Shuffle過程,前一個stage的通過ShuffleMapTask進行Shuffle write, 把數據存儲在blockManager上面, 並且把數據位置元信息上報到driver的mapOutTrack組件中,下一個stage根據數據位置元信息,進行 shuffle read,拉取上個stage的輸出數據,進行數據處理。

常見Shuffle算子

Spark中常見的Shuffle算子有以下幾類:

  1. 去重: distinct
  2. 聚合: reduceByKey,groupBy,groupByKey,aggregateByKey,combineByKey,sortByKey
  3. 重分區:coalesce,repartition
  4. 集合或者表操作:intersection,subtract,join

當然判斷是否爲shuffle,最好還是看debug出來的Lineage信息,看到中間過程有ShuffledRDD表明發生了Shuffle操作:

scala> val rdd = sc.parallelize(Array("hello world", "hah xx")).flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
rdd: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[7] at reduceByKey at <console>:24

scala> rdd.toDebugString
res0: String =
(50) ShuffledRDD[7] at reduceByKey at <console>:24 []
 +-(50) MapPartitionsRDD[6] at map at <console>:24 []
    |   MapPartitionsRDD[5] at flatMap at <console>:24 []
    |   ParallelCollectionRDD[4] at parallelize at <console>:24 []

Spark Shuffle整體流程

Shuffle包含寫和讀兩個過程。寫是在map階段,將數據按照一定的分區規則歸類到不同的分區中,讀是在reduce階段,每個分區從map階段的輸出中拉取屬於自己的數據,如下圖所示:
在這裏插入圖片描述

所以,我們總結Shuffle的完整過程如下步驟(Shuffle write階段的Map任務我們稱爲Map-stage,Shuffle read階段的Reduce任務我們稱爲Reduce-stage):

  1. RDD是彈性分佈式數據集,五要素中有一個partition字段,一般有多個partition,而且可能分佈在不同的節點上面。所以Map-stage階段,每一個Map task負責處理一個特定的partition;在用戶程序中Shuffle算子一般會指定一個Partitioner函數,沒有指定時候默認是HashPartitioner,進行Reduce-stage的task的劃分,即處理結果可能會產生多少個不同的data partition。
  2. Map端執行的是Shuffle write操作,是將上游RDD的各個Partition數據,利用partitioner函數,將處理結果存入到不同的partition,這些數據存放在當前task執行的機器上,供Reduce端進行拉取。如上圖所示,map端有三個task: task0,task1和task2, 可能輸出3個不同的partition, task 0,task 1 和task2各自運行於不同的機器上,task 0中的部分處理結果會存入到data partition 0/1/2,task 1的部分處理結果也可能存入到data partition 0/1/2,同樣適用於task3
  3. 由於Map端產生了4個不同的data partition, 後續Shuffle Read中的task個數就爲4。task 0 就負責讀取data partition 0的數據,對於(reduce-stage, task0)來說,所要讀取的data partition 0的內容由task 0和task 1以及task 3中的partition 0共同組成,拉取到數據後進行用戶自定義的reduce函數操作得到結果。

步驟如上所述,但是我們應該有這樣子一個疑惑:Map-stage將結果寫到本地文件中,Reduce-stage任務是如何獲取有哪些文件以及這些文件的位置在哪裏的?這個主要是依賴MapOutputTracker,ShuffleMapTask結束後會上報一個MapStatus到MapOutputTracker,在MapStatus中會反應出給哪些data partition寫入了數據,寫入了數據則的大小等信息。reduce任務就是根據MapOutputTracker提供的信息決定從哪些executor獲取需要的map輸出數據。

Spark Shuffle演進過程

Hash Based Shuffle

Hash Shuffle v1

在Spark 0.8及以前默認是使用Hash Based Shuffle。

  1. map階段(shuffle write),每個map都會爲下游stage的每個partition寫一個臨時文件,假如下游stage有1000個partition,那麼每個map都會生成1000個臨時文件,一般來說一個executor上會運行多個map task,這樣下來,一個executor上會有非常多的臨時文件,假如一個executor上運行M個map task,下游stage有N個partition,那麼一個executor上會生成M*N個文件。另外一個executor上有K個核,同時運行K個map task, 那麼就需要K*N個write handler,可能會耗盡executor的文件描述符,同時帶來大量內存的消耗。
  2. reduce階段(shuffle read),每個reduce task都會拉取所有map對應的那部分partition數據,那麼executor會打開所有臨時文件準備網絡傳輸,這裏又涉及到大量文件描述符,另外,如果reduce階段有combiner操作,那麼它會把網絡中拉到的數據保存在一個HashMap中進行合併操作,如果數據量較大,很容易引發OOM操作。

可見早期的Hash Shuffle實現簡單,在小數據量下運行比較快,一旦數據量較大,基本就崩了。

Hash Shuffle v2

爲了解決文件過多的問題,在Spark 0.8.1爲Hash Based Shuffle引入File Consolidation機制,和V1思路一樣,只不過在map階段,一個executor上所有的map task生成的分區文件只有一份,即將所有的map task相同的分區文件合併,這樣每個executor上最多隻生成N個分區文件,雖然減少了文件數目,但是沒有解決多個核心造成的大量內存消耗以及reduce階段的OOM問題。

爲了解決OOM問題,Spark 0.9 引入ExternalAppendOnlyMap,在combine的時候,可以將數據spill到磁盤,然後通過堆排序merge。

Sort Based Shuffle

Sort Shuffle V1

spark 1.1.0版本中參考Hadoop MapReduce中Shuffle的實現,引入Sort Based Shuffle,但默認仍爲Hash Based Shuffle,Spark 1.2 默認的Shuffle方式改爲Sort Based Shuffle。

  1. 在map階段(shuffle write),會按照partition id以及key對記錄進行排序,將所有partition的數據寫在同一個文件中,該文件中的數據首先按照partition id排序;每個partition內部是按照key進行排序存放。另外還會通過一個索引文件記錄每個partition的大小和偏移量。這樣一來,每個map task一次只開兩個文件描述符,一個寫數據,一個寫索引,大大減輕了Hash Shuffle大量文件描述符的問題,即使一個executor有K個core,那麼最多一次性開K*2個文件描述符。
  2. 在reduce階段(shuffle read),reduce task拉取數據做combine時不再是採用HashMap,而是採用ExternalAppendOnlyMap,該數據結構在做combine時,如果內存不足,會刷寫磁盤,很大程度的保證了魯棒性,避免大數據情況下的OOM。

總體上看來Sort Shuffle解決了Hash Shuffle的所有弊端,但是因爲需要其shuffle過程需要對記錄進行排序,所以在性能上有所損失。

Unsafe Shuffle

從spark 1.5.0開始,spark開始了鎢絲計劃(Tungsten-Sort Based Shuffle),目的是優化內存和CPU的使用,進一步提升spark的性能。爲此,引入Unsafe Shuffle,它的做法是將數據記錄用二進制的方式存儲,直接在序列化的二進制數據上sort而不是在java對象上,這樣一方面可以減少memory的使用和GC的開銷,另一方面避免shuffle過程中頻繁的序列化以及反序列化。在排序過程中,它提供cache-efficient sorter,使用一個8 bytes的指針,把排序轉化成了一個指針數組的排序,極大的優化了排序性能。但是使用Unsafe Shuffle有幾個限制: shuffle階段不能有aggregate操作,分區數不能超過一定大小(2^24−1,這是可編碼的最大parition id),所以像reduceByKey這類有aggregate操作的算子是不能使用Unsafe Shuffle,它會退化採用Sort Shuffle。

Sort Shuffle V2

從spark-1.6.0開始,把Sort Shuffle和Unsafe Shuffle全部統一到Sort Shuffle中,如果檢測到滿足Unsafe Shuffle條件會自動採用Unsafe Shuffle,否則採用Sort Shuffle。從spark-2.0.0開始,spark把Hash Shuffle移除,可以說目前spark-2.0中只有一種Shuffle,即爲Sort Shuffle。

參考

  1. https://www.jianshu.com/p/4c5c2e535da5
  2. https://toutiao.io/posts/eicdjo/preview
  3. http://sharkdtu.com/posts/spark-shuffle.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章