Spark Shuffle的技術演進


在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
    1. Map side combine (if needed)
    2. Write to local output file
  • Shuffle Read
    1. Block fetch
    2. Reduce side combine
    3. 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到磁盤。
  • 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 取決于是否需要對最終結果進行排序
    

參考資料及推薦閱讀

  1. Spark 1.0之前Hash Based Shuffle的原理
  1. Spark 1.1時Sort Based Shuffle的資料
  1. Spark 1.2之前兩種Shuffle方式的分析和對比
  1. Spark 1.6之前三種Shuffle方式的分析和對比
  1. Spark 1.6之前Sort Based Shuffle的源碼和原理
  1. Spark 1.6之前Tungsten-sort Based Shuffle的原理
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容