JobScheduler, Job, JobSet 詳解

前面在 Spark Streaming 實現思路與模塊概述 和 DStream 生成 RDD 實例詳解 裏我們分析了 DStreamGraph和 DStream 具有能夠實例化 RDD 和 RDD DAG 的能力,下面我們來看 Spark Streaming 是如何將其動態調度的。

在 Spark Streaming 程序的入口,我們都會定義一個 batchDuration,就是需要每隔多長時間就比照靜態的 DStreamGraph 來動態生成一個 RDD DAG 實例。在 Spark Streaming 裏,總體負責動態作業調度的具體類是 JobScheduler,在 Spark Streaming 程序在 ssc.start() 開始運行時,會生成一個 JobScheduler 的實例,並被 start() 運行起來。

// 來自 StreamingContext
def start(): Unit = synchronized {
  ...
  ThreadUtils.runInNewThread("streaming-start") {
    sparkContext.setCallSite(startSite.get)
    sparkContext.clearJobGroup()
    sparkContext.setLocalProperty(SparkContext.SPARK_JOB_INTERRUPT_ON_CANCEL, "false")
    scheduler.start()  // 【這裏調用了 JobScheduler().start()】
  }
  state = StreamingContextState.ACTIVE
  ...
}

Spark Streaming 的 Job 總調度者 JobScheduler

JobScheduler 是 Spark Streaming 的 Job 總調度者

JobScheduler 有兩個非常重要的成員:JobGenerator 和 ReceiverTrackerJobScheduler 將每個 batch 的 RDD DAG 具體生成工作委託給 JobGenerator,而將源頭輸入數據的記錄工作委託給 ReceiverTracker

image

JobScheduler    的全限定名是:org.apache.spark.streaming.scheduler.JobScheduler
JobGenerator    的全限定名是:org.apache.spark.streaming.scheduler.JobGenerator
ReceiverTracker 的全限定名是:org.apache.spark.streaming.scheduler.ReceiverTracker

JobGenerator 維護了一個定時器,週期就是我們剛剛提到的 batchDuration定時爲每個 batch 生成 RDD DAG 的實例。 具體的,根據我們在 DStream 生成 RDD 實例詳解 中的解析,DStreamGraph.generateJobs(time) 將返回一個 Seq[Job],其中的每個 Job 是一個 ForEachDStream 實例的 generateJob(time) 返回的結果。

image

此時,JobGenerator 拿到了 Seq[Job] 後(如上圖 (2) ),就將其包裝成一個 JobSet(如上圖 (3) ),然後就調用JobScheduler.submitJobSet(jobSet) 來交付回 JobScheduler(如上圖 (4) )。

那麼 JobScheduler 收到 jobSet 後是具體如何處理的呢?我們看其實現:

// 來自 JobScheduler.submitJobSet(jobSet: JobSet)
if (jobSet.jobs.isEmpty) {
  logInfo("No jobs added for time " + jobSet.time)
} else {
  listenerBus.post(StreamingListenerBatchSubmitted(jobSet.toBatchInfo))
  jobSets.put(jobSet.time, jobSet)
  // 【下面這行是最主要的處理邏輯:將每個 job 都在 jobExecutor 線程池中、用 new JobHandler 來處理】
  jobSet.jobs.foreach(job => jobExecutor.execute(new JobHandler(job)))
  logInfo("Added jobs for time " + jobSet.time)
}

這裏最重要的處理邏輯是 job => jobExecutor.execute(new JobHandler(job)),也就是將每個 job 都在 jobExecutor 線程池中、用 new JobHandler 來處理。

JobHandler

先來看 JobHandler 針對 Job 的主要處理邏輯:

// 來自 JobHandler
def run()
{
  ...
  // 【發佈 JobStarted 消息】
  _eventLoop.post(JobStarted(job))
  PairRDDFunctions.disableOutputSpecValidation.withValue(true) {
    // 【主要邏輯,直接調用了 job.run()】
    job.run()
  }
  _eventLoop = eventLoop
  if (_eventLoop != null) {
  // 【發佈 JobCompleted 消息】
    _eventLoop.post(JobCompleted(job))
  }
  ...
}

也就是說,JobHandler 除了做一些狀態記錄外,最主要的就是調用 job.run()!這裏就與我們在 DStream 生成 RDD 實例詳解 裏分析的對應起來了: 在 ForEachDStream.generateJob(time) 時,是定義了 Job 的運行邏輯,即定義了 Job.func。而在JobHandler 這裏,是真正調用了 Job.run()、將觸發 Job.func 的真正執行!

Job 運行的線程池 jobExecutor

上面 JobHandler 是解決了做什麼的問題,本節 jobExecutor 是解決 Job 在哪裏做。

具體的,jobExecutor 是 JobScheduler 的成員:

// 來自 JobScheduler
private[streaming]
class JobScheduler(val ssc: StreamingContext) extends Logging {
  ...
  private val numConcurrentJobs = ssc.conf.getInt("spark.streaming.concurrentJobs", 1)
  private val jobExecutor =
      ThreadUtils.newDaemonFixedThreadPool(numConcurrentJobs, "streaming-job-executor")
  ...
}

也就是,ThreadUtils.newDaemonFixedThreadPool() 調用將產生一個名爲 "streaming-job-executor" 的線程池,所以,Job 將在這個線程池的線程裏,被實際執行 func

spark.streaming.concurrentJobs 參數

這裏 jobExecutor 的線程池大小,是由 spark.streaming.concurrentJobs 參數來控制的,當沒有顯式設置時,其取值爲 1

進一步說,這裏 jobExecutor 的線程池大小,就是能夠並行執行的 Job 數。而回想前文講解的DStreamGraph.generateJobs(time) 過程,一次 batch 產生一個 Seq[Job},裏面可能包含多個 Job —— 所以,確切的,*有幾個 *output 操作,就調用幾次 ForEachDStream.generatorJob(time),就產生出幾個 Job **。

爲了驗證這個結果,我們做一個簡單的小測試:先設置 spark.streaming.concurrentJobs = 10,然後在每個 batch 裏做 2 次foreachRDD() 這樣的 output 操作:

// 完整代碼可見本文最後的附錄
val BLOCK_INTERVAL = 1 // in seconds
val BATCH_INTERVAL = 5 // in seconds
val CURRENT_JOBS = 10  // in seconds
...

// DStream DAG 定義開始
val inputStream = ssc.receiverStream(...)
inputStream.foreachRDD(_ => Thread.sleep(Int.MaxValue))  // output 1
inputStream.foreachRDD(_ => Thread.sleep(Int.MaxValue))  // output 2
// DStream DAG 定義結束
...

在上面的設定下,我們很容易知道,能夠同時在處理的 batch 有 10 / 2 = 5 個,其餘的 batch 的 Job 只能處於等待處理狀態。

下面的就是剛纔測試代碼的運行結果,驗證了我們前面的分析和計算:

image

Spark Streaming 的 JobSet, Job,與 Spark Core 的 Job, Stage, TaskSet, Task

最後,我們專門拿出一個小節,辨別一下這 Spark Streaming 的 JobSet, Job,與 Spark Core 的 Job, Stage, TaskSet, Task 這幾個概念。

[Spark Streaming]
JobSet  的全限定名是:org.apache.spark.streaming.scheduler.JobSet
Job     的全限定名是:org.apache.spark.streaming.scheduler.Job

[Spark Core]
Job     沒有一個對應的實體類,主要是通過 jobId:Int 來表示一個具體的 job
Stage   的全限定名是:org.apache.spark.scheduler.Stage
TaskSet 的全限定名是:org.apache.spark.scheduler.TaskSet
Task    的全限定名是:org.apache.spark.scheduler.Task

Spark Core 的 Job, Stage, Task 就是我們“日常”談論 Spark 任務時所說的那些含義,而且在 Spark 的 WebUI 上有非常好的體現,比如下圖就是 1 個 Job 包含 3 個 Stage;3 個 Stage 各包含 8, 2, 4 個 Task。而 TaskSet則是 Spark Core 的內部代碼裏用的類,是 Task 的集合,和 Stage 是同義的。

image

而 Spark Streaming 裏也有一個 Job,但此 Job 非彼 Job。Spark Streaming 裏的 Job 更像是個 Java 裏的 Runnable,可以 run() 一個自定義的 func 函數。而這個 func, 可以:

  • 直接調用 RDD 的 action,從而產生 1 個或多個 Spark Core 的 Job
  • 先打印一行表頭;然後調用 firstTen = RDD.collect(),再打印 firstTen 的內容;最後再打印一行表尾 —— 這正是DStream.print() 的 Job 實現
  • 也可以是任何用戶定義的 code,甚至整個 Spark Streaming 執行過程都不產生任何 Spark Core 的 Job—— 如上一小節所展示的測試代碼,其 Job 的 func 實現就是:Thread.sleep(Int.MaxValue),僅僅是爲了讓這個 Job 一直跑在jobExecutor 線程池裏,從而測試 jobExecutor 的並行度 :)

最後,Spark Streaming 的 JobSet 就是多個 Job 的集合了。

如果對上面 5 個概念做一個層次劃分的話(上一層與下一層多是一對多的關係,但不完全準確),就應該是下表的樣子:

  Spark Core Spark Streaming
lv 5 RDD DAGs DStreamGraph
lv 4 RDD DAG JobSet
lv 3 Job Job
lv 2 Stage
lv 1 Task

轉自:GitHub

https://github.com/proflin/CoolplaySpark/blob/master/Spark%20Streaming%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%E7%B3%BB%E5%88%97/2.1%20JobScheduler,%20Job,%20JobSet%20%E8%AF%A6%E8%A7%A3.md

轉載請註明:人人都是數據咖 » JobScheduler, Job, JobSet 詳解

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