spark2原理分析-shuffleWriter:SortShuffleWriter實現分析

概述

本文分析shuffleWriter的實現類:SortShuffleWriter的詳細實現。

ShuffleWriter抽象類

/**
 * Obtained inside a map task to write out records to the shuffle system.
 */
private[spark] abstract class ShuffleWriter[K, V] {
  /** Write a sequence of records to this task's output */
  @throws[IOException]
  def write(records: Iterator[Product2[K, V]]): Unit

  /** Close this writer, passing along whether the map completed */
  def stop(success: Boolean): Option[MapStatus]
}

通過代碼可知,實現該接口的類要完成的功能是:在map任務中獲取數據,並把數據寫入到shuffle系統。

ShuffleWriter的實現方式

從這篇文章《spark2原理分析—shuffle框架的實現概要分析》可知,ShuffleWriter的實現方式有三種:

  1. BypassMergeSortShuffleWriter

    使用這種shuffle writer的條件是:

    • (1) 沒有map端的聚合操作
    • (2) 分區數小於參數:spark.shuffle.sort.bypassMergeThreshold,默認是200
  2. UnsafeShuffleWriter

    使用這種shuffle writer的條件是:

    • (1) 序列化工具類支持對象的重定位
    • (2) 不需要在map端進行聚合操作
    • (3) 分區數不能大於:PackedRecordPointer.MAXIMUM_PARTITION_ID + 1
  3. SortShuffleWriter

    若以上兩種shuffle writer都不能選擇,則使用該shuffle writer類。
    這也是相對比較常用的一種shuffle writer。

SortShuffleWriter過程

函數調用

SortShuffleWriter.write()
    ->sorter = new ExternalSorter
    ->sorter.insertAll()
        if (shouldCombine):
        -> PartitionedAppendOnlyMap.changeValue
        -> sorter.maybeSpillCollection()
        else:
        -> PartitionedPairBuffer().insert()
        -> sorter.maybeSpillCollection()
    ->sorter.writePartitionedFile()
    ->shuffleBlockResolver.writeIndexFileAndCommit()

shuffle writer的實現要點

  • 寫入時先會創建一個ExternalSorter對象:sort,寫入shuffle數據時,通過該對象的insertAll函數進行。
  • insertAll函數會先把數據寫入到一個緩衝中,該緩存是一個Array,該Array可以動態擴容,擴容時先創建一個更大的Array然後把數據複製到新的Array中。
  • 若使用combineKey等算子,或者在map端發生了聚合,將會使用PartitionedAppendOnlyMap這個buffer來緩存寫入的數據。
  • 若沒有使用聚合算子,則通過PartitionedPairBuffer來緩存寫入數據。
  • 當緩衝區的數據達到某個閾值時,在insertAll中會判斷是否應該把數據寫入磁盤。若需要,則調用sort對象的writePartitionedFile來把數據寫入文件。注意:這裏會把某個節點處理的所有分區數據寫入到一個文件中。
  • 接下來調用shuffleBlockResolver.writeIndexFileAndCommit來對寫入的數據塊創建索引(若索引文件已創建則不需要創建,否則需要創建索引文件)。

外部排序器:ExternalSorter

實現說明

  • 該類會反覆填充數據的緩衝區,若想通過key來進行聚合操作,則使用:PartitionedAppendOnlyMap來緩存數據。若不對數據進行聚合,則使用:PartitionedPairBuffer來緩存數據。在緩存區中,ExternalSorter會按分區ID對元素進行排序,然後也可以按key進行排序。 爲避免每個key多次調用分區程序,將分區ID存儲在每條記錄處。
  • 當每個緩衝區達到ExternalSorter規定的內存限制時,會將其寫出到文件中。 如果想要進行聚合,則該文件首先按分區ID排序,並且可能按key或key的哈希值進行排序。對於每個文件,ExternalSorter會跟蹤內存中每個分區中有多少對象,因此不必把每個元素寫出分區ID。
  • 當用戶請求迭代器或文件輸出時,使用上面定義的相同排序順序合併寫出的文件以及任何剩餘的內存數據(除非禁用排序和聚合)。如果我們需要按key聚合,要麼使用排序參數的總排序,要麼讀取具有相同哈希值的鍵key,並將它們相互比較以獲得合併值的一致性。
  • 用戶期望在調用stop()函數時,刪除所有中間的臨時文件。

ExternalSorter初始化

在ExternalSorter對象進行初始化時,會獲取一些環境配置,和輔助的類對象。具體的初始化過程如下:

  • 獲取SparkEnv的配置數據,直接通過conf = SparkEnv.get.conf進行獲取。
  • 獲取分區數:numPartitions = partitioner.map(_.numPartitions).getOrElse(1)
  • 獲取數據塊管理器:blockManager = SparkEnv.get.blockManager
  • 獲取磁盤數據塊管理器:diskBlockManager = blockManager.diskBlockManager
  • 獲取序列化管理器:serializerManager = SparkEnv.get.serializerManager
  • 實例化序列化管理器:serInstance = serializer.newInstance()
  • 獲取配置參數的值:spark.shuffle.file.buffer
    • 說明,一般情況下,該參數表示:每個shuffle文件輸出流的內存緩衝區大小(K)。默認值是:32K
    • 作用:通過這些緩衝區,減少了在創建中間shuffle文件時進行的磁盤I/O和系統調用的次數。
  • 獲取配置參數:spark.shuffle.spill.batchSize的值:
    • 說明,該參數表示:每次從序列化器批量讀/寫對象的個數。該參數的默認值是:10000
    • 對象是批量寫入的,每個批處理使用自己的序列化流。這樣減少了反序列化流時構造的消耗。
    • 注意:在實際使用時,該值不能設置的太低。若將此設置得太低會導致序列化時過度複製。
  • 創建兩個內存緩存

    這兩個緩存區分別用於有聚合和無聚合的情況。
    // 有聚合操作時使用
    @volatile private var map = new PartitionedAppendOnlyMap[K, C]
    // 無聚合操作時使用
    @volatile private var buffer = new PartitionedPairBuffer[K, C]
    

ExternalSorter的insertAll()的實現

insertAll函數主要負責把數據放到shuffle writer的寫入緩衝區中。上面已經分析過了,在shuffle writer中,buffer分爲兩種:

  • map端無聚合操作,使用:PartitionedPairBuffer[K,C]
  • map端有聚合操作,使用:PartitionedAppendOnlyMap[K,C]

並且,在使用這兩種buffer時,會按分區ID作爲key值。

該函數的詳細實現步驟如下:

  • 在aggregator對象中獲取設置初始值和更新值的回調函數
  • 遍歷記錄,若使用了聚合操作,則把記錄數據添加到:PartitionedAppendOnlyMap中。若沒有聚合操作,則把記錄添加到:PartitionedPairBuffer緩衝區中。在寫入緩衝區時,都以分區ID作爲key。
  • 調用maybeSpillCollection()函數。該函數會調用maybeSpill函數,來判斷緩衝區目前的使用情況,並根據使用情況把數據刷到磁盤,每次寫入磁盤時都會生成一個臨時文件,這樣可能會形成多個臨時文件,這些臨時文件會被記錄在定義的變量spillfile中,這些文件會在最後進行合併。
  • maybeSpill()函數的操作如下:
    • 判斷讀取的元素個數是否是32的倍數(內存對齊) 且 目前的內存使用量是否超過閾值,即參數:spark.shuffle.spill.initialMemoryThreshold"的值。默認是5M。
    • 若以上條件滿足,則會申請:目前使用內存的2倍內存-閾值 這麼大的內存。若申請失敗,則直接把數據寫入到磁盤,並釋放內存空間。
    • 若目前的內存佔有量大於可用內存的閾值,就應該把數據寫入到磁盤上,此時會shouldSpill變量設置爲true。
    • 接下來會調用spill()函數把數據寫入到磁盤。
    • 最後會調用releaseMemory()來釋放內存。

總結

總的來說:

  • SortShuffleWriter會先把數據先寫入到內存中,並會嘗試擴展內存大小,若內存不足,則把數據持久化到磁盤上。
  • SortShuffleWriter在把數據寫入磁盤時,會按分區ID進行合併,並對key進行排序,然後寫入到該分區的臨時文件中。
  • SortShuffleWriter最後會把前面寫的分區臨時文件進行合併,合併成一個文件,也就是說,會在map操作結束時把各個分區文件合併成一個文件。這樣做可以有效的減少文件個數,和爲了維護這些文件而產生的資源消耗。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章