在Spark或Hadoop MapReduce的分布式計算框架中,數據被按照key分成一塊一塊的分區,打散分布在集群中各個節點的物理存儲或內存空間中,每個計算任務一次處理一個分區,但map端和reduce端的計算任務并非按照一種方式對相同的分區進行計算,例如,當需要對數據進行排序時,就需要將key相同的數據分布到同一個分區中,原分區的數據需要被打亂重組,這個按照一定的規則對數據重新分區的過程就是Shuffle(洗牌)。
Spark Shuffle的兩階段
對于Spark來講,一些Transformation或Action算子會讓RDD產生寬依賴,即parent RDD中的每個Partition被child RDD中的多個Partition使用,這時便需要進行Shuffle,根據Record的key對parent RDD進行重新分區。如果對這些概念還有一些疑問,可以參考我的另一篇文章《Spark基本概念快速入門》。
以Shuffle為邊界,Spark將一個Job劃分為不同的Stage,這些Stage構成了一個大粒度的DAG。Spark的Shuffle分為Write和Read兩個階段,分屬于兩個不同的Stage,前者是Parent Stage的最后一步,后者是Child Stage的第一步。如下圖所示:
執行Shuffle的主體是Stage中的并發任務,這些任務分ShuffleMapTask和ResultTask兩種,ShuffleMapTask要進行Shuffle,ResultTask負責返回計算結果,一個Job中只有最后的Stage采用ResultTask,其他的均為ShuffleMapTask。如果要按照map端和reduce端來分析的話,ShuffleMapTask可以即是map端任務,又是reduce端任務,因為Spark中的Shuffle是可以串行的;ResultTask則只能充當reduce端任務的角色。
我把Spark Shuffle的流程簡單抽象為以下幾步以便于理解:
-
Shuffle Write
- Map side combine (if needed)
- Write to local output file
-
Shuffle Read
- Block fetch
- Reduce side combine
- Sort (if needed)
Write階段發生于ShuffleMapTask對該Stage的最后一個RDD完成了map端的計算之后,首先會判斷是否需要對計算結果進行聚合,然后將最終結果按照不同的reduce端進行區分,寫入當前節點的本地磁盤。
Read階段開始于reduce端的任務讀取ShuffledRDD之時,首先通過遠程或本地數據拉取獲得Write階段各個節點中屬于當前任務的數據,根據數據的Key進行聚合,然后判斷是否需要排序,最后生成新的RDD。
Spark Shuffle具體實現的演進
在具體的實現上,Shuffle經歷了Hash、Sort、Tungsten-Sort三階段:
-
Spark 0.8及以前
Hash Based Shuffle
在Shuffle Write過程按照Hash的方式重組Partition的數據,不進行排序。每個map端的任務為每個reduce端的Task生成一個文件,通常會產生大量的文件(即對應為M*R
個中間文件,其中M表示map端的Task個數,R表示reduce端的Task個數),伴隨大量的隨機磁盤IO操作與大量的內存開銷。
Shuffle Read過程如果有combiner操作,那么它會把拉到的數據保存在一個Spark封裝的哈希表(AppendOnlyMap)中進行合并。
在代碼結構上:- org.apache.spark.storage.ShuffleBlockManager負責Shuffle Write
- org.apache.spark.BlockStoreShuffleFetcher負責Shuffle Read
- org.apache.spark.Aggregator負責combine,依賴于AppendOnlyMap
Spark 0.8.1
為Hash Based Shuffle引入File Consolidation機制
通過文件合并,中間文件的生成方式修改為每個執行單位(一個Executor中的執行單位等于Core的個數除以每個Task所需的Core數)為每個reduce端的任務生成一個文件。最終可以將文件個數從M*R
修改為E*C/T*R
,其中,E表示Executor的個數,C表示每個Executor中可用Core的個數,T表示Task所分配的Core的個數。
是否采用Consolidate機制,需要配置spark.shuffle.consolidateFiles
參數Spark 0.9
引入ExternalAppendOnlyMap
在combine的時候,可以將數據spill到磁盤,然后通過堆排序merge(可以參考這篇文章,了解其具體實現)-
Spark 1.1
引入Sort Based Shuffle,但默認仍為Hash Based Shuffle
在Sort Based Shuffle的Shuffle Write階段,map端的任務會按照Partition id以及key對記錄進行排序。同時將全部結果寫到一個數據文件中,同時生成一個索引文件,reduce端的Task可以通過該索引文件獲取相關的數據。
在代碼結構上:- 從以前的ShuffleBlockManager中分離出ShuffleManager來專門管理Shuffle Writer和Shuffle Reader。兩種Shuffle方式分別對應
org.apache.spark.shuffle.hash.HashShuffleManager和
org.apache.spark.shuffle.sort.SortShuffleManager,
可通過spark.shuffle.manager
參數配置。兩種Shuffle方式有各自的ShuffleWriter:org.apache.spark.shuffle.hash.HashShuffle和org.apache.spark.shuffle.sort.SortShuffleWriter;但共用一個ShuffleReader,即org.apache.spark.shuffle.hash.HashShuffleReader。 - org.apache.spark.util.collection.ExternalSorter實現排序功能。可通過對
spark.shuffle.spill
參數配置,決定是否可以在排序時將臨時數據Spill到磁盤。
- 從以前的ShuffleBlockManager中分離出ShuffleManager來專門管理Shuffle Writer和Shuffle Reader。兩種Shuffle方式分別對應
Spark 1.2
默認的Shuffle方式改為Sort Based Shuffle-
Spark 1.4
引入Tungsten-Sort Based Shuffle
將數據記錄用序列化的二進制方式存儲,把排序轉化成指針數組的排序,引入堆外內存空間和新的內存管理模型,這些技術決定了使用Tungsten-Sort要符合一些嚴格的限制,比如Shuffle dependency不能帶有aggregation、輸出不能排序等。由于堆外內存的管理基于JDK Sun Unsafe API,故Tungsten-Sort Based Shuffle也被稱為Unsafe Shuffle。
在代碼層面:- 新增org.apache.spark.shuffle.unsafe.UnsafeShuffleManager
- 新增org.apache.spark.shuffle.unsafe.UnsafeShuffleWriter(用java實現)
- ShuffleReader復用HashShuffleReader
-
Spark 1.6
Tungsten-sort并入Sort Based Shuffle
由SortShuffleManager自動判斷選擇最佳Shuffle方式,如果檢測到滿足Tungsten-sort條件會自動采用Tungsten-sort Based Shuffle,否則采用Sort Based Shuffle。
在代碼方面:- UnsafeShuffleManager合并到SortShuffleManager
- HashShuffleReader 重命名為BlockStoreShuffleReader,Sort Based Shuffle和Hash Based Shuffle仍共用ShuffleReader。
Spark 2.0
Hash Based Shuffle退出歷史舞臺
從此Spark只有Sort Based Shuffle。
Spark Shuffle源碼結構
這里以最新的Spark 2.1為例簡單介紹一下Spark Shuffle相關部分的代碼結構
- Shuffle Write
- ShuffleWriter的入口鏈路
org.apache.spark.scheduler.ShuffleMapTask#runTask ---> org.apache.spark.shuffle.sort.SortShuffleManager#getWriter ---> org.apache.spark.shuffle.sort.SortShuffleWriter#write(如果是普通sort) ---> org.apache.spark.shuffle.sort.UnsafeShuffleWriter#write (如果是Tungsten-sort)
- SortShuffleWriter的主要依賴
org.apache.spark.util.collection.ExternalSorter 負責按照(partition id, key)排序,如果需要Map side combine,需要提供aggregator ---> org.apache.spark.util.collection.PartitionedAppendOnlyMap
- UnsafeShuffleWriter的主要依賴
org.apache.spark.shuffle.sort.ShuffleExternalSorter (Java實現)
- Shuffle Read
- ShuffleReader的入口鏈路
org.apache.spark.rdd.ShuffledRDD#compute ---> org.apache.spark.shuffle.sort.SortShuffleManager#getReader ---> org.apache.spark.shuffle.BlockStoreShuffleReader#read
- ShuffleReader主要依賴
org.apache.spark.Aggregator 負責combine ---> org.apache.spark.util.collection.ExternalAppendOnlyMap org.apache.spark.util.collection.ExternalSorter 取決于是否需要對最終結果進行排序
參考資料及推薦閱讀
- Spark 1.0之前Hash Based Shuffle的原理
- Spark 1.1時Sort Based Shuffle的資料
- Spark 1.2之前兩種Shuffle方式的分析和對比
- Spark 1.6之前三種Shuffle方式的分析和對比
- Spark 1.6之前Sort Based Shuffle的源碼和原理
- Spark Core源碼解讀(十)-shuffle write
- Spark Core源碼解讀(十二)-shuffle read
- Spark Sort Based Shuffle內存分析
- Shuffle的框架之框架演進與框架內核
- Spark 1.6之前Tungsten-sort Based Shuffle的原理