上一篇“SparkStream例子HdfsWordCount--從Dstream到RDD全過程解析”解析了每個Dstream週期內,是如何生成的RDD的。
該篇描述一下RDD變成Streaming的Job之後,如何到Executor上面執行開發者寫的foreachFunc(rdd,time)的過程。
四、 Streaming的Job是如何進行調度執行的?
1, 在DstreamGraph的generateJobs()中通過調用ForeachInputDstream.generateJob(time)將time這個週期內的所有數據放到UnionRdd中,
2, 同時和開發者寫的業務代碼放在Some(newJob(time, jobFunc)) 這個Steaming的Job裏面的jobFunc中。然後返回jobOption
def generateJobs(time: Time): Seq[Job] = { logDebug("Generating jobs for time " + time) val jobs = this.synchronized { outputStreams.flatMap { outputStream => //jobOption返回就是Some(new Job(time, jobFunc)),如果是調用的print()方法,則jobFunc就是print方法中聲明的方法 val jobOption = outputStream.generateJob(time) jobOption.foreach(_.setCallSite(outputStream.creationSite)) jobOption } } logDebug("Generated " + jobs.length + " jobs for time " + time) jobs }
3,按上一篇的執行流程,DstreamGraph的generateJobs()執行完成之後,返回JobGenerator.generateJobs(time)繼續往下執行
/** Generate jobs and perform checkpoint for the given `time`. */ private def generateJobs(time: Time) { SparkEnv.set(ssc.env) Try { jobScheduler.receiverTracker.allocateBlocksToBatch(time) // allocate received blocks to batch //調用graph的generateJobs方法,通過scala的Try的apply函數,返回Success(jobs) 或者 Failure(e), 其中的jobs就是該方法返回的Job對象集合,如果Job創建成功,再調用JobScheduler的submitJobSet方法將job提交給集羣執行。 graph.generateJobs(time) // generate jobs using allocated block } match { case Success(jobs) => val streamIdToInputInfos = jobScheduler.inputInfoTracker.getInfo(time) //其中streamIdToInputInfos就是接收的數據的元數據 //JobSet代表了一個batch duration中的一批jobs。就是一個普通對象,包含了未提交的jobs,提交的時間,執行開始和結束時間等信息。 jobScheduler.submitJobSet(JobSet(time, jobs, streamIdToInputInfos)) case Failure(e) => jobScheduler.reportError("Error generating jobs for time " + time, e) } //發送執行CheckPoint時間,發送週期爲streaming batch接收數據的時間 eventLoop.post(DoCheckpoint(time, clearCheckpointDataLater = false)) }
4、上面執行成功之後會轉入JobScheduler.submitJobSet方法中
class JobScheduler(val ssc: StreamingContext) extends Logging { private val jobSets: java.util.Map[Time, JobSet] = new ConcurrentHashMap[Time, JobSet] private val numConcurrentJobs = ssc.conf.getInt("spark.streaming.concurrentJobs", 1) //JobScheduler在實例化的時候會實例化JobGenerator和線程池。 private val jobExecutor = ThreadUtils.newDaemonFixedThreadPool(numConcurrentJobs, "streaming-job-executor")
==》其中jobExecutor就是Jdk的Executor線程池。
==》上面的代碼可以看。線程池中默認是有一條線程,可以在spark配置文件中配置或者使用代碼在sparkconf中修改默認的線程數,在一定程度上增加默認線程數可以提高執行Job的效率,這也是一個性能調優的方法(尤其是在一個程序中有多個Job時)
def 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) //將當前時間和將JobSet放在jobSets的ConcurrentHashMap中 jobSet.jobs.foreach(job => jobExecutor.execute(new JobHandler(job))) logInfo("Added jobs for time " + jobSet.time) } }
5,JobExecutor是線程池,說明JobHandler肯定是Runnable實現類:
private class JobHandler(job: Job) extends Runnable with Logging { import JobScheduler._ def run() { try { 。。。。 var _eventLoop = eventLoop if (_eventLoop != null) {
//從這個代碼可知,在當前週期內,Rdd數據Job之後,纔將JobStarted放到EventLoop的成員隊列中,開始執行EventLoop中的onReceive方法,即執行JobScheduler中processEvent()對應的handleJobStart() _eventLoop.post(JobStarted(job, clock.getTimeMillis()))
。。。。。
}
}
private def processEvent(event: JobSchedulerEvent) { try { event match { case JobStarted(job, startTime) => handleJobStart(job, startTime) case JobCompleted(job, completedTime) => handleJobCompletion(job, completedTime) case ErrorReported(m, e) => handleError(m, e) } } catch { case e: Throwable => reportError("Error in job scheduler", e) } }
6、實際上這個handleJobStart也沒有什麼神祕的,就是記錄一下啓動事件而以:
//捕獲job的啓動事件 private def handleJobStart(job: Job, startTime: Long) { val jobSet = jobSets.get(job.time) val isFirstJobOfJobSet = !jobSet.hasStarted jobSet.handleJobStart(job) if (isFirstJobOfJobSet) { // "StreamingListenerBatchStarted" should be posted after calling "handleJobStart" to get the // correct "jobSet.processingStartTime". //listenerBus它是StreamingListenerBus對象 listenerBus.post(StreamingListenerBatchStarted(jobSet.toBatchInfo)) } job.setStartTime(startTime) listenerBus.post(StreamingListenerOutputOperationStarted(job.toOutputOperationInfo)) logInfo("Starting job " + job.id + " from job set of time " + jobSet.time) }
7,再返回到JobHandler這個Runnable類中查看一下Job.run到底幹了什麼了?
private class JobHandler(job: Job) extends Runnable with Logging { import JobScheduler._ def run() { try { 。。。。 var _eventLoop = eventLoop if (_eventLoop != null) { _eventLoop.post(JobStarted(job, clock.getTimeMillis())) PairRDDFunctions.disableOutputSpecValidation.withValue(true) { job.run() } _eventLoop = eventLoop if (_eventLoop != null) { _eventLoop.post(JobCompleted(job, clock.getTimeMillis())) } } else { // JobScheduler has been stopped. } } finally { ssc.sc.setLocalProperty(JobScheduler.BATCH_TIME_PROPERTY_KEY, null) ssc.sc.setLocalProperty(JobScheduler.OUTPUT_OP_ID_PROPERTY_KEY, null) } } }
8,Job.run()可以很清晰的看出實際上是執行的ForEachDstream
// val jobFunc = () =>createRDDWithLocalProperties(time, displayInnerRDDOps) {
// foreachFunc(rdd, time)
// }
private[streaming] class Job(val time: Time, func: () => _) {
…. private var _result: Try[_] = null … def run() { _result = Try(func()) }
===》而ForEachDstream中的foreachFunc(rdd, time)不就是咱們在案例中看到的調用print代碼中聲明的foreachFunc嗎。這邊的foreachFunc裏面的調用就是sparkCore的內容了
def print(num: Int): Unit = ssc.withScope { def foreachFunc: (RDD[T], Time) => Unit = { (rdd: RDD[T], time: Time) => { val firstNum = rdd.take(num + 1) // scalastyle:off println println("-------------------------------------------") println("Time: " + time) println("-------------------------------------------") firstNum.take(num).foreach(println) if (firstNum.length > num) println("...") println() // scalastyle:on println } } foreachRDD(context.sparkContext.clean(foreachFunc), displayInnerRDDOps = false) }
===》再回到JobHandler,當Job.run方法執行結束之後,就會發送JobCompleted的case class給EventLoop。
====》至此,當前Time週期內的Job調度完成。