Spark深入淺出之剖析 Spark Shuffle 原理

Shuffle 一般被翻譯爲數據混洗,是類 MapReduce 分佈式計算框架獨有的機制,也是這類分佈式計算框架最重要的執行機制。本課時主要從兩個層面講解 Shuffle,主要分爲:

  • 邏輯層面、
  • 物理層面。

邏輯層面主要從 RDD 的血統機制出發,從 DAG 的角度來講解 Shuffle,另外也會講解 Spark 容錯機制,而物理層面是從執行角度來剖析 Shuffle 是如何發生的。

RDD 血統與 Spark 容錯

在 DAG 中,最初的 RDD 被稱爲基礎 RDD,後續生成的 RDD 都是由算子以及依賴關係生成的,也就是說,無論哪個 RDD 出現問題,都可以由這種依賴關係重新計算而成。這種依賴關係被稱爲 RDD 血統(lineage)。血統的表現形式主要分爲寬依賴(wide dependency)與窄依賴(narrow dependency),如下圖所示:

窄依賴的準確定義是:子 RDD 中的分區與父 RDD 中的分區只存在一對一的映射關係,而寬依賴則是子 RDD 中的分區與父 RDD 中的分區存在一對多的映射關係,那麼從這個角度來說,map、 filter、 union 等就是窄依賴,而 groupByKey、 coGroup 就是典型的寬依賴,如下圖所示:

寬依賴還有個名字,叫 Shuffle 依賴,也就是說寬依賴必然會發生 Shuffle 操作,在前面也提到過 Shuffle 也是劃分 Stage 的依據。而窄依賴由於不需要發生 Shuffle,所有計算都是在分區所在節點完成,它類似於 MapReduce 中的 ChainMapper。所以說,在你自己的 DAG 中,如果你選取的算子形成了寬依賴,那麼就一定會觸發 Shuffle。

當 RDD 中的某個分區出現故障,那麼只需要按照這種依賴關係重新計算即可,窄依賴最簡單,只涉及某個節點內的計算,而寬依賴,則會按照依賴關係由父分區計算而得到,如下圖所示:

如果 P1_0 分區發生故障,那麼按照依賴關係,則需要 P0_0 與 P0_1 的分區重算,如果 P0_0與 P0_1 沒有持久化,就會不斷回溯,直到找到存在的父分區爲止。當計算邏輯複雜時,就會引起依賴鏈過長,這樣重算的代價會極其高昂,所以用戶可以在計算過程中,適時調用 RDD 的 checkpoint 方法,保存當前算好的中間結果,這樣依賴鏈就會大大縮短。RDD 的血統機制就是 RDD 的容錯機制。

spark 的容錯主要分爲資源管理平臺的容錯和 Spark 應用的容錯, Spark 應用是基於資源管理平臺運行,所以資源管理平臺的容錯也是 Spark 容錯的一部分,如 YARN 的 ResourceManager HA 機制。在 Spark 應用執行的過程中,可能會遇到以下幾種失敗的情況:

  • Driver 報錯;
  • Executor 報錯;
  • Task 執行失敗。

Driver 執行失敗是 Spark 應用最嚴重的一種情況,標誌整個作業徹底執行失敗,需要開發人員手動重啓 Driver;Executor 報錯通常是因爲 Executor 所在的機器故障導致,這時 Driver 會將執行失敗的 Task 調度到另一個 Executor 繼續執行,重新執行的 Task 會根據 RDD 的依賴關係繼續計算,並將報錯的 Executor 從可用 Executor 的列表中去掉;Spark 會對執行失敗的 Task 進行重試,重試 3 次後若仍然失敗會導致整個作業失敗。在這個過程中,Task 的數據恢復和重新執行都用到了 RDD 的血統機制。

Spark Shuffle

很多算子都會引起 RDD 中的數據進行重分區,新的分區被創建,舊的分區被合併或者被打碎,在重分區的過程中,如果數據發生了跨節點移動,就被稱爲 Shuffle,在 Spark 中, Shuffle 負責將 Map 端(這裏的 Map 端可以理解爲寬依賴的左側)的處理的中間結果傳輸到 Reduce 端供 Reduce 端聚合(這裏的 Reduce 端可以理解爲寬依賴的右側),它是 MapReduce 類型計算框架中最重要的概念,同時也是很消耗性能的步驟。Shuffle 體現了從函數式編程接口到分佈式計算框架的實現。與 MapReduce 的 Sort-based Shuffle 不同,Spark 對 Shuffle 的實現方式有兩種:Hash Shuffle 與 Sort-based Shuffle,這其實是一個優化的過程。在較老的版本中,Spark Shuffle 的方式可以通過 spark.shuffle.manager 配置項進行配置,而在最新的 Spark 版本中,已經去掉了該配置,統一稱爲 Sort-based Shuffle。

Hash Shuffle

在 Spark 1.6.3 之前, Hash Shuffle 都是 Spark Shuffle 的解決方案之一。 Shuffle 的過程一般分爲兩個部分:Shuffle Write 和 Shuffle Fetch,前者是 Map 任務劃分分區、輸出中間結果,而後者則是 Reduce 任務獲取到的這些中間結果。Hash Shuffle 的過程如下圖所示:

在圖中,Shuffle Write 發生在一個節點上,該節點用來執行 Shuffle 任務的 CPU 核數爲 2,每個核可以同時執行兩個任務,每個任務輸出的分區數與 Reducer(這裏的 Reducer 指的是 Reduce 端的 Executor)數相同,即爲 3,每個分區都有一個緩衝區(bucket)用來接收結果,每個緩衝區的大小由配置 spark.shuffle.file.buffer.kb 決定。這樣每個緩衝區寫滿後,就會輸出到一個文件段(filesegment),而 Reducer 就會去相應的節點拉取文件。這樣的實現很簡單,但是問題也很明顯。主要有兩個:

  1. 生成的中間結果文件數太大。理論上,每個 Shuffle 任務輸出會產生 R 個文件( R爲Reducer 的個數),而 Shuffle 任務的個數往往由 Map 任務個數 M 決定,所以總共會生成 M * R 箇中間結果文件,而往往在一個作業中 M 和 R 都是很大的數字,在大型作業中,經常會出現文件句柄數突破操作系統限制。
  2. 緩衝區佔用內存空間過大。單節點在執行 Shuffle 任務時緩存區大小消耗爲 m * R * spark.shuffle.file.buffer.kb,m 爲該節點運行的 Shuffle 任務數,如果一個核可以執行一個任務,m 就與 CPU 核數相等。這對於動輒有 32、64 物理核的服務器來說,是比不小的內存開銷。

爲了解決第一個問題, Spark 推出過 File Consolidation 機制,旨在通過共用輸出文件以降低文件數,如下圖所示:

每當 Shuffle 任務輸出時,同一個 CPU 核心處理的 Map 任務的中間結果會輸出到同分區的一個文件中,然後 Reducer 只需一次性將整個文件拿到即可。這樣,Shuffle 產生的文件數爲 C(CPU 核數)* R。 Spark 的 FileConsolidation 機制默認開啓,可以通過 spark.shuffle.consolidateFiles 配置項進行配置。

Sort-based Shuffle

在 Spark 先後引入了 Hash Shuffle 與 FileConsolidation 後,還是無法根本解決中間文件數太大的問題,所以 Spark 在 1.2 之後又推出了與 MapReduce 一樣(你可以參照《Hadoop 海量數據處理》(第 2 版)的 Shuffle 相關章節)的 Shuffle 機制: Sort-based Shuffle,才真正解決了 Shuffle 的問題,再加上 Tungsten 計劃的優化, Spark 的 Sort-based Shuffle 比 MapReduce 的 Sort-based Shuffle 青出於藍。如下圖所示:

每個 Map 任務會最後只會輸出兩個文件(其中一個是索引文件),其中間過程採用的是與 MapReduce 一樣的歸併排序,但是會用索引文件記錄每個分區的偏移量,輸出完成後,Reducer 會根據索引文件得到屬於自己的分區,在這種情況下,Shuffle 產生的中間結果文件數爲 2 * M(M 爲 Map 任務數)。

在基於排序的 Shuffle 中, Spark 還提供了一種折中方案——Bypass Sort-based Shuffle,當 Reduce 任務小於 spark.shuffle.sort.bypassMergeThreshold 配置(默認 200)時,Spark Shuffle 開始按照 Hash Shuffle 的方式處理數據,而不用進行歸併排序,只是在 Shuffle Write 步驟的最後,將其合併爲 1 個文件,並生成索引文件。這樣實際上還是會生成大量的中間文件,只是最後合併爲 1 個文件並省去排序所帶來的開銷,該方案的準確說法是 Hash Shuffle 的Shuffle Fetch 優化版。

Spark 在1.5 版本時開始了 Tungsten 計劃,也在 1.5.0、 1.5.1、 1.5.2 的時候推出了一種 tungsten-sort 的選項,這是一種成果應用,類似於一種實驗,該類型 Shuffle 本質上還是給予排序的 Shuffle,只是用 UnsafeShuffleWriter 進行 Map 任務輸出,並採用了要在後面介紹的 BytesToBytesMap 相似的數據結構,把對數據的排序轉化爲對指針數組的排序,能夠基於二進制數據進行操作,對 GC 有了很大提升。但是該方案對數據量有一些限制,隨着 Tungsten 計劃的逐漸成熟,該方案在 1.6 就消失不見了。

從上面整個過程的變化來看, Spark Shuffle 也是經過了一段時間才趨於成熟和穩定,這也正像學習的過程,不用一蹴而就,貴在堅持。

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