在JobScheduler的start中,当receiverTracker启动完毕之后,将启动JobGenerator。JobGenerator负责对DstreamGraph的初始化,DStream与RDD的转换,生成Job,提交执行等工作.
/** Start generation of jobs */
def start(): Unit = synchronized {
if (eventLoop != null) return // generator has already been started
// Call checkpointWriter here to initialize it before eventLoop uses it to avoid a deadlock.
// See SPARK-10125
checkpointWriter
eventLoop = new EventLoop[JobGeneratorEvent]("JobGenerator") {
override protected def onReceive(event: JobGeneratorEvent): Unit = processEvent(event)
override protected def onError(e: Throwable): Unit = {
jobScheduler.reportError("Error in job generator", e)
}
}
eventLoop.start()
if (ssc.isCheckpointPresent) {
restart()
} else {
startFirstTime()
}
}
可以看到,首先会启动EventLoop[JobGeneratorEvent]来处理各类JobGeneratorEvent。之后会根据是否是第一次启动,也就是是否存在checkpoint,来具体的决定是restart()还是startFirstTime()。
JobGenerator维护了一个定时器,周期就是之前设置的batchDuration,定时为每个batch生成RDD DAG的实例。
private val timer = new RecurringTimer(clock, ssc.graph.batchDuration.milliseconds,
longTime => eventLoop.post(GenerateJobs(new Time(longTime))), "JobGenerator")
不论是restart()还是startFirstTime()都会启动这个定时器,每当周期到了之后,就会给JobGenerator自身的eventLoop发送一个GenerateJobs消息,eventLoop收到消息后会调用
processEvent(event):
/** Processes all events */
private def processEvent(event: JobGeneratorEvent) {
logDebug("Got event " + event)
event match {
case GenerateJobs(time) => generateJobs(time)
case ClearMetadata(time) => clearMetadata(time)
case DoCheckpoint(time, clearCheckpointDataLater) =>
doCheckpoint(time, clearCheckpointDataLater)
case ClearCheckpointData(time) => clearCheckpointData(time)
}
}
/** Generate jobs and perform checkpoint for the given `time`. */
private def generateJobs(time: Time) {
// Set the SparkEnv in this thread, so that job generation code can access the environment
// Example: BlockRDDs are created in this thread, and it needs to access BlockManager
// Update: This is probably redundant after threadlocal stuff in SparkEnv has been removed.
SparkEnv.set(ssc.env)
Try {
jobScheduler.receiverTracker.allocateBlocksToBatch(time) // allocate received blocks to batch
graph.generateJobs(time) // generate jobs using allocated block
} match {
case Success(jobs) =>
val streamIdToInputInfos = jobScheduler.inputInfoTracker.getInfo(time)
jobScheduler.submitJobSet(JobSet(time, jobs, streamIdToInputInfos))
case Failure(e) =>
jobScheduler.reportError("Error generating jobs for time " + time, e)
}
eventLoop.post(DoCheckpoint(time, clearCheckpointDataLater = false))
}
当消息是GenerateJobs(time) 时,将调用generateJobs(time),generateJobs完成以下5个任务:
jobScheduler.receiverTracker.allocateBlocksToBatch(time)
是要求ReceiverTracker将目前已收到数据的meta信息进行一次allocate,即将上次batch切分后剩余的数据切分到到本次新的batch里。每个块数据的meta信息,将被划入一个、且只被划入一个batch。
graph.generateJobs(time)
要求DStreamGraph复制出一套新的RDD DAG的实例,返回一个 Seq[Job]。
jobScheduler.inputInfoTracker.getInfo(time)
获取之前ReceiverTracker分配到本batch的源头数据的meta信息。
jobScheduler.submitJobSet(JobSet(time, jobs, streamIdToInputInfos))
将time、生成的本batch的RDD DAG、获取到的meta信息封装成JobSet提交给JobScheduler。这里的向 JobScheduler提交过程与JobScheduler接下来在jobExecutor里执行过程是异步的,因此本步将非常快即可返回。
最后只要提交结束,就马上对整个系统的当前运行状态做一个checkpoint。这里做 checkpoint 也只是异步提交一个 DoCheckpoint 消息请求,不用等checkpoint真正写完成即可返回。
那么JobScheduler收到jobSet后是具体如何处理的呢:
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.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实质上是Runnable对象,其run()中除了做一些状态记录外,最主要的就是调用job.run()。
jobExecutor的定义如下:
private val numConcurrentJobs = ssc.conf.getInt("spark.streaming.concurrentJobs", 1)
private val jobExecutor =
ThreadUtils.newDaemonFixedThreadPool(numConcurrentJobs, "streaming-job-executor")
这里jobExecutor的线程池大小,是由spark.streaming.concurrentJobs
参数来控制的,当没有显式设置时,其取值为1。这里jobExecutor的线程池大小,就是能够并行执行的Job数。可以如下设置:
val conf = new SparkConf()
conf.setMaster("local[2]")
conf.set("spark.streaming.blockInterval", s"${BLOCK_INTERVAL}s")
conf.set("spark.streaming.concurrentJobs", s"${CURRENT_JOBS}")
三.总结
在StreamingContext调用start方法的内部其实是会启动JobScheduler的Start方法,进行消息循环,在JobScheduler的start内部会构造JobGenerator和ReceiverTacker,并且调用JobGenerator和ReceiverTacker的start方法:
JobGenerator启动后会不断的根据batchDuration生成一个个的Job。
ReceiverTracker启动后首先在Spark Cluster中启动Receiver(其实是在Executor中先启动ReceiverSupervisor),在Receiver收到数据后会通过ReceiverSupervisor存储到Executor并且把数据的Metadata信息发送给Driver中的ReceiverTracker,在ReceiverTracker内部会通过ReceivedBlockTracker来管理接收到的元数据信息。
每个BatchInterval会产生一个具体的Job,其实这里的Job不是Spark Core中所指的Job,它只是基于DStreamGraph而生成的RDD的DAG而已,从Java角度讲,相当于Runnable接口实例,此时要想运行Job需要提交给JobScheduler,在JobScheduler中通过线程池的方式找到一个单独的线程来提交Job到集群运行(其实是在线程中基于RDD的Action触发真正的作业的运行)。
SparkStreaming的WordCount示例及源码分析(一)
SparkStreaming的WordCount示例及源码分析(二)
SparkStreaming的WordCount示例及源码分析(三)