Spark的Shuffle總結分析

說出你的願望吧發表於2020-02-15

前言

一、shuffle原理分析

1.1 shuffle概述

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

在MapReduce框架,Shuffle是連線Map和Reduce之間的橋樑,Map階段通過shuffle讀取資料並輸出到對應的Reduce,而Reduce階段負責從Map端拉取資料並進行計算。在整個shuffle過程中,往往伴隨著大量的磁碟和網路I/O。所以shuffle效能的高低也直接決定了整個程式的效能高低。而Spark也會有自己的shuffle實現過程。

1.2 Spark中的 shuffle 介紹

在DAG排程的過程中,Stage 階段的劃分是根據是否有shuffle過程,也就是存在 寬依賴 的時候,需要進行shuffle,這時候會將 job 劃分成多個Stage,每一個 Stage 內部有很多可以並行執行的 Task

stage與stage之間的過程就是 shuffle 階段,在 Spark 中,負責 shuffle 過程的執行、計算和處理的元件主要就是 ShuffleManager 。ShuffleManager 隨著Spark的發展有兩種實現的方式,分別為 HashShuffleManagerSortShuffleManager ,因此spark的Shuffle有 Hash ShuffleSort Shuffle 兩種。

1.3 HashShuffle機制

1.3.1 HashShuffle 的介紹

Spark 1.2 以前,預設的shuffle計算引擎是 HashShuffleManager

HashShuffleManager 有著一個非常嚴重的弊端,就是會產生大量的中間磁碟檔案,進而由大量的磁碟IO操作影響了效能。因此在Spark 1.2以後的版本中,預設的 ShuffleManager 改成了 SortShuffleManager

SortShuffleManager 相較於 HashShuffleManager 來說,有了一定的改進。主要就在於每個Task在進行shuffle操作時,雖然也會產生較多的臨時磁碟檔案,但是最後會將所有的臨時檔案合併(merge)成一個磁碟檔案,因此每個 Task 就只有一個磁碟檔案。在下一個 Stage 的shuffle read task拉取自己的資料時,只要根據索引讀取每個磁碟檔案中的部分資料即可。

Hash shuffle是不具有排序的Shuffle。

1.3.2 普通機制的Hash shuffle

HashShuffleManager的執行機制主要分成兩種:一種是 普通執行機制 ,另一種是 合併執行機制 ,而合併機制主要是通過複用buffer來優化Shuffle過程中產生的小檔案的數量。

先簡單說明一下情況。此時任務劃分為了兩個 Stage ,第一個 Stage 最上方有4個 MapTask , 而第二個 Stage 有3個 ReduceTask,但是如果我們現在的MapTask增多成1000個,那我們所產生的 block file 那不就有 MapTask*3 這麼多了,在這時大量的IO操作會造成很大的效能問題

1.3.3 普通機制的 Hash shuffle 的步驟詳細說明

這裡我們先明確一個假設前提:每個Executor只有1個CPU core,也就是說,無論這個Executor上分配多少個task執行緒,同一時間都只能執行一個task執行緒

圖中有3個ReduceTask,從ShuffleMapTask 開始那邊各自把自己進行 Hash 計算(分割槽器:hash/numReduce取模),分類出3個不同的類別,每個 ShuffleMapTask 都分成3種類別的資料,想把不同的資料匯聚然後計算出最終的結果,所以ReduceTask 會在屬於自己類別的資料收集過來,匯聚成一個同類別的大集合,每1個 ShuffleMapTask 輸出3份本地檔案,這裡有4個 ShuffleMapTask,所以總共輸出了4 x 3個分類檔案 = 12個本地小檔案。

Shuffle Write 階段:

主要就是在一個stage結束計算之後,為了下一個stage可以執行shuffle類的運算元(比如reduceByKey,groupByKey),而將每個task處理的資料按key進行分割槽。所謂 “分割槽”,就是對相同的key執行hash演算法,從而將相同key都寫入同一個磁碟檔案中,而每一個磁碟檔案都只屬於reduce端的stage的一個task。在將資料寫入磁碟之前,會先將資料寫入記憶體緩衝中,當記憶體緩衝填滿之後,才會溢寫到磁碟檔案中去。

那麼每個執行 Shuffle Write 的 Task,要為下一個 Stage 建立多少個磁碟檔案呢? 很簡單,下一個stage的task有多少個,當前stage的每個task就要建立多少份磁碟檔案。比如下一個stage總共有100個task,那麼當前stage的每個task都要建立100份磁碟檔案。如果當前stage有50個task,總共有10個Executor,每個Executor執行5個Task,那麼每個Executor上總共就要建立500個磁碟檔案,所有Executor上會建立5000個磁碟檔案。由此可見,未經優化的shuffle write操作所產生的磁碟檔案的數量是極其驚人的。

Shuffle Read 階段:

Shuffle Read,通常就是一個stage剛開始時要做的事情。此時該stage的每一個task就需要將上一個stage的計算結果中的所有相同key,從各個節點上通過網路都拉取到自己所在的節點上,然後進行key的聚合或連線等操作。由於shuffle write的過程中,task給Reduce端的stage的每個task都建立了一個磁碟檔案,因此shuffle read的過程中,每個task只要從上游stage的所有task所在節點上,拉取屬於自己的那一個磁碟檔案即可。

Shuffle Read的拉取過程是一邊拉取一邊進行聚合的。每個shuffle read task都會有一個自己的buffer緩衝,每次都只能拉取與buffer緩衝相同大小的資料,然後通過記憶體中的一個Map進行聚合等操作。聚合完一批資料後,再拉取下一批資料,並放到buffer緩衝中進行聚合操作。以此類推,直到最後將所有資料到拉取完,並得到最終的結果。

注意

  1. buffer起到的是快取作用,快取能夠加速寫磁碟,提高計算的效率,buffer的預設大小32k。

  2. 分割槽器:根據hash/numRedcue取模決定資料由幾個Reduce處理,也決定了寫入幾個buffer中

  3. block file:磁碟小檔案,從圖中我們可以知道磁碟小檔案的個數計算公式:block file=M*R 。 M為map task的數量,R為Reduce的數量,一般Reduce的數量等於buffer的數量,都是由分割槽器決定的

Hash shuffle普通機制的問題

  1. Shuffle階段在磁碟上會產生海量的小檔案,建立通訊和拉取資料的次數變多,此時會產生大量耗時低效的 IO 操作 (因為產生過多的小檔案)

  2. 可能導致 OOM,大量耗時低效的 IO 操作 ,導致寫磁碟時的物件過多,讀磁碟時候的物件也過多,這些物件儲存在堆記憶體中,會導致堆記憶體不足,相應會導致頻繁的GC,GC會導致OOM。由於記憶體中需要儲存海量檔案操作控制程式碼和臨時資訊,如果資料處理的規模比較龐大的話,記憶體不可承受,會出現 OOM 等問題

1.3.4 合併機制的Hash shuffle

合併機制就是複用buffer緩衝區,開啟合併機制的配置是spark.shuffle.consolidateFiles。該引數預設值為false,將其設定為true即可開啟優化機制。通常來說,如果我們使用HashShuffleManager,那麼都建議開啟這個選項。

這裡有6個這裡有6個shuffleMapTask,資料類別還是分成3種型別,因為Hash演算法會根據你的 Key 進行分類,在同一個程式中,無論是有多少過Task,都會把同樣的Key放在同一個Buffer裡,然後把Buffer中的資料寫入以Core數量為單位的本地檔案中,(一個Core只有一種型別的Key的資料),每1個Task所在的程式中,分別寫入共同程式中的3份本地檔案,這裡有6個shuffleMapTasks,所以總共輸出是 2個Cores x 3個分類檔案 = 6個本地小檔案。

此時block file = Core * R ,Core為CPU的核數,R為Reduce的數量,但是如果 Reducer 端的並行任務或者是資料分片過多的話則 Core * Reducer Task 依舊過大,也會產生很多小檔案。

1.4 Sort shuffle

SortShuffleManager的執行機制也是主要分成兩種,普通執行機制bypass執行機制

1.4.1 Sort shuffle 的普通機制

在該模式下,資料會先寫入一個資料結構,聚合運算元寫入 Map,一邊通過 Map 區域性聚合,一遍寫入記憶體。Join 運算元寫入 ArrayList 直接寫入 記憶體 中。然後需要判斷是否達到 閾值(5M),如果達到就會將記憶體資料結構的資料寫入到磁碟,清空記憶體資料結構。

在溢寫磁碟前,先根據 key 進行 排序,排序 過後的資料,會分批寫入到磁碟檔案中。預設批次為10000條,資料會以每批一萬條寫入到磁碟檔案。寫入磁碟檔案通過緩衝區溢寫的方式,每次溢寫都會產生一個磁碟檔案,也就是說一個task過程會產生多個臨時檔案。

最後在每個task中,將所有的臨時檔案合併,這就是 merge 過程,此過程將所有臨時檔案讀取出來,一次寫入到最終檔案。意味著一個task的所有資料都在這一個檔案中。同時單獨寫一份索引檔案,標識下游各個task的資料在檔案中的索引start offset和end offset(比如對於wordCount,下標從哪裡(start offset)到哪裡(end offset)是這個單詞)。

這個機制的好處:

  1. 小檔案明顯變少了,一個task只生成一個file檔案
  2. file檔案整體有序,加上索引檔案的輔助,查詢變快,雖然排序浪費一些效能,但是查詢變快很多

1.4.2 bypass模式的sortShuffle

bypass機制執行條件是shuffle map task數量小於spark.shuffle.sort.bypassMergeThreshold引數(預設值200)的值,且不是聚合類的shuffle運算元(比如reduceByKey)

在 shuffleMapTask 數量 小於預設值200 時,啟用bypass模式的 sortShuffle,並沒有進行sort,原因是資料量本身比較少,沒必要進行sort全排序,因為資料量少本身查詢速度就快,正好省了sort的那部分效能開銷。

1.5 使用到的引數

1.5.1 spark.shuffle.file.buffer

buffer大小預設是32K,為了減少磁碟溢寫的次數,可以適當調整這個數值的大小。降低磁碟IO

1.5.2 spark.reducer.MaxSizeFlight

ReduceTask 拉取資料量的大小,預設48M

1.5.3 spark.shuffle.memoryFraction

shuffle聚合記憶體的比例,佔用executor記憶體比例的大小

1.5.4 spark.shuffle.io.maxRetries

拉取資料重試次數,防止網路抖動帶來的影響

1.5.5 spark.shuffle.io.retryWait

調整到重試間隔時間,拉取失敗後多久才重新進行拉取

1.5.6 spark.shuffle.consolidateFiles

針對 HashShuffle 合併機制

1.5.7 spark.shuffle.sort.bypassMergeThreshold

SortShuffle bypass機制,預設200次

1.5.8 spark.sql.shuffle.partitions

預設200,shuffle時所使用到的分割槽數,也就是你生成的 part-00000,part-00001···最多也就只能 part-00199 了

finally

···

相關文章