《spark streaming源碼七》Receiver, ReceiverSupervisor, BlockGenerator, ReceivedBlockHandler 詳解

轉載自https://github.com/lw-lin/CoolplaySpark

本系列內容適用範圍:

* 2017.07.11 update, Spark 2.2 全系列 √ (已發佈:2.2.0)
* 2017.10.02 update, Spark 2.1 全系列 √ (已發佈:2.1.0, 2.1.1, 2.1.2)
* 2016.11.14 update, Spark 2.0 全系列 √ (已發佈:2.0.0, 2.0.1, 2.0.2)

目錄

 

引言

Receiver 詳解

ReceiverSupervisor 詳解

ReceivedBlockHandler 詳解

(a) BlockManagerBasedBlockHandler 實現

(b) WriteAheadLogBasedBlockHandler 實現

BlockGenerator 詳解

總結


引言

我們在前面 [Spark Streaming 實現思路與模塊概述](Spark Streaming 實現思路與模塊概述.md) 中分析過,Spark Streaming 在程序剛開始運行時:

  • (1) 由 Receiver 的總指揮 ReceiverTracker 分發多個 job(每個 job 有 1 個 task),到多個 executor 上分別啓動 ReceiverSupervisor 實例;

  • (2) 每個 ReceiverSupervisor 啓動後將馬上生成一個用戶提供的 Receiver 實現的實例 —— 該 Receiver 實現可以持續產生或者持續接收系統外數據,比如 TwitterReceiver 可以實時爬取 twitter 數據 —— 並在 Receiver 實例生成後調用 Receiver.onStart()

 

ReceiverSupervisor 的全限定名是:org.apache.spark.streaming.receiver.ReceiverSupervisor
Receiver           的全限定名是:org.apache.spark.streaming.receiver.Receiver

 

(1)(2) 的過程由上圖所示,這時 Receiver 啓動工作已運行完畢。

接下來 ReceiverSupervisor 將在 executor 端作爲的主要角色,並且:

  • (3) Receiver 在 onStart() 啓動後,就將持續不斷地接收外界數據,並持續交給 ReceiverSupervisor 進行數據轉儲;

  • (4) ReceiverSupervisor 持續不斷地接收到 Receiver 轉來的數據:

    • 如果數據很細小,就需要 BlockGenerator 攢多條數據成一塊(4a)、然後再成塊存儲(4b 或 4c)

    • 反之就不用攢,直接成塊存儲(4b 或 4c)

    • 這裏 Spark Streaming 目前支持兩種成塊存儲方式,一種是由 blockManagerskManagerBasedBlockHandler 直接存到 executor 的內存或硬盤,另一種由 WriteAheadLogBasedBlockHandler 是同時寫 WAL(4c) 和 executor 的內存或硬盤

  • (5) 每次成塊在 executor 存儲完畢後,ReceiverSupervisor 就會及時上報塊數據的 meta 信息給 driver 端的 ReceiverTracker;這裏的 meta 信息包括數據的標識 id,數據的位置,數據的條數,數據的大小等信息。

  • (6) ReceiverTracker 再將收到的塊數據 meta 信息直接轉給自己的成員 ReceivedBlockTracker,由 ReceivedBlockTracker 專門管理收到的塊數據 meta 信息。

BlockGenerator                 的全限定名是:org.apache.spark.streaming.receiver.BlockGenerator
BlockManagerBasedBlockHandler  的全限定名是:org.apache.spark.streaming.receiver.BlockManagerBasedBlockHandler
WriteAheadLogBasedBlockHandler 的全限定名是:org.apache.spark.streaming.receiver.WriteAheadLogBasedBlockHandler
ReceivedBlockTracker           的全限定名是:org.apache.spark.streaming.scheduler.ReceivedBlockTracker
ReceiverInputDStream           的全限定名是:org.apache.spark.streaming.dstream.ReceiverInputDStream

這裏 (3)(4)(5)(6) 的過程是一直持續不斷地發生的,我們也將其在上圖裏標識出來。

後續在 driver 端,就由 ReceiverInputDStream 在每個 batch 去檢查 ReceiverTracker 收到的塊數據 meta 信息,界定哪些新數據需要在本 batch 內處理,然後生成相應的 RDD 實例去處理這些塊數據。

下面我們來詳解 Receiver, ReceiverSupervisor, BlockGenerator 這三個類。

Receiver 詳解

Receiver 是一個 abstract 的基類:

// 來自 Receiver

abstract class Receiver[T](val storageLevel: StorageLevel) extends Serializable {
  // 需要子類實現
  def onStart()
  def onStop()
  
  // 基類實現,供子類調用
  def store(dataItem: T) {...}                  // 【存儲單條小數據】
  def store(dataBuffer: ArrayBuffer[T]) {...}   // 【存儲數組形式的塊數據】
  def store(dataIterator: Iterator[T]) {...}    // 【存儲 iterator 形式的塊數據】
  def store(bytes: ByteBuffer) {...}            // 【存儲 ByteBuffer 形式的塊數據】
  
  ...
}

這裏需要 Receiver 子類具體實現的是,onStart() 和 onStop() 方法。onStart() 是在 executor 端被 ReceiverSupervisor調用的,而且 onStart() 的實現應該很快就能返回,不要寫成阻塞式的。

比如,Spark Streaming 自帶的 SocketReceiver 的 onStart() 實現如下:

// 來自 SocketReceiver

def onStart() {
  new Thread("Socket Receiver") {
    setDaemon(true)
    override def run() { receive() }
  }.start()  // 【僅新拉起了一個線程來接收數據】
  // 【onStart() 方法很快就返回了】
}

另外的 onStop() 實現,就是在 Receiver 被關閉時調用了,可以做一些 close 工作。

我們看當 Receiver 真正啓動起來後,可以開始產生或者接收數據了,那接收到的數據該怎麼存到 Spark Streaming 裏?

答案很簡單,就是直接調用 store() 方法即可。Receiver 基類提供了 4 種簽名的 store() 方法,分別可用於存儲:

  • (a) 單條小數據
  • (b) 數組形式的塊數據
  • (c) iterator 形式的塊數據
  • (d) ByteBuffer 形式的塊數據

這 4 種簽名的 store() 的實現都是直接將數據轉給 ReceiverSupervisor,由 ReceiverSupervisor 來具體負責存儲。

所以,一個具體的 Receiver 子類實現,只要在 onStart() 裏新拉起數據接收線程,並在接收到數據時 store() 到 Spark Streamimg 框架就可以了。

ReceiverSupervisor 詳解

我們在 [Receiver 分發詳解](3.1 Receiver 分發詳解.md) 裏分析過,在 executor 端,分發 Receiver 的 Job 的 Task 執行的實現是:

(iterator: Iterator[Receiver[_]]) => {
  ...
  val receiver = iterator.next()
  assert(iterator.hasNext == false)
  // 【ReceiverSupervisor 的具體實現 ReceiverSupervisorImpl】
  val supervisor = new ReceiverSupervisorImpl(receiver, ...)
  supervisor.start()
  supervisor.awaitTermination()
  ...
}

ReceiverSupervisor 定義了一些方法接口,其具體的實現類是 ReceiverSupervisorImpl

我們看到在上面的代碼中,executor 端會先 new 一個 ReceiverSupervisorImpl,然後 ReceiverSupervisorImpl.start()。這裏 .start() 很重要的工作就是調用 Receiver.onStart(),來啓動 Receiver 的數據接收線程:

start() 成功後,ReceiverSurpervisorImpl 最重要的工作就是接收 Receiver 給 store() 過來的數據了。

ReceiverSurpervisorImpl 有 4 種簽名的 push() 方法,被 Receiver 的 4 種 store() 一一調用。不過接下來對單條小數據和三種塊數據的處理稍有區別。

單條的情況,ReceiverSupervisorImpl 要在 BlockGenerator 的協助下,將多個單條的數據積攢爲一個塊數據,然後重新調用 push 交給 ReceiverSurpervisorImpl 來處理這個塊數據。我們一會再詳解 BlockGenerator 的這個過程。

所以接下來,我們主要看這 3 個存儲塊數據的 push...() 方法,它們的實現非常簡單:

// 來自 ReceiverSupervisorImpl

def pushArrayBuffer(arrayBuffer: ArrayBuffer[_], ...) {
  pushAndReportBlock(ArrayBufferBlock(...), ...)
}

def pushIterator(iterator: Iterator[_], ...) {
  pushAndReportBlock(IteratorBlock(...), ...)
}

def pushBytes(bytes: ByteBuffer, ...){
  pushAndReportBlock(ByteBufferBlock(...), ...)
}

def pushAndReportBlock(receivedBlock: ReceivedBlock, ...) {
...
}

顧名思義,這 3 個存儲塊數據的 push...() 方法即是將自己的數據統一包裝爲 ReceivedBlock,然後由 pushAndReportBlock() 做兩件事情:

  • (a) push:將 ReceivedBlock 交給 ReceivedBlockHandler 來存儲,具體的,可以在 ReceivedBlockHandler 的兩種存儲實現裏二選一
  • (b) report:將已存儲好的 ReceivedBlock 的塊數據 meta 信息報告給 ReceiverTracker

上面的過程可以總結爲:

ReceivedBlockHandler 詳解

ReceivedBlockHandler 是一個接口類,在 executor 端負責對接收到的塊數據進行具體的存儲和清理:

// 來自 ReceivedBlockHandler

private[streaming] trait ReceivedBlockHandler {

  /** Store a received block with the given block id and return related metadata */
  def storeBlock(blockId: StreamBlockId, receivedBlock: ReceivedBlock): ReceivedBlockStoreResult

  /** Cleanup old blocks older than the given threshold time */
  def cleanupOldBlocks(threshTime: Long)
}

ReceivedBlockHandler 有兩個具體的存儲策略的實現:

  • (a) BlockManagerBasedBlockHandler,是直接存到 executor 的內存或硬盤
  • (b) WriteAheadLogBasedBlockHandler,是先寫 WAL,再存儲到 executor 的內存或硬盤

(a) BlockManagerBasedBlockHandler 實現

BlockManagerBasedBlockHandler 主要是直接存儲到 Spark Core 裏的 BlockManager 裏。

BlockManager 將在 executor 端接收 Block 數據,而在 driver 端維護 Block 的 meta 信息。 BlockManager 根據存儲者的 StorageLevel 要求來存到本 executor 的 RAM 或者 DISK,也可以同時再額外複製一份到其它 executor 的 RAM 或者 DISK點這裏查看 StorageLevel 支持的所有枚舉值。

下面是 BlockManagerBasedBlockHandler.store() 向 BlockManager 存儲 3 種塊數據的具體實現:

// 來自 BlockManagerBasedBlockHandler

def storeBlock(blockId: StreamBlockId, block: ReceivedBlock): ReceivedBlockStoreResult = {
  val putResult: Seq[(BlockId, BlockStatus)] = block match {
    case ArrayBufferBlock(arrayBuffer) =>
      blockManager.putIterator(blockId, arrayBuffer.iterator, ...)   // 【存儲數組到 blockManager 裏】
    case IteratorBlock(iterator) =>
      blockManager.putIterator(blockId, countIterator, ...)          // 【存儲 iterator 到 blockManager 裏】
    case ByteBufferBlock(byteBuffer) =>
      blockManager.putBytes(blockId, byteBuffer, ...)                // 【存儲 ByteBuffer 到 blockManager 裏】
  ...
}

(b) WriteAheadLogBasedBlockHandler 實現

WriteAheadLogBasedBlockHandler 的實現則是同時寫到可靠存儲的 WAL 中和 executor 的 BlockManager 中;在兩者都寫完成後,再上報塊數據的 meta 信息。

BlockManager 中的塊數據是計算時首選使用的,只有在 executor 失效時,纔去 WAL 中讀取寫入過的數據。

同其它系統的 WAL 一樣,數據是完全順序地寫入 WAL 的;在稍後上報塊數據的 meta 信息,就額外包含了塊數據所在的 WAL 的路徑,及在 WAL 文件內的偏移地址和長度。

具體的寫入邏輯如下:

// 來自 WriteAheadLogBasedBlockHandler

def storeBlock(blockId: StreamBlockId, block: ReceivedBlock): ReceivedBlockStoreResult = {
  ...
  // 【生成向 BlockManager 存儲數據的 future】
  val storeInBlockManagerFuture = Future {
    val putResult =
      blockManager.putBytes(blockId, serializedBlock, effectiveStorageLevel, tellMaster = true)
    if (!putResult.map { _._1 }.contains(blockId)) {
      throw new SparkException(
        s"Could not store $blockId to block manager with storage level $storageLevel")
    }
  }

  // 【生成向 WAL 存儲數據的 future】
  val storeInWriteAheadLogFuture = Future {
    writeAheadLog.write(serializedBlock, clock.getTimeMillis())
  }

  // 【開始執行兩個 future、等待兩個 future 都結束】
  val combinedFuture = storeInBlockManagerFuture.zip(storeInWriteAheadLogFuture).map(_._2)
  val walRecordHandle = Await.result(combinedFuture, blockStoreTimeout)
  
  // 【返回存儲結果,用於後續的塊數據 meta 上報】
  WriteAheadLogBasedStoreResult(blockId, numRecords, walRecordHandle)
}

BlockGenerator 詳解

最後我們來補充一下 ReceiverSupervisorImpl 在收到單塊條小數據後,委託 BlockGenerator 進行積攢,並封裝多條小數據爲一整個塊數據的詳細過程。

BlockGenerator 在內部主要是維護一個臨時的變長數組 currentBuffer,每收到一條 ReceiverSupervisorImpl 轉發來的數據就加入到這個 currentBuffer 數組中。

這裏非常需要注意的地方,就是在加入 currentBuffer 數組時會先由 rateLimiter 檢查一下速率,是否加入的頻率已經太高。如果太高的話,就需要 block 住,等到下一秒再開始添加。這裏的最高頻率是由 spark.streaming.receiver.maxRate (default = Long.MaxValue) 控制的,是單個 Receiver 每秒鐘允許添加的條數。控制了這個速率,就控制了整個 Spark Streaming 系統每個 batch 需要處理的最大數據量。之前版本的 Spark Streaming 是靜態設置了這樣的一個上限並由所有 Receiver 統一遵守;但在 1.5.0 以來,Spark Streaming 加入了分別動態控制每個 Receiver 速率的特性,這個我們會單獨有一篇文章介紹。

然後會維護一個定時器,每隔 blockInterval 的時間就生成一個新的空變長數組替換老的數組作爲新的 currentBuffer ,並把老的數組加入到一個自己的一個 blocksForPushing 的隊列裏。

這個 blocksForPushing 隊列實際上是一個 ArrayBlockingQueue,大小由 spark.streaming.blockQueueSize(默認 = 10) 來控制。然後就有另外的一個線程專門從這個隊列裏取出來已經包裝好的塊數據,然後調用 ReceiverSupervisorImpl.pushArrayBuffer(...) 來將塊數據交回給 ReceiverSupervisorImpl

BlockGenerator 工作的整個過程示意圖如下:

總結

總結我們在本文所做的詳解 —— ReceiverSupervisor 將在 executor 端作爲的主要角色,並且:

  • (3) Receiver 在 onStart() 啓動後,就將持續不斷地接收外界數據,並持續交給 ReceiverSupervisor 進行數據轉儲;

  • (4) ReceiverSupervisor 持續不斷地接收到 Receiver 轉來的數據:

    • 如果數據很細小,就需要 BlockGenerator 攢多條數據成一塊(4a)、然後再成塊存儲(4b 或 4c)

    • 反之就不用攢,直接成塊存儲(4b 或 4c)

    • 這裏 Spark Streaming 目前支持兩種成塊存儲方式,一種是由 blockManagerskManagerBasedBlockHandler 直接存到 executor 的內存或硬盤,另一種由 WriteAheadLogBasedBlockHandler 是同時寫 WAL(4c) 和 executor 的內存或硬盤

  • (5) 每次成塊在 executor 存儲完畢後,ReceiverSupervisor 就會及時上報塊數據的 meta 信息給 driver 端的 ReceiverTracker;這裏的 meta 信息包括數據的標識 id,數據的位置,數據的條數,數據的大小等信息。

  • (6) ReceiverTracker 再將收到的塊數據 meta 信息直接轉給自己的成員 ReceivedBlockTracker,由 ReceivedBlockTracker 專門管理收到的塊數據 meta 信息。

 

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