轉載自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)
引言
前面在 [Spark Streaming 實現思路與模塊概述](Spark Streaming 實現思路與模塊概述) 和 [DStream 生成 RDD 實例詳解](DStream 生成 RDD 實例詳解) 裏我們分析了 DStream
和 DStreamGraph
具有能夠實例化 RDD
和 RDD
DAG 的能力,下面我們來看 Spark Streaming 是如何將其動態調度的。
在 Spark Streaming 程序的入口,我們都會定義一個 batchDuration
,就是需要每隔多長時間就比照靜態的 DStreamGraph
來動態生成一個 RDD DAG 實例。在 Spark Streaming 裏,總體負責動態作業調度的具體類是 JobScheduler
,
JobScheduler
有兩個非常重要的成員:JobGenerator
和 ReceiverTracker
。JobScheduler
將每個 batch 的 RDD DAG 具體生成工作委託給 JobGenerator
,而將源頭輸入數據的記錄工作委託給 ReceiverTracker
。
JobScheduler 的全限定名是:org.apache.spark.streaming.scheduler.JobScheduler
JobGenerator 的全限定名是:org.apache.spark.streaming.scheduler.JobGenerator
ReceiverTracker 的全限定名是:org.apache.spark.streaming.scheduler.ReceiverTracker
本文我們來詳解 JobScheduler
。
JobGenerator 啓動
在用戶 code 最後調用 ssc.start()
時,將隱含的導致一系列模塊的啓動,其中對我們 JobGenerator
這裏的啓動調用關係如下:
// 來自 StreamingContext.start(), JobScheduler.start(), JobGenerator.start()
ssc.start() // 【用戶 code:StreamingContext.start()】
-> scheduler.start() // 【JobScheduler.start()】
-> jobGenerator.start() // 【JobGenerator.start()】
具體的看,JobGenerator.start()
的代碼如下:
// 來自 JobGenerator.start()
def start(): Unit = synchronized {
...
eventLoop.start() // 【啓動 RPC 處理線程】
if (ssc.isCheckpointPresent) {
restart() // 【如果不是第一次啓動,就需要從 checkpoint 恢復】
} else {
startFirstTime() // 【第一次啓動,就 startFirstTime()】
}
}
可以看到,在啓動了 RPC 處理線程 eventLoop
後,就會根據是否是第一次啓動,也就是是否存在 checkpoint,來具體的決定是 restart()
還是 startFirstTime()
。
後面我們會分析失效後重啓的 restart()
流程,這裏我們來關注 startFirstTime()
:
// 來自 JobGenerator.startFirstTime()
private def startFirstTime() {
val startTime = new Time(timer.getStartTime())
graph.start(startTime - graph.batchDuration)
timer.start(startTime.milliseconds)
logInfo("Started JobGenerator at " + startTime)
}
可以看到,這裏首次啓動時做的工作,先是通過 graph.start()
來告知了 DStreamGraph
第 1 個 batch 的啓動時間,然後就是 timer.start()
啓動了關鍵的定時器。
當定時器 timer
啓動以後,JobGenerator
的 startFirstTime()
就完成了。
RecurringTimer
通過之前幾篇文章的分析我們知道,JobGenerator
維護了一個定時器,週期就是用戶設置的 batchDuration
,定時爲每個 batch 生成 RDD DAG 的實例。
具體的,這個定時器實例就是:
// 來自 JobGenerator
private[streaming]
class JobGenerator(jobScheduler: JobScheduler) extends Logging {
...
private val timer = new RecurringTimer(clock, ssc.graph.batchDuration.milliseconds,
longTime => eventLoop.post(GenerateJobs(new Time(longTime))), "JobGenerator")
...
}
通過代碼也可以看到,整個 timer
的調度週期就是 batchDuration
,每次調度起來就是做一個非常簡單的工作:往 eventLoop
裏發送一個消息 —— 該爲當前 batch (new Time(longTime)
) GenerateJobs 了!
GenerateJobs
接下來,eventLoop
收到消息時,會在一個消息處理的線程池裏,執行對應的操作。在這裏,處理 GenerateJobs(time)
消息的對應操作是 generateJobs(time)
:
private def generateJobs(time: Time) {
SparkEnv.set(ssc.env)
Try {
jobScheduler.receiverTracker.allocateBlocksToBatch(time) // 【步驟 (1)】
graph.generateJobs(time) // 【步驟 (2)】
} match {
case Success(jobs) =>
val streamIdToInputInfos = jobScheduler.inputInfoTracker.getInfo(time) // 【步驟 (3)】
jobScheduler.submitJobSet(JobSet(time, jobs, streamIdToInputInfos)) // 【步驟 (4)】
case Failure(e) =>
jobScheduler.reportError("Error generating jobs for time " + time, e)
}
eventLoop.post(DoCheckpoint(time, clearCheckpointDataLater = false)) // 【步驟 (5)】
}
這段代碼異常精悍,包含了 JobGenerator
主要工作 —— 如下圖所示 —— 的 5 個步驟!
- (1) 要求
ReceiverTracker
將目前已收到的數據進行一次 allocate,即將上次 batch 切分後的數據切分到到本次新的 batch 裏- 這裏
ReceiverTracker
對已收到數據的 meta 信息進行allocateBlocksToBatch(time)
,與ReceiverTracker
自己接收ReceiverSupervisorImpl
上報塊數據 meta 信息的過程,是相互獨立的,但通過synchronized
關鍵字來互斥同步 - 即是說,不管
ReceiverSupervisorImpl
形成塊數據的時間戳t1
、ReceiverSupervisorImpl
發送塊數據的時間戳t2
、ReceiverTracker
收到塊數據的時間戳t3
分別是啥,最終塊數據劃入哪個 batch,還是由ReceiverTracker.allocateBlocksToBatch(time)
方法獲得synchronized
鎖的那一刻,還有未劃入之前任何一個 batch 的塊數據 meta,將被劃分入最新的 batch - 所以,每個塊數據的 meta 信息,將被劃入一個、且只被劃入一個 batch
- 這裏
- (2) 要求
DStreamGraph
複製出一套新的 RDD DAG 的實例,具體過程是:DStreamGraph
將要求圖裏的尾DStream
節點生成具體的 RDD 實例,並遞歸的調用尾DStream
的上游DStream
節點……以此遍歷整個DStreamGraph
,遍歷結束也就正好生成了 RDD DAG 的實例- 這個過程的詳解,請參考前面的文章 [DStream 生成 RDD 實例詳解](1.2 DStream 生成 RDD 實例詳解.md)
- 精確的說,整個
DStreamGraph.generateJobs(time)
遍歷結束的返回值是Seq[Job]
- (3) 獲取第 1 步
ReceiverTracker
分配到本 batch 的源頭數據的 meta 信息- 第 1 步中
ReceiverTracker
只是對 batch 的源頭數據 meta 信息進行了 batch 的分配,本步驟是按照 batch 時間來向ReceiverTracker
查詢得到劃分到本 batch 的塊數據 meta 信息
- 第 1 步中
- (4) 將第 2 步生成的本 batch 的 RDD DAG,和第 3 步獲取到的 meta 信息,一同提交給
JobScheduler
異步執行- 這裏我們提交的是將 (a)
time
(b)Seq[job]
(c)塊數據的 meta 信息
這三者包裝爲一個JobSet
,然後調用JobScheduler.submitJobSet(JobSet)
提交給JobScheduler
- 這裏的向
JobScheduler
提交過程與JobScheduler
接下來在jobExecutor
裏執行過程是異步分離的,因此本步將非常快即可返回
- 這裏我們提交的是將 (a)
- (5) 只要提交結束(不管是否已開始異步執行),就馬上對整個系統的當前運行狀態做一個 checkpoint
- 這裏做 checkpoint 也只是異步提交一個
DoCheckpoint
消息請求,不用等 checkpoint 真正寫完成即可返回 - 這裏也簡單描述一下 checkpoint 包含的內容,包括已經提交了的、但尚未運行結束的 JobSet 等實際運行時信息。
- 這裏做 checkpoint 也只是異步提交一個