上一篇“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调度完成。