轉載自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
,在 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
和 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
JobGenerator
維護了一個定時器,週期就是我們剛剛提到的 batchDuration
,定時爲每個 batch 生成 RDD DAG 的實例。 具體的,根據我們在 [DStream 生成 RDD 實例詳解](1.2 DStream 生成 RDD 實例詳解.md) 中的解析,DStreamGraph.generateJobs(time)
將返回一個 Seq[Job]
,其中的每個 Job
是一個 ForEachDStream
實例的 generateJob(time)
返回的結果。
此時,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 實例詳解](1.2 DStream 生成 RDD 實例詳解.md) 裏分析的對應起來了: 在 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
...
// 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
只能處於等待處理狀態。
下面的就是剛纔測試代碼的運行結果,驗證了我們前面的分析和計算:
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
是同義的。
而 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 | ← |
附錄
import java.util.concurrent.{Executors, TimeUnit}
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.receiver.Receiver
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.SparkConf
object ConcurrentJobsDemo {
def main(args: Array[String]) {
// 完整代碼可見本文最後的附錄
val BLOCK_INTERVAL = 1 // in seconds
val BATCH_INTERVAL = 5 // in seconds
val CURRENT_JOBS = 10
val conf = new SparkConf()
conf.setAppName(this.getClass.getSimpleName)
conf.setMaster("local[2]")
conf.set("spark.streaming.blockInterval", s"${BLOCK_INTERVAL}s")
conf.set("spark.streaming.concurrentJobs", s"${CURRENT_JOBS}")
val ssc = new StreamingContext(conf, Seconds(BATCH_INTERVAL))
// DStream DAG 定義開始
val inputStream = ssc.receiverStream(new MyReceiver)
inputStream.foreachRDD(_ => Thread.sleep(Int.MaxValue)) // output 1
inputStream.foreachRDD(_ => Thread.sleep(Int.MaxValue)) // output 2
// DStream DAG 定義結束
ssc.start()
ssc.awaitTermination()
}
class MyReceiver extends Receiver[String](StorageLevel.MEMORY_ONLY) {
override def onStart() {
// invoke store("str") every 100ms
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(new Runnable {
override def run(): Unit = store("str")
}, 0, 100, TimeUnit.MILLISECONDS)
}
override def onStop() {}
}
}