轉載自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)
目錄
(a) BlockManagerBasedBlockHandler 實現
(b) WriteAheadLogBasedBlockHandler 實現
引言
我們在前面 [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 信息。