Spark的Shuffle過程

1. Spark Shuffle概述

Shuffle就是對數據進行重組,由於分佈式計算的特性和要求,在實現細節上更加繁瑣和複雜。

在MapReduce框架中,Shuffle是連接Map和Reduce之間的橋樑,Map階段通過shuffle讀取數據,並輸出到對應的Reduce端;而Reduce階段負責從Map端拉取數據並進行計算。在整個shuffle過程中,往往伴隨着大量的磁盤和網絡IO,所以shuffle性能的高低也直接決定了整個應用程序的性能高低。

在Spark中也有自己的shuffle過程,RDD之間的關係包含寬窄依賴,在寬依賴之間是存在shuffle過程的,因此在spark程序的每個job中,都是根據是否有shuffle操作進行階段(stage)劃分,每個stage都是一系列的RDD map操作。

2. Shuffle的作用

shuffle的中文解釋是“洗牌”,可以理解爲將集羣中各節點上的數據重新打亂整合的過程,下面以問題的形式來闡述Spark shuffle的作用。

問題:每一個key對應的value不一定都是在一個partition中,也不太可能在同一個節點上,因爲RDD是分佈式的彈性的數據集,他的partition極有可能分佈在各個節點上,那麼他們怎麼聚合呢?

Shuffle Write:上一個stage的每個map task就必須保證將自己處理的當前分區中的數據相同的key寫入一個分區文件中,可能會寫入多個不同的分區文件中

Shuffle Read:reduce task就會從上一個stage的所有task所在的機器上尋找屬於自己的那些分區文件,這樣就可以保證每一個key所對應的value都會匯聚到同一個節點上去處理和聚合

3. Spark Shuffle的運行時機

先拿Spark shuffle與MR shuffle 做個類比,spark的shuffle過程是在stage與stage之間進行的,那麼兩個stage可以一個看做是Map task,一個是Reduce task。

產生shuffle的算子

算子 作用
distinct 去重
reduceBykey 聚合
groupBykey 分組聚合
combinerBykey 聚合
sortBykey 排序
sortBy 排序(按某個字段)
coalesce 重分區
repartition 重分區
join 表關聯

4. Spark Shuffle的運行機理及圖解

MR Shuffle過程

在這裏插入圖片描述

4.1 HashShuffle

4.1.1 HashShuffle概述

在spark-1.6版本之前,一直使用HashShuffle,在spark-1.6版本之後使用Sort-Base Shuffle,因爲HashShuffle存在的不足所以就替換了HashShuffle.

我們知道,Spark的運行主要分爲2部分:一部分是驅動程序,其核心是SparkContext;另一部分是Worker節點上Task,它是運行實際任務的。程序運行的時候,Driver和Executor進程相互交互:運行什麼任務,即Driver會分配Task到Executor,Driver 跟 Executor 進行網絡傳輸; 任務數據從哪兒獲取,即Task要從 Driver 抓取其他上游的 Task 的數據結果,所以有這個過程中就不斷的產生網絡結果。其中,下一個 Stage 向上一個 Stage 要數據這個過程,我們就稱之爲 Shuffle。

4.1.2 優化前的Hash shuffle機制

在這裏插入圖片描述
shuffle write階段

shuffle read階段

缺點:
在Hash shuffle沒有優化前,每一個shuffle MapTask 會爲每一個ReduceTask創建一個buffer緩存,並且會爲每一個buffer創建一個block文件,這樣就會導致shuffle過程中產生大量磁盤小文件。

磁盤小文件過多帶來的問題?

  1. Write階段創建大量的寫文件對象
  2. read階段,來拉去數據時就要產生大量的網絡IO
  3. read階段創建大量的讀文件對象

注意:創建對象過多的話,會導致JVM內存不足,JVM內存不足就會導致GC(OOM)

4.1.3 優化後的Hash shuffle機制

在這裏插入圖片描述
每一個Executor進程根據核數,決定Task的併發數量,比如executor核數是2,就是可以併發運行兩個task,如果是一個則只能運行一個task。

假設executor核數是1,ShuffleMapTask數量是M,那麼它依然會根據ResultTask的數量R,創建R個buffer緩存,然後對key進行hash,數據進入不同的buffer中,每一個bucket對應着一個block file,用於刷新buffer緩存裏的數據。

然後下一個task運行的時候,那麼不會再創建新的buffer和block file,而是複用之前的task已經創建好的buffer和block file。即所謂同一個Executor進程裏所有Task都會把相同的key放入相同的buffer緩衝區中。

這樣的話,生成文件的數量就是(本地worker的executor數量executor的coresResultTask數量)如上圖所示,即2 * 1* 3 = 6個文件,每一個Executor的shuffleMapTask數量100,ReduceTask數量爲100,那麼

未優化的HashShuffle的文件數是2 1 100100 =20000,優化之後的數量是21*100 = 200文件,相當於少了100倍

缺點:如果 Reducer 端的並行任務或者是數據分片過多的話則 Core * Reducer Task 依舊過大,也會產生很多小文件。

磁盤小文件個數 = core * reduce Task

4.2 SortShuffle

SortShuffle介紹

爲了緩解Shuffle過程產生文件數過多和Writer緩存開銷過大的問題,spark引入了類似於hadoop Map-Reduce的shuffle機制。該機制每一個ShuffleMapTask不會爲後續的任務創建單獨的文件,而是會將所有的Task結果寫入同一個文件,並且對應生成一個索引文件。以前的數據是放在內存緩存中,等到數據完了再刷到磁盤,現在爲了減少內存的使用,在內存不夠用的時候,可以將輸出溢寫到磁盤,結束的時候,再將這些不同的文件聯合內存的數據一起進行歸併,從而減少內存的使用量。一方面文件數量顯著減少,另一方面減少Writer緩存所佔用的內存大小,而且同時避免GC的風險和頻率。

SortShuffleManager的運行機制主要分成兩種。

  1. 普通運行機制
  2. bypass運行機制。

當shuffle read task(Reduce Task)的數量小於等於spark.shuffle.sort.bypassMergeThreshold參數的值時(默認爲200),就會啓用bypass機制。當shuffle read task(Reduce Task)的數量小於等於spark.shuffle.sort.bypassMergeThreshold參數的值時(默認爲200),就會啓用bypass機制。

4.2.1 普通運行機制

在這裏插入圖片描述
寫入內存數據結構

該圖說明了普通的SortShuffleManager的原理。在該模式下,數據會先寫入一個內存數據結構中(默認5M),此時根據不同的shuffle算子,可能選用不同的數據結構。如果是reduceByKey這種聚合類的shuffle算子,那麼會選用Map數據結構,一邊通過Map進行聚合,一邊寫入內存;如果是join這種普通的shuffle算子,那麼會選用Array數據結構,直接寫入內存。接着,每寫一條數據進入內存數據結構之後,就會判斷一下,是否達到了某個臨界閾值。如果達到臨界閾值的話,那麼就會嘗試將內存數據結構中的數據溢寫到磁盤,然後清空內存數據結構。

注意:

shuffle中的定時器:定時器會檢查內存數據結構的大小,如果內存數據結構空間不夠,那麼會申請額外的內存,申請的大小滿足如下公式:

applyMemory=nowMenory*2-oldMemory

申請的內存=當前的內存情況*2-上一次的內嵌情況

意思就是說內存數據結構的大小的動態變化,如果存儲的數據超出內存數據結構的大小,將申請內存數據結構存儲的數據*2-內存數據結構的設定值的內存大小空間。申請到了,內存數據結構的大小變大,內存不夠,申請不到,則發生溢寫

排序

在溢寫到磁盤文件之前,會先根據key對內存數據結構中已有的數據進行排序。

溢寫

排序過後,會分批將數據寫入磁盤文件。默認的batch數量是10000條,也就是說,排序好的數據,會以每批1萬條數據的形式分批寫入磁盤文件。寫入磁盤文件是通過Java的BufferedOutputStream實現的。BufferedOutputStream是Java的緩衝輸出流,首先會將數據緩衝在內存中,當內存緩衝滿溢之後再一次寫入磁盤文件中,這樣可以減少磁盤IO次數,提升性能。

merge

一個task將所有數據寫入內存數據結構的過程中,會發生多次磁盤溢寫操作,也就會產生多個臨時文件。最後會將之前所有的臨時磁盤文件都進行合併,這就是merge過程,此時會將之前所有臨時磁盤文件中的數據讀取出來,然後依次寫入最終的磁盤文件之中。此外,由於一個task就只對應一個磁盤文件,也就意味着該task爲Reduce端的stage的task準備的數據都在這一個文件中,因此還會單獨寫一份索引文件,其中標識了下游各個task的數據在文件中的start offset與end offset。

SortShuffleManager由於有一個磁盤文件merge的過程,因此大大減少了文件數量。比如第一個stage有50個task,總共有10個Executor,每個Executor執行5個task,而第二個stage有100個task。由於每個task最終只有一個磁盤文件,因此此時每個Executor上只有5個磁盤文件,所有Executor只有50個磁盤文件。

注意:

①block file= 2M

一個map task會產生一個索引文件和一個數據大文件

② m*r>2m(r>2):

SortShuffle會使得磁盤小文件的個數再次的減少

4.2.2 bypass運行機制

在這裏插入圖片描述
bypass運行機制的觸發條件如下:

  1. shuffle map task數量小於spark.shuffle.sort.bypassMergeThreshold參數的值。

  2. 不是聚合類的shuffle算子(比如reduceByKey)。

此時task會爲每個reduce端的task都創建一個臨時磁盤文件,並將數據按key進行hash然後根據key的hash值,將key寫入對應的磁盤文件之中。當然,寫入磁盤文件時也是先寫入內存緩衝,緩衝寫滿之後再溢寫到磁盤文件的。最後,同樣會將所有臨時磁盤文件都合併成一個磁盤文件,並創建一個單獨的索引文件。

該過程的磁盤寫機制其實跟未經優化的HashShuffleManager是一模一樣的,因爲都要創建數量驚人的磁盤文件,只是在最後會做一個磁盤文件的合併而已。因此少量的最終磁盤文件,也讓該機制相對未經優化的HashShuffleManager來說,shuffle read的性能會更好。

而該機制與普通SortShuffleManager運行機制的不同在於:

第一,磁盤寫機制不同;

第二,不會進行排序。也就是說,啓用該機制的最大好處在於,shuffle write過程中,不需要進行數據的排序操作,也就節省掉了這部分的性能開銷。

5. Spark的內存管理及參數調優

5.1 spark的內存管理

Spark的內存管理分爲靜態內存管理和統一內存管理。

靜態內存管理:內存存儲、執行內存和其他內存的大小在運行期間是固定的。

統一內存管理:Spark1.6之後引入的,與靜態內存管理的不同在於儲存內存和執行內存共享同一塊空間,可以互相借用對方的空間。

5.1.1 靜態內存管理圖解

在這裏插入圖片描述

5.1.2 統一內存管理圖解

在這裏插入圖片描述

5.2 shuffle調優

spark.shuffle.file.buffer
默認值:32k
參數說明:該參數用於設置shuffle write task的BufferedOutputStream的buffer緩衝大小。將數據寫到磁盤文件之前,會先寫入buffer緩衝中,待緩衝寫滿之後,纔會溢寫到磁盤。
調優建議:如果作業可用的內存資源較爲充足的話,可以適當增加這個參數的大小(比如64k),從而減少shuffle write過程中溢寫磁盤文件的次數,也就可以減少磁盤IO次數,進而提升性能。在實踐中發現,合理調節該參數,性能會有1%~5%的提升。

spark.reducer.maxSizeInFlight
默認值:48m
參數說明:該參數用於設置shuffle read task的buffer緩衝大小,而這個buffer緩衝決定了每次能夠拉取多少數據。
調優建議:如果作業可用的內存資源較爲充足的話,可以適當增加這個參數的大小(比如96m),從而減少拉取數據的次數,也就可以減少網絡傳輸的次數,進而提升性能。在實踐中發現,合理調節該參數,性能會有1%~5%的提升。

spark.shuffle.io.maxRetries
默認值:3
參數說明:shuffle read task從shuffle write task所在節點拉取屬於自己的數據時,如果因爲網絡異常導致拉取失敗,是會自動進行重試的。該參數就代表了可以重試的最大次數。如果在指定次數之內拉取還是沒有成功,就可能會導致作業執行失敗。
調優建議:對於那些包含了特別耗時的shuffle操作的作業,建議增加重試最大次數(比如60次),以避免由於JVM的full gc或者網絡不穩定等因素導致的數據拉取失敗。在實踐中發現,對於針對超大數據量(數十億~上百億)的shuffle過程,調節該參數可以大幅度提升穩定性。
shuffle file not find taskScheduler不負責重試task,由DAGScheduler負責重試stage

spark.shuffle.io.retryWait
默認值:5s
參數說明:具體解釋同上,該參數代表了每次重試拉取數據的等待間隔,默認是5s。
調優建議:建議加大間隔時長(比如60s),以增加shuffle操作的穩定性。

spark.shuffle.memoryFraction
默認值:0.2
參數說明:該參數代表了Executor內存中,分配給shuffle read task進行聚合操作的內存比例,默認是20%。
調優建議:在資源參數調優中講解過這個參數。如果內存充足,而且很少使用持久化操作,建議調高這個比例,給shuffle read的聚合操作更多內存,以避免由於內存不足導致聚合過程中頻繁讀寫磁盤。在實踐中發現,合理調節該參數可以將性能提升10%左右。

spark.shuffle.manager
默認值:sort
參數說明:該參數用於設置ShuffleManager的類型。Spark 1.5以後,有三個可選項:hash、sort和tungsten-sort。HashShuffleManager是Spark 1.2以前的默認選項,但是Spark 1.2以及之後的版本默認都是SortShuffleManager了。tungsten-sort與sort類似,但是使用了tungsten計劃中的堆外內存管理機制,內存使用效率更高。
調優建議:由於SortShuffleManager默認會對數據進行排序,因此如果你的業務邏輯中需要該排序機制的話,則使用默認的SortShuffleManager就可以;而如果你的業務邏輯不需要對數據進行排序,那麼建議參考後面的幾個參數調優,通過bypass機制或優化的HashShuffleManager來避免排序操作,同時提供較好的磁盤讀寫性能。這裏要注意的是,tungsten-sort要慎用,因爲之前發現了一些相應的bug。

spark.shuffle.sort.bypassMergeThreshold
默認值:200
參數說明:當ShuffleManager爲SortShuffleManager時,如果shuffle read task的數量小於這個閾值(默認是200),則shuffle write過程中不會進行排序操作,而是直接按照未經優化的HashShuffleManager的方式去寫數據,但是最後會將每個task產生的所有臨時磁盤文件都合併成一個文件,並會創建單獨的索引文件。
調優建議:當你使用SortShuffleManager時,如果的確不需要排序操作,那麼建議將這個參數調大一些,大於shuffle read task的數量。那麼此時就會自動啓用bypass機制,map-side就不會進行排序了,減少了排序的性能開銷。但是這種方式下,依然會產生大量的磁盤文件,因此shuffle write性能有待提高。

如果你的Application在執行的過程中,出現了類似reduce OOM的錯誤,錯誤原因會有哪一些?
1、代碼不規範。。。
三種解決方案:
1、提高Executor的內存
2、提高shuffle聚合的內存比例
3、減少每次拉去的數據量

6. 磁盤小文件尋址

未完待續。。。。

7. MR的shuffle過程與Spark shuffle的不同之處

比較 不同點
spark shuffle與MR shuffle spark中HashShuffle的shuffle write中沒有分組和排序
SortShuffle普通機制與MR shuffle SortShuffle的內存是約等於5M且動態變化的,而MR的內存是固定100M
SortShuffle bypass機制與MR shuffle Spark中SortShuffle的bypass運行機制中沒有排序,Spark shuffle默認是SortShuffle的bypass運行機制,因爲它沒有排序和分組,所以這也是比MR快的原因之一
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章