SparkSQL的自適應執行---Adaptive Execution

1 背景

本文介紹的 Adaptive Execution 將可以根據執行過程中的中間數據優化後續執行,從而提高整體執行效率。核心在於兩點

  • 執行計劃可動態調整

  • 調整的依據是中間結果的精確統計信息

2 動態設置 Shuffle Partition

2.1 Spark Shuffle 原理

 

 

如上圖所示,該 Shuffle 總共有 2 個 Mapper 與 5 個 Reducer。每個 Mapper 會按相同的規則(由 Partitioner 定義)將自己的數據分爲五份。每個 Reducer 從這兩個 Mapper 中拉取屬於自己的那一份數據。

2.2 原有 Shuffle 的問題

使用 Spark SQL 時,可通過 spark.sql.shuffle.partitions 指定 Shuffle 時 Partition 個數,也即 Reducer 個數

該參數決定了一個 Spark SQL Job 中包含的所有 Shuffle 的 Partition 個數。如下圖所示,當該參數值爲 3 時,所有 Shuffle 中 Reducer 個數都爲 3

這種方法有如下問題

  • Partition 個數不宜設置過大

  • Reducer(代指 Spark Shuffle 過程中執行 Shuffle Read 的 Task) 個數過多,每個 Reducer 處理的數據量過小。大量小 Task 造成不必要的 Task 調度開銷與可能的資源調度開銷(如果開啓了 Dynamic Allocation)

  • Reducer 個數過大,如果 Reducer 直接寫 HDFS 會生成大量小文件,從而造成大量 addBlock RPC,Name node 可能成爲瓶頸,並影響其它使用 HDFS 的應用

  • 過多 Reducer 寫小文件,會造成後面讀取這些小文件時產生大量 getBlock RPC,對 Name node 產生衝擊

  • Partition 個數不宜設置過小

  • 每個 Reducer 處理的數據量太大,Spill 到磁盤開銷增大

  • Reducer GC 時間增長

  • Reducer 如果寫 HDFS,每個 Reducer 寫入數據量較大,無法充分發揮並行處理優勢

  • 很難保證所有 Shuffle 都最優

  • 不同的 Shuffle 對應的數據量不一樣,因此最優的 Partition 個數也不一樣。使用統一的 Partition 個數很難保證所有 Shuffle 都最優

  • 定時任務不同時段數據量不一樣,相同的 Partition 數設置無法保證所有時間段執行時都最優

2.3 自動設置 Shuffle Partition 原理

如 Spark Shuffle 原理 一節圖中所示,Stage 1 的 5 個 Partition 數據量分別爲 60MB,40MB,1MB,2MB,50MB。其中 1MB 與 2MB 的 Partition 明顯過小(實際場景中,部分小 Partition 只有幾十 KB 及至幾十字節)

開啓 Adaptive Execution 後

  • Spark 在 Stage 0 的 Shuffle Write 結束後,根據各 Mapper 輸出,統計得到各 Partition 的數據量,即 60MB,40MB,1MB,2MB,50MB

  • 通過 ExchangeCoordinator 計算出合適的 post-shuffle Partition 個數(即 Reducer)個數(本例中 Reducer 個數設置爲 3)

  • 啓動相應個數的 Reducer 任務

每個 Reducer 讀取一個或多個 Shuffle Write Partition 數據(如下圖所示,Reducer 0 讀取 Partition 0,Reducer 1 讀取 Partition 1、2、3,Reducer 2 讀取 Partition 4)

 

 

三個 Reducer 這樣分配是因爲

  • targetPostShuffleInputSize 默認爲 64MB,每個 Reducer 讀取數據量不超過 64MB

  • 如果 Partition 0 與 Partition 2 結合,Partition 1 與 Partition 3 結合,雖然也都不超過 64 MB。但讀完 Partition 0 再讀 Partition 2,對於同一個 Mapper 而言,如果每個 Partition 數據比較少,跳着讀多個 Partition 相當於隨機讀,在 HDD 上性能不高

  • 目前的做法是隻結合相臨的 Partition,從而保證順序讀,提高磁盤 IO 性能

  • 該方案只會合併多個小的 Partition,不會將大的 Partition 拆分,因爲拆分過程需要引入一輪新的 Shuffle

  • 基於上面的原因,默認 Partition 個數(本例中爲 5)可以大一點,然後由 ExchangeCoordinator 合併。如果設置的 Partition 個數太小,Adaptive Execution 在此場景下無法發揮作用

由上圖可見,Reducer 1 從每個 Mapper 讀取 Partition 1、2、3 都有三根線,是因爲原來的 Shuffle 設計中,每個 Reducer 每次通過 Fetch 請求從一個特定 Mapper 讀數據時,只能讀一個 Partition 的數據。也即在上圖中,Reducer 1 讀取 Mapper 0 的數據,需要 3 輪 Fetch 請求。對於 Mapper 而言,需要讀三次磁盤,相當於隨機 IO。

爲了解決這個問題,Spark 新增接口,一次 Shuffle Read 可以讀多個 Partition 的數據。如下圖所示,Task 1 通過一輪請求即可同時讀取 Task 0 內 Partition 0、1 和 2 的數據,減少了網絡請求數量。同時 Mapper 0 一次性讀取並返回三個 Partition 的數據,相當於順序 IO,從而提升了性能。

由於 Adaptive Execution 的自動設置 Reducer 是由 ExchangeCoordinator 根據 Shuffle Write 統計信息決定的,因此即使在同一個 Job 中不同 Shuffle 的 Reducer 個數都可以不一樣,從而使得每次 Shuffle 都儘可能最優。

上文 原有 Shuffle 的問題 一節中的例子,在啓用 Adaptive Execution 後,三次 Shuffle 的 Reducer 個數從原來的全部爲 3 變爲 2、4、3。

2.4 使用與優化方法

可通過 spark.sql.adaptive.enabled=true 啓用 Adaptive Execution 從而啓用自動設置 Shuffle Reducer 這一特性

通過 spark.sql.adaptive.shuffle.targetPostShuffleInputSize 可設置每個 Reducer 讀取的目標數據量,其單位是字節,默認值爲 64 MB。上文例子中,如果將該值設置爲 50 MB,最終效果仍然如上文所示,而不會將 Partition 0 的 60MB 拆分。具體原因上文已說明

3 動態調整執行計劃

3.1 固定執行計劃的不足

在不開啓 Adaptive Execution 之前,執行計劃一旦確定,即使發現後續執行計劃可以優化,也不可更改。如下圖所示,SortMergJoin 的 Shuffle Write 結束後,發現 Join 一方的 Shuffle 輸出只有 46.9KB,仍然繼續執行 SortMergeJoin

 

此時完全可將 SortMergeJoin 變更爲 BroadcastJoin 從而提高整體執行效率。

3.2 SortMergeJoin 原理

SortMergeJoin 是常用的分佈式 Join 方式,它幾乎可使用於所有需要 Join 的場景。但有些場景下,它的性能並不是最好的。

SortMergeJoin 的原理如下圖所示

  • 將 Join 雙方以 Join Key 爲 Key 按照 HashPartitioner 分區,且保證分區數一致

  • Stage 0 與 Stage 1 的所有 Task 在 Shuffle Write 時,都將數據分爲 5 個 Partition,並且每個 Partition 內按 Join Key 排序

  • Stage 2 啓動 5 個 Task 分別去 Stage 0 與 Stage 1 中所有包含 Partition 分區數據的 Task 中取對應 Partition 的數據。(如果某個 Mapper 不包含該 Partition 的數據,則 Redcuer 無須向其發起讀取請求)。

  • Stage 2 的 Task 2 分別從 Stage 0 的 Task 0、1、2 中讀取 Partition 2 的數據,並且通過 MergeSort 對其進行排序

  • Stage 2 的 Task 2 分別從 Stage 1 的 Task 0、1 中讀取 Partition 2 的數據,且通過 MergeSort 對其進行排序

  • Stage 2 的 Task 2 在上述兩步 MergeSort 的同時,使用 SortMergeJoin 對二者進行 Join

3.3 BroadcastJoin 原理

當參與 Join 的一方足夠小,可全部置於 Executor 內存中時,可使用 Broadcast 機制將整個 RDD 數據廣播到每一個 Executor 中,該 Executor 上運行的所有 Task 皆可直接讀取其數據。(本文中,後續配圖,爲了方便展示,會將整個 RDD 的數據置於 Task 框內,而隱藏 Executor)

對於大 RDD,按正常方式,每個 Task 讀取並處理一個 Partition 的數據,同時讀取 Executor 內的廣播數據,該廣播數據包含了小 RDD 的全量數據,因此可直接與每個 Task 處理的大 RDD 的部分數據直接 Join

根據 Task 內具體的 Join 實現的不同,又可分爲 BroadcastHashJoin 與 BroadcastNestedLoopJoin。後文不區分這兩種實現,統稱爲 BroadcastJoin

與 SortMergeJoin 相比,BroadcastJoin 不需要 Shuffle,減少了 Shuffle 帶來的開銷,同時也避免了 Shuffle 帶來的數據傾斜,從而極大地提升了 Job 執行效率

同時,BroadcastJoin 帶來了廣播小 RDD 的開銷。另外,如果小 RDD 過大,無法存於 Executor 內存中,則無法使用 BroadcastJoin

對於基礎表的 Join,可在生成執行計劃前,直接通過 HDFS 獲取各表的大小,從而判斷是否適合使用 BroadcastJoin。但對於中間表的 Join,無法提前準確判斷中間表大小從而精確判斷是否適合使用 BroadcastJoin

《Spark SQL 性能優化再進一步 CBO 基於代價的優化》一文介紹的 CBO 可通過表的統計信息與各操作對數據統計信息的影響,推測出中間表的統計信息,但是該方法得到的統計信息不夠準確。同時該方法要求提前分析表,具有較大開銷

而開啓 Adaptive Execution 後,可直接根據 Shuffle Write 數據判斷是否適用 BroadcastJoin

3.4 動態調整執行計劃原理

如上文 SortMergeJoin 原理 中配圖所示,SortMergeJoin 需要先對 Stage 0 與 Stage 1 按同樣的 Partitioner 進行 Shuffle Write

Shuffle Write 結束後,可從每個 ShuffleMapTask 的 MapStatus 中統計得到按原計劃執行時 Stage 2 各 Partition 的數據量以及 Stage 2 需要讀取的總數據量。(一般來說,Partition 是 RDD 的屬性而非 Stage 的屬性,本文爲了方便,不區分 Stage 與 RDD。可以簡單認爲一個 Stage 只有一個 RDD,此時 Stage 與 RDD 在本文討論範圍內等價)

如果其中一個 Stage 的數據量較小,適合使用 BroadcastJoin,無須繼續執行 Stage 2 的 Shuffle Read。相反,可利用 Stage 0 與 Stage 1 的數據進行 BroadcastJoin,如下圖所示

具體做法是

  • 將 Stage 1 全部 Shuffle Write 結果廣播出去

  • 啓動 Stage 2,Partition 個數與 Stage 0 一樣,都爲 3

  • 每個 Stage 2 每個 Task 讀取 Stage 0 每個 Task 的 Shuffle Write 數據,同時與廣播得到的 Stage 1 的全量數據進行 Join

注:廣播數據存於每個 Executor 中,其上所有 Task 共享,無須爲每個 Task 廣播一份數據。上圖中,爲了更清晰展示爲什麼能夠直接 Join 而將 Stage 2 每個 Task 方框內都放置了一份 Stage 1 的全量數據

雖然 Shuffle Write 已完成,將後續的 SortMergeJoin 改爲 Broadcast 仍然能提升執行效率

  • SortMergeJoin 需要在 Shuffle Read 時對來自 Stage 0 與 Stage 1 的數據進行 Merge Sort,並且可能需要 Spill 到磁盤,開銷較大

  • SortMergeJoin 時,Stage 2 的所有 Task 需要取 Stage 0 與 Stage 1 的所有 Task 的輸出數據(如果有它要的數據 ),會造成大量的網絡連接。且當 Stage 2 的 Task 較多時,會造成大量的磁盤隨機讀操作,效率不高,且影響相同機器上其它 Job 的執行效率

  • SortMergeJoin 時,Stage 2 每個 Task 需要從幾乎所有 Stage 0 與 Stage 1 的 Task 取數據,無法很好利用 Locality

  • Stage 2 改用 Broadcast,每個 Task 直接讀取 Stage 0 的每個 Task 的數據(一對一),可很好利用 Locality 特性。最好在 Stage 0 使用的 Executor 上直接啓動 Stage 2 的 Task。如果 Stage 0 的 Shuffle Write 數據並未 Spill 而是在內存中,則 Stage 2 的 Task 可直接讀取內存中的數據,效率非常高。如果有 Spill,那可直接從本地文件中讀取數據,且是順序讀取,效率遠比通過網絡隨機讀數據效率高

3.5 使用與優化方法

該特性的使用方式如下

  • 當 spark.sql.adaptive.enabled 與 spark.sql.adaptive.join.enabled 都設置爲 true 時,開啓 Adaptive Execution 的動態調整 Join 功能

  • spark.sql.adaptiveBroadcastJoinThreshold 設置了 SortMergeJoin 轉 BroadcastJoin 的閾值。如果不設置該參數,該閾值與 spark.sql.autoBroadcastJoinThreshold 的值相等

  • 除了本文所述 SortMergeJoin 轉 BroadcastJoin,Adaptive Execution 還可提供其它 Join 優化策略。部分優化策略可能會需要增加 Shuffle。spark.sql.adaptive.allowAdditionalShuffle 參數決定了是否允許爲了優化 Join 而增加 Shuffle。其默認值爲 false

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章