? ? ? ?JobScheduler的地位非常的重要,所有的關(guān)鍵都在JobScheduler,它的重要性就相當(dāng)于是Spark Core當(dāng)中的DAGScheduler,因此,我們要花重點(diǎn)在JobScheduler上面。
? ? ? ?我們?cè)谶M(jìn)行sparkstreaming開發(fā)的時(shí)候,會(huì)對(duì)Dstream進(jìn)行各種transform和action級(jí)別的操作,這些操作就構(gòu)成Dstream graph,也就是Dstream 之間的依賴關(guān)系,隨著時(shí)間的流逝,Dstream graph會(huì)根據(jù)batchintaval時(shí)間間隔,產(chǎn)生RDD的DAG,然后進(jìn)行job的執(zhí)行。Dstream 的Dstream graph是邏輯級(jí)別的,RDD的DAG是物理執(zhí)行級(jí)別的。DStream是空間維度的層面,空間維度加上時(shí)間構(gòu)成時(shí)空維度。
? ? ? ?JobSchedule是將邏輯級(jí)別的job物理的運(yùn)行在spark core上。JobGenerator是產(chǎn)生邏輯級(jí)別的job,使用JobSchedule將job在線程池中運(yùn)行。JobSchedule是在StreamingContext中進(jìn)行實(shí)例化的,并在StreamingContext的start方法中開辟一條新的線程啟動(dòng)的。
// 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.大括號(hào)中的代碼作為一個(gè)匿名函數(shù)在新的線程中執(zhí)行。Sparkstreaming運(yùn)行時(shí)至少需要兩條線程,其中一條用于一直循環(huán)接收數(shù)據(jù),現(xiàn)在所說的至少兩條線程和上邊開辟一條新線程運(yùn)行scheduler.start()并沒有關(guān)系。Sparkstreaming運(yùn)行時(shí)至少需要兩條線程是用于作業(yè)處理的,上邊的代碼開辟新的線程是在調(diào)度層面的中,不論Sparkstreaming程序運(yùn)行時(shí)指定多少線程,這里都會(huì)開辟一條新線程,之間沒有一點(diǎn)關(guān)系。
? ? ? 2.每一條線程都有自己私有的屬性,在這里給新的線程設(shè)置私有的屬性,這些屬性不會(huì)影響主線程中的。
sparkContext.setCallSite(startSite.get)
sparkContext.clearJobGroup()
sparkContext.setLocalProperty(SparkContext.SPARK_JOB_INTERRUPT_ON_CANCEL, "false")
? ? ? 源碼中代碼的書寫模式非常值得學(xué)習(xí),以后看源碼的時(shí)候就把它當(dāng)做是一個(gè)普通的應(yīng)用程序,從jvm的角度看,spark就是一個(gè)分布式的應(yīng)用程序。不要對(duì)源碼有代碼崇拜情節(jié),不然就沒有掌控源碼的信心。
JobSchedule在實(shí)例化的時(shí)候會(huì)實(shí)例化JobGenerator和線程池。
private valnumConcurrentJobs= ssc.conf.getInt("spark.streaming.concurrentJobs", 1)
private valjobExecutor=
ThreadUtils.newDaemonFixedThreadPool(numConcurrentJobs, "streaming-job-executor")
private valjobGenerator=newJobGenerator(this)
? ? ? 線程池中默認(rèn)是有一條線程,當(dāng)然可以在spark配置文件中配置或者使用代碼在sparkconf中修改默認(rèn)的線程數(shù),在一定程度上增加默認(rèn)線程數(shù)可以提高執(zhí)行job的效率,這也是一個(gè)性能調(diào)優(yōu)的方法(尤其是在一個(gè)程序中有多個(gè)job時(shí))。
? ? ? Java在企業(yè)生產(chǎn)環(huán)境下已經(jīng)形成了生態(tài)系統(tǒng),在spark開發(fā)中和數(shù)據(jù)庫、hbase、radis、javaEE交互一般都采用java,所以開發(fā)大型spark項(xiàng)目大部分都是scala+java的方式進(jìn)行開發(fā)。
? ? ? JobGenerator和線程池在JobSchedule在實(shí)例化的時(shí)候就已經(jīng)實(shí)例化了,而eventloop和receiverTracker是在調(diào)用JobGenerator的start方法時(shí)才被實(shí)例化。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方法中會(huì)回調(diào)onStart方法,一般在onStart方法中會(huì)執(zhí)行一些準(zhǔn)備性的代碼,在JobSchedule中雖然并沒有復(fù)寫onStart方法,不過sparkStreaming框架在這里顯然是為了代碼的可擴(kuò)展性考慮的,這是開發(fā)項(xiàng)目時(shí)需要學(xué)習(xí)的。
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級(jí)別的操作轉(zhuǎn)過來還是會(huì)調(diào)用foreachRDD這個(gè)方法,生動(dòng)的說明在對(duì)Dstream操作的時(shí)候其實(shí)還是對(duì)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這個(gè)方法是對(duì)Dstream action級(jí)別的方法的進(jìn)一步封裝,增加了如下代碼,在運(yùn)行spark streaming程序時(shí)對(duì)這些輸出很熟悉。
println("-------------------------------------------")
println("Time: " + time)
println("-------------------------------------------")
foreachRDD方法,轉(zhuǎn)過來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()
}
注釋中說的:將這個(gè)函數(shù)作用于這個(gè)Dstream中的每一個(gè)RDD,這是一個(gè)輸出操作,因此這個(gè)Dstream會(huì)被注冊(cè)成outputstream,并進(jìn)行物化。
ForEachDstream中很重要的一個(gè)函數(shù)generateJob。考慮時(shí)間維度和action級(jí)別,每個(gè)Duration都基于generateJob來生成作業(yè)。foreachFunc(rdd, time)//這個(gè)方法就是對(duì)Dstream最后的操作 ,newJob(time, jobFunc)只是在RDD的基礎(chǔ)上,加上時(shí)間維度的封裝而已。這里的Job只是一個(gè)普通的對(duì)象,代表了一個(gè)spark的計(jì)算,調(diào)用Job的run方法時(shí),真正的作業(yè)就觸發(fā)了。foreachFunc(rdd, time)中的rdd其實(shí)就是通過DstreamGraph中最后一個(gè)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方法。
現(xiàn)在考慮一下ForEachDstream的generateJob方法是誰調(diào)用的?當(dāng)然是JobGenerator。ForEachDstream的generateJob方法是靜態(tài)的邏輯級(jí)別,他如果想要真正運(yùn)行起來變成物理級(jí)別的這時(shí)候就需要JobGenerator。
現(xiàn)在就來看看JobGenerator的代碼,JobGenerator中有一個(gè)定時(shí)器timer和消息循環(huán)體eventloop,timer會(huì)基于batchinteval,一直向eventloop中發(fā)送JenerateJobs的消息,進(jìn)而導(dǎo)致processEvent方法->generateJobs方法的執(zhí)行。
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)這個(gè)方法的代碼:
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)方法。
這是從時(shí)間維度調(diào)用空間維度的東西,所以時(shí)空結(jié)合就轉(zhuǎn)變成物理的執(zhí)行了。
再來看看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產(chǎn)生job后,會(huì)封裝成JobSet并提交給JobScheduler,JobSet(time, jobs, streamIdToInputInfos),其中streamIdToInputInfos就是接收的數(shù)據(jù)的元數(shù)據(jù)。
JobSet代表了一個(gè)batch duration中的一批jobs。就是一個(gè)普通對(duì)象,包含了未提交的jobs,提交的時(shí)間,執(zhí)行開始和結(jié)束時(shí)間等信息。
JobSet提交給JobScheduler后,會(huì)放入jobSets數(shù)據(jù)結(jié)構(gòu)中,jobSets.put(jobSet.time, jobSet) ,所以JobScheduler就擁有了每個(gè)batch中的jobSet.在線程池中進(jìn)行執(zhí)行。
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放入線程池中時(shí),采用JonHandler進(jìn)行封裝。JonHandler是一個(gè)Runable接口的實(shí)例。
其中主要的代碼就是job.run(),前面說過job.run()調(diào)用的就是Dstream的action級(jí)別的方法。
在job.run()前后會(huì)發(fā)送JonStarted和JobCompleted的消息,JobScheduler接收到這兩個(gè)消息只是記錄一下時(shí)間,通知一下job要開始執(zhí)行或者執(zhí)行完成,并沒有過多的操作。
_eventLoop.post(JobStarted(job,clock.getTimeMillis()))
PairRDDFunctions.disableOutputSpecValidation.withValue(true) {
job.run()
}
_eventLoop =eventLoop
if(_eventLoop !=null) {
_eventLoop.post(JobCompleted(job,clock.getTimeMillis()))
}
備注:
資料來源于:DT_大數(shù)據(jù)夢(mèng)工廠(Spark發(fā)行版本定制)
更多私密內(nèi)容,請(qǐng)關(guān)注微信公眾號(hào):DT_Spark
如果您對(duì)大數(shù)據(jù)Spark感興趣,可以免費(fèi)聽由王家林老師每天晚上20:00開設(shè)的Spark永久免費(fèi)公開課,地址YY房間號(hào):68917580