文章目錄
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過程中產生大量磁盤小文件。
磁盤小文件過多帶來的問題?
- Write階段創建大量的寫文件對象
- read階段,來拉去數據時就要產生大量的網絡IO
- 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的運行機制主要分成兩種。
- 普通運行機制
- 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運行機制的觸發條件如下:
-
shuffle map task數量小於spark.shuffle.sort.bypassMergeThreshold參數的值。
-
不是聚合類的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快的原因之一 |