继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

贯通Spark Streaming JobScheduler内幕实现和深入思考

九州编程
关注TA
已关注
手记 475
粉丝 42
获赞 203

    JobScheduler的地位非常的重要,所有的关键都在JobScheduler,它的重要性就相当于是Spark Core当中的DAGScheduler,因此,我们要花重点在JobScheduler上面。

       我们在进行sparkstreaming开发的时候,会对Dstream进行各种transform和action级别的操作,这些操作就构成Dstream graph,也就是Dstream 之间的依赖关系,随着时间的流逝,Dstream graph会根据batchintaval时间间隔,产生RDD的DAG,然后进行job的执行。Dstream 的Dstream graph是逻辑级别的,RDD的DAG是物理执行级别的。DStream是空间维度的层面,空间维度加上时间构成时空维度。

       JobSchedule是将逻辑级别的job物理的运行在spark core上。JobGenerator是产生逻辑级别的job,使用JobSchedule将job在线程池中运行。JobSchedule是在StreamingContext中进行实例化的,并在StreamingContext的start方法中开辟一条新的线程启动的。

// Start the streaming scheduler in a new thread, so that thread local properties

// like call sites and job groups can be reset without affecting those of the

// current thread.ThreadUtils.runInNewThread("streaming-start") {

sparkContext.setCallSite(startSite.get)

sparkContext.clearJobGroup()

sparkContext.setLocalProperty(SparkContext.SPARK_JOB_INTERRUPT_ON_CANCEL, "false")

scheduler.start()

}

      1.大括号中的代码作为一个匿名函数在新的线程中执行。Sparkstreaming运行时至少需要两条线程,其中一条用于一直循环接收数据,现在所说的至少两条线程和上边开辟一条新线程运行scheduler.start()并没有关系。Sparkstreaming运行时至少需要两条线程是用于作业处理的,上边的代码开辟新的线程是在调度层面的中,不论Sparkstreaming程序运行时指定多少线程,这里都会开辟一条新线程,之间没有一点关系。

      2.每一条线程都有自己私有的属性,在这里给新的线程设置私有的属性,这些属性不会影响主线程中的。

sparkContext.setCallSite(startSite.get)

sparkContext.clearJobGroup()

sparkContext.setLocalProperty(SparkContext.SPARK_JOB_INTERRUPT_ON_CANCEL, "false")

      源码中代码的书写模式非常值得学习,以后看源码的时候就把它当做是一个普通的应用程序,从jvm的角度看,spark就是一个分布式的应用程序。不要对源码有代码崇拜情节,不然就没有掌控源码的信心。

JobSchedule在实例化的时候会实例化JobGenerator和线程池。

private valnumConcurrentJobs= ssc.conf.getInt("spark.streaming.concurrentJobs", 1)

private valjobExecutor=

ThreadUtils.newDaemonFixedThreadPool(numConcurrentJobs, "streaming-job-executor")

private valjobGenerator=newJobGenerator(this)

      线程池中默认是有一条线程,当然可以在spark配置文件中配置或者使用代码在sparkconf中修改默认的线程数,在一定程度上增加默认线程数可以提高执行job的效率,这也是一个性能调优的方法(尤其是在一个程序中有多个job时)。

      Java在企业生产环境下已经形成了生态系统,在spark开发中和数据库、hbase、radis、javaEE交互一般都采用java,所以开发大型spark项目大部分都是scala+java的方式进行开发。

      JobGenerator和线程池在JobSchedule在实例化的时候就已经实例化了,而eventloop和receiverTracker是在调用JobGenerator的start方法时才被实例化。defstart(): Unit = synchronized {

if(eventLoop!=null)return// scheduler has already been started

eventLoop=newEventLoop[JobSchedulerEvent]("JobScheduler") {

override protected defonReceive(event: JobSchedulerEvent): Unit = processEvent(event)override protected defonError(e: Throwable): Unit = reportError("Error in job scheduler", e)

}

eventLoop.start()

receiverTracker=newReceiverTracker(ssc)

receiverTracker.start()

jobGenerator.start()

}

        在eventloop的start方法中会回调onStart方法,一般在onStart方法中会执行一些准备性的代码,在JobSchedule中虽然并没有复写onStart方法,不过sparkStreaming框架在这里显然是为了代码的可扩展性考虑的,这是开发项目时需要学习的。

defstart(): Unit = {

if(stopped.get) {

throw newIllegalStateException(name + " has already been stopped")

}

// Call onStart before starting the event thread to make sure it happens before onReceive

onStart()

eventThread.start()

}

Dstream的action级别的操作转过来还是会调用foreachRDD这个方法,生动的说明在对Dstream操作的时候其实还是对RDD的操作。defprint(num: Int): Unit = ssc.withScope {

defforeachFunc: (RDD[T], Time) => Unit = {

(rdd: RDD[T], time: Time) => {

valfirstNum = rdd.take(num + 1)

// scalastyle:off println

println("-------------------------------------------")

println("Time: " + time)

println("-------------------------------------------")

firstNum.take(num).foreach(println)

if(firstNum.length > num)println("...")

println()

}

}

foreachRDD(context.sparkContext.clean(foreachFunc), displayInnerRDDOps =false)

}

上边代码中foreachFunc这个方法是对Dstream action级别的方法的进一步封装,增加了如下代码,在运行spark streaming程序时对这些输出很熟悉。

println("-------------------------------------------")

println("Time: " + time)

println("-------------------------------------------")

foreachRDD方法,转过来new ForEachDstream

Apply a function to each RDD in this DStream. This is an output operator, so

* 'this' DStream will be registered as an output stream and therefore materialized.

private defforeachRDD(

foreachFunc: (RDD[T], Time) => Unit,

displayInnerRDDOps: Boolean): Unit = {

newForEachDStream(this,

context.sparkContext.clean(foreachFunc,false), displayInnerRDDOps).register()

}

注释中说的:将这个函数作用于这个Dstream中的每一个RDD,这是一个输出操作,因此这个Dstream会被注册成outputstream,并进行物化。

ForEachDstream中很重要的一个函数generateJob。考虑时间维度和action级别,每个Duration都基于generateJob来生成作业。foreachFunc(rdd, time)//这个方法就是对Dstream最后的操作 ,newJob(time, jobFunc)只是在RDD的基础上,加上时间维度的封装而已。这里的Job只是一个普通的对象,代表了一个spark的计算,调用Job的run方法时,真正的作业就触发了。foreachFunc(rdd, time)中的rdd其实就是通过DstreamGraph中最后一个Dstream来决定的。

override defgenerateJob(time: Time): Option[Job] = {

parent.getOrCompute(time)match{

caseSome(rdd) =>

valjobFunc = () => createRDDWithLocalProperties(time, displayInnerRDDOps) {

foreachFunc(rdd, time)

}

Some(newJob(time, jobFunc))

caseNone => None

}

}

Jon是通过ForEachDstream的generateJob来生成的,值得注意的是在Dstream的子类中,只有ForEachDstream重写了generateJob方法。

现在考虑一下ForEachDstream的generateJob方法是谁调用的?当然是JobGenerator。ForEachDstream的generateJob方法是静态的逻辑级别,他如果想要真正运行起来变成物理级别的这时候就需要JobGenerator。

现在就来看看JobGenerator的代码,JobGenerator中有一个定时器timer和消息循环体eventloop,timer会基于batchinteval,一直向eventloop中发送JenerateJobs的消息,进而导致processEvent方法->generateJobs方法的执行。

private valtimer=newRecurringTimer(clock,ssc.graph.batchDuration.milliseconds,

longTime =>eventLoop.post(GenerateJobs(newTime(longTime))), "JobGenerator")

eventLoop =newEventLoop[JobGeneratorEvent]("JobGenerator") {

override protected defonReceive(event: JobGeneratorEvent): Unit = processEvent(event)

override protected defonError(e: Throwable): Unit = {

jobScheduler.reportError("Error in job generator", e)

}

}

generateJobs方法的代码:

private defgenerateJobs(time: Time) {

SparkEnv.set(ssc.env)

Try{

jobScheduler.receiverTracker.allocateBlocksToBatch(time)

graph.generateJobs(time) // generate jobs using allocated block

graph.generateJobs(time)这个方法的代码:

defgenerateJobs(time: Time): Seq[Job] = {

logDebug("Generating jobs for time " + time)

valjobs =this.synchronized {

outputStreams.flatMap { outputStream =>

valjobOption = outputStream.generateJob(time)

jobOption.foreach(_.setCallSite(outputStream.creationSite))

jobOption

}

}

logDebug("Generated " + jobs.length + " jobs for time " + time)

jobs

}

其中的outputStream.generateJob(time)中的outputStream就是前面说ForEachDstream,generateJob(time)方法就是ForEachDstream中的generateJob(time)方法。

这是从时间维度调用空间维度的东西,所以时空结合就转变成物理的执行了。

再来看看JobGenerator的generateJobs方法:

Try{

jobScheduler.receiverTracker.allocateBlocksToBatch(time) // allocate received blocks to batch

graph.generateJobs(time) // generate jobs using allocated block

}match{

caseSuccess(jobs) =>

valstreamIdToInputInfos = jobScheduler.inputInfoTracker.getInfo(time)

jobScheduler.submitJobSet(JobSet(time, jobs, streamIdToInputInfos))

caseFailure(e) =>

jobScheduler.reportError("Error generating jobs for time " + time, e)

}

eventLoop.post(DoCheckpoint(time, clearCheckpointDataLater =false))

基于graph.generateJobs产生job后,会封装成JobSet并提交给JobScheduler,JobSet(time, jobs, streamIdToInputInfos),其中streamIdToInputInfos就是接收的数据的元数据。

JobSet代表了一个batch duration中的一批jobs。就是一个普通对象,包含了未提交的jobs,提交的时间,执行开始和结束时间等信息。

JobSet提交给JobScheduler后,会放入jobSets数据结构中,jobSets.put(jobSet.time, jobSet) ,所以JobScheduler就拥有了每个batch中的jobSet.在线程池中进行执行。

defsubmitJobSet(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(newJobHandler(job)))

logInfo("Added jobs for time " + jobSet.time)

}

}

在把job放入线程池中时,采用JonHandler进行封装。JonHandler是一个Runable接口的实例。

其中主要的代码就是job.run(),前面说过job.run()调用的就是Dstream的action级别的方法。

在job.run()前后会发送JonStarted和JobCompleted的消息,JobScheduler接收到这两个消息只是记录一下时间,通知一下job要开始执行或者执行完成,并没有过多的操作。

_eventLoop.post(JobStarted(job,clock.getTimeMillis()))

PairRDDFunctions.disableOutputSpecValidation.withValue(true) {

job.run()

}

_eventLoop =eventLoop

if(_eventLoop !=null) {

_eventLoop.post(JobCompleted(job,clock.getTimeMillis()))

}

备注:

资料来源于:DT_大数据梦工厂(Spark发行版本定制)



作者:阳光男孩spark
链接:https://www.jianshu.com/p/c23a0175a924


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP