揭開Spark Streaming神秘面紗③ - 動態生成 job

JobScheduler有兩個重要成員,一是上文介紹的 ReceiverTracker,負責分發 receivers 及源源不斷地接收數據;二是本文將要介紹的 JobGenerator,負責定時的生成 jobs 并 checkpoint。

定時邏輯

在 JobScheduler 的主構造函數中,會創建 JobGenerator 對象。在 JobGenerator 的主構造函數中,會創建一個定時器:

  private val timer = new RecurringTimer(clock, ssc.graph.batchDuration.milliseconds,
    longTime => eventLoop.post(GenerateJobs(new Time(longTime))), "JobGenerator")

該定時器每隔 ssc.graph.batchDuration.milliseconds 會執行一次 eventLoop.post(GenerateJobs(new Time(longTime))) 向 eventLoop 發送 GenerateJobs(new Time(longTime))消息,eventLoop收到消息后會進行這個 batch 對應的 jobs 的生成及提交執行,eventLoop 是一個消息接收處理器。
需要注意的是,timer 在創建之后并不會馬上啟動,將在 StreamingContext#start() 啟動 Streaming Application 時間接調用到 timer.start(restartTime.milliseconds)才啟動。

為 batch 生成 jobs

eventLoop 在接收到 GenerateJobs(new Time(longTime))消息后的主要處理流程有以上圖中三步:

  1. 將已接收到的 blocks 分配給 batch
  2. 生成該 batch 對應的 jobs
  3. 將 jobs 封裝成 JobSet 并提交執行

接下來我們就將逐一展開這三步進行分析

將已接受到的 blocks 分配給 batch

上圖是根據源碼畫出的為 batch 分配 blocks 的流程圖,這里對 『獲得 batchTime 各個 InputDStream 未分配的 blocks』作進一步說明:
在文章 『文章鏈接』 中我們知道了各個 ReceiverInputDStream 對應的 receivers 接收并保存的 blocks 信息會保存在 ReceivedBlockTracker#streamIdToUnallocatedBlockQueues,該成員 key 為 streamId,value 為該 streamId 對應的 InputDStream 已接收保存但尚未分配的 blocks 信息。
所以獲取某 InputDStream 未分配的 blocks 只要以該 InputDStream 的 streamId 來從 streamIdToUnallocatedBlockQueues 來 get 就好。獲取之后,會清楚該 streamId 對應的value,以保證 block 不會被重復分配。

在實際調用中,為 batchTime 分配 blocks 時,會從streamIdToUnallocatedBlockQueues取出未分配的 blocks 塞進 timeToAllocatedBlocks: mutable.HashMap[Time, AllocatedBlocks] 中,以在之后作為該 batchTime 對應的 RDD 的輸入數據。

通過以上步驟,就可以為 batch 的所有 InputDStream 分配 blocks。也就是為 batch 分配了 blocks。

生成該 batch 對應的 jobs

為指定 batchTime 生成 jobs 的邏輯如上圖所示。你可能會疑惑,為什么 DStreamGraph#generateJobs(time: Time)為什么返回 Seq[Job],而不是單個 job。這是因為,在一個 batch 內,可能會有多個 OutputStream 執行了多次 output 操作,每次 output 操作都將產生一個 Job,最終就會產生多個 Jobs。

我們結合上圖對執行流程進一步分析。
DStreamGraph#generateJobs(time: Time)中,對于DStreamGraph成員ArrayBuffer[DStream[_]]的每一項,調用DStream#generateJob(time: Time)來生成這個 outputStream 在該 batchTime 的 job。該生成過程主要有三步:

Step1: 獲取該 outputStream 在該 batchTime 對應的 RDD

每個 DStream 實例都有一個 generatedRDDs: HashMap[Time, RDD[T]] 成員,用來保存該 DStream 在每個 batchTime 生成的 RDD,當 DStream#getOrCompute(time: Time)調用時

  • 首先會查看generatedRDDs中是否已經有該 time 對應的 RDD,若有則直接返回

  • 若無,則調用compute(validTime: Time)來生成 RDD,這一步根據每個 InputDStream繼承 compute 的實現不同而不同。例如,對于 FileInputDStream,其 compute 實現邏輯如下:

    1. 先通過一個 findNewFiles() 方法,找到多個新 file
    2. 對每個新 file,都將其作為參數調用 sc.newAPIHadoopFile(file),生成一個 RDD 實例
    3. 將 2 中的多個新 file 對應的多個 RDD 實例進行 union,返回一個 union 后的 UnionRDD

Step2: 根據 Step1中得到的 RDD 生成最終 job 要執行的函數 jobFunc

jobFunc定義如下:

val jobFunc = () => {
  val emptyFunc = { (iterator: Iterator[T]) => {} }
  context.sparkContext.runJob(rdd, emptyFunc)
}

可以看到,每個 outputStream 的 output 操作生成的 Job 其實與 RDD action 一樣,最終調用 SparkContext#runJob 來提交 RDD DAG 定義的任務

Step3: 根據 Step2中得到的 jobFunc 生成最終要執行的 Job 并返回

Step2中得到了定義 Job 要干嘛的函數-jobFunc,這里便以 jobFunc及 batchTime 生成 Job 實例:

Some(new Job(time, jobFunc))

該Job實例將最終封裝在 JobHandler 中被執行

至此,我們搞明白了 JobScheduler 是如何通過一步步調用來動態生成每個 batchTime 的 jobs。下文我們將分析這些動態生成的 jobs 如何被分發及如何執行。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容