spark streaming原始碼分析3 排程及執行

五柳-先生發表於2016-01-29

前面的兩節內容介紹了StreamingContext的構造以及在此上的一系列操作。

通過呼叫start方法,真正開始排程執行。首先校驗狀態是否是INITIALIZED,然後呼叫JobScheduler的start方法,並將狀態設定為ACTIVE。

看一下JobScheduler的start方法內部

[java] view plain copy
  1. def start(): Unit = synchronized {  
  2.     if (eventLoop != nullreturn // scheduler has already been started  
  3.   
  4.     logDebug("Starting JobScheduler")  
  5.     eventLoop = new EventLoop[JobSchedulerEvent]("JobScheduler") {  
  6.       override protected def onReceive(event: JobSchedulerEvent): Unit = processEvent(event)  
  7.   
  8.       override protected def onError(e: Throwable): Unit = reportError("Error in job scheduler", e)  
  9.     }  
  10.     eventLoop.start()  
  11.   
  12.     listenerBus.start(ssc.sparkContext)  
  13.     receiverTracker = new ReceiverTracker(ssc)  
  14.     inputInfoTracker = new InputInfoTracker(ssc)  
  15.     receiverTracker.start()  
  16.     jobGenerator.start()  
  17.     logInfo("Started JobScheduler")  
  18.   }  
1、首先構造一個事件型別為[JobSchedulerEvent]的迴圈器eventLoop(包含JobStarted,JobCompleted,ErrorReported三個事件),內部有一個執行緒實時獲取佇列中的事件,有則處理。實際呼叫如上的onReceive/onError方法。eventLoop.start後,內部執行緒真正執行起來,並等待事件的到來。 

2、構造ReceiverTracker

(1)從DStreamGraph中獲取註冊的ReceiverInputStreams

(2)獲取所有ReceiverInputStreams的streamId

(3)構造一個ReceiverLauncher,它是一個接受器

(4)構造一個ReceivedBlockTracker,用於維護所有的接收器(receiver)接收到的所有block資訊,即ReceivedBlockInfo

3、呼叫receiverTracker的start方法。

如果receiverInputStreams不為空,則建立akka RPC服務,名稱為ReceiverTracker,負責註冊Receiver、AddBlock、ReportError(報告錯誤)、登出Receiver四個事件

呼叫receiverExecutor的start方法,最終呼叫了startReceivers方法。

[java] view plain copy
  1. /** 
  2.      * Get the receivers from the ReceiverInputDStreams, distributes them to the 
  3.      * worker nodes as a parallel collection, and runs them. 
  4.      */  
  5.     private def startReceivers() {  
  6.       val receivers = receiverInputStreams.map(nis => {  
  7.         val rcvr = nis.getReceiver()  
  8.         rcvr.setReceiverId(nis.id)  
  9.         rcvr  
  10.       })  
  11.   
  12.       // Right now, we only honor preferences if all receivers have them  
  13.       val hasLocationPreferences = receivers.map(_.preferredLocation.isDefined).reduce(_ && _)  
  14.   
  15.       // Create the parallel collection of receivers to distributed them on the worker nodes  
  16.       val tempRDD =  
  17.         if (hasLocationPreferences) {  
  18.           val receiversWithPreferences = receivers.map(r => (r, Seq(r.preferredLocation.get)))  
  19.           ssc.sc.makeRDD[Receiver[_]](receiversWithPreferences)  
  20.         } else {  
  21.           ssc.sc.makeRDD(receivers, receivers.size)  
  22.         }  
  23.   
  24.       val checkpointDirOption = Option(ssc.checkpointDir)  
  25.       val serializableHadoopConf = new SerializableWritable(ssc.sparkContext.hadoopConfiguration)  
  26.   
  27.       // Function to start the receiver on the worker node  
  28.       val startReceiver = (iterator: Iterator[Receiver[_]]) => {  
  29.         if (!iterator.hasNext) {  
  30.           throw new SparkException(  
  31.             "Could not start receiver as object not found.")  
  32.         }  
  33.         val receiver = iterator.next()  
  34.         val supervisor = new ReceiverSupervisorImpl(  
  35.           receiver, SparkEnv.get, serializableHadoopConf.value, checkpointDirOption)  
  36.         supervisor.start()  
  37.         supervisor.awaitTermination()  
  38.       }  
  39.       // Run the dummy Spark job to ensure that all slaves have registered.  
  40.       // This avoids all the receivers to be scheduled on the same node.  
  41.       if (!ssc.sparkContext.isLocal) {  
  42.         ssc.sparkContext.makeRDD(1 to 5050).map(x => (x, 1)).reduceByKey(_ + _, 20).collect()  
  43.       }  
  44.   
  45.       // Distribute the receivers and start them  
  46.       logInfo("Starting " + receivers.length + " receivers")  
  47.       running = true  
  48.       ssc.sparkContext.runJob(tempRDD, ssc.sparkContext.clean(startReceiver))  
  49.       running = false  
  50.       logInfo("All of the receivers have been terminated")  
  51.     }  

1)獲取所有的receiver(接收器)

2)將receivers建立tempRDD,並分割槽並行化,每個分割槽一個元素,元素為receiver

3)建立方法startReceiver,該方法以分割槽元素(receiver)的迭代器作為引數,之後將該方法引數傳入runJob中,針對每個分割槽,依次將每個分割槽中的元素(receiver)應用到該方法上

4)runJob的startReceiver方法。每個分割槽只有一個receiver,因此在該方法內構造一個ReceiverSupervisorImpl,在它內部真正的接收資料並儲存。傳送RegisterReceiver訊息給dirver驅動。

重點介紹一下supervisor.start方法內部的邏輯實現:主要分為以下兩個方法

[java] view plain copy
  1. /** Start the supervisor */  
  2.   def start() {  
  3.     onStart()  
  4.     startReceiver()  
  5.   }  
(1)onStart方法:

[java] view plain copy
  1. override protected def onStart() {  
  2.     blockGenerator.start()  
  3.   }  

  1. 資料真正接收到是發生在SocketReceiver.receive函式中,將接收到的資料放入到BlockGenerator.currentBuffer
  2. 在BlockGenerator中有一個重複定時器,處理函式為updateCurrentBuffer, updateCurrentBuffer將當前buffer中的資料封裝為一個新的Block,放入到blocksForPush佇列中
  3. 同樣是在BlockGenerator中有一個BlockPushingThread,其職責就是不停的將blocksForPushing佇列中的成員通過pushArrayBuffer函式傳遞給blockmanager,讓BlockManager將資料儲存到MemoryStore中
  4. pushArrayBuffer還會將已經由BlockManager儲存的Block的id號傳遞給ReceiverTracker,ReceiverTracker會將儲存的blockId放到對應StreamId的佇列中

(2)startReceiver方法:

[java] view plain copy
  1. /** Start receiver */  
  2.   def startReceiver(): Unit = synchronized {  
  3.     try {  
  4.       logInfo("Starting receiver")  
  5.       receiver.onStart()  
  6.       logInfo("Called receiver onStart")  
  7.       onReceiverStart()  
  8.       receiverState = Started  
  9.     } catch {  
  10.       case t: Throwable =>  
  11.         stop("Error starting receiver " + streamId, Some(t))  
  12.     }  
  13.   }  
1)receiver.onStart方法
建立socket連線,逐行讀取資料,最終將資料插入BlockGenerator的currentBuffer中。一旦插入了資料,就觸發了上面重複定時器。按設定的block生產間隔(預設200ms),生成block,將block插入blocksForPushing佇列中。然後,blockPushingThread執行緒逐個取出傳遞給blockmanager儲存起來,同時通過AddBlock訊息通知ReceiverTracker已經將哪些block儲存到了blockmanager中

2)onReceiverStart方法

向receiverTracker(位於driver端)傳送RegisterReceiver訊息,報告自己(receiver)啟動了,目的是可以在UI中反饋出來。ReceiverTracker將每一個stream接收到但還沒有進行處理的block放入到receiverInfo,其為一Hashmap. 在後面的generateJobs中會從receiverInfo提取資料以生成相應的RDD。

4、呼叫jobGenerator的start方法。

(1)首先構建JobGeneratorEvent型別事件的EventLoop,包含GenerateJobs,ClearMetadata,DoCheckpoint,ClearCheckpointData四個事件。並執行起來。

(2)呼叫startFirstTime啟動generator

[java] view plain copy
  1. /** Starts the generator for the first time */  
  2.   private def startFirstTime() {  
  3.     val startTime = new Time(timer.getStartTime())  
  4.     graph.start(startTime - graph.batchDuration)  
  5.     timer.start(startTime.milliseconds)  
  6.     logInfo("Started JobGenerator at " + startTime)  
  7.   }  
timer.getStartTime計算出來下一個週期的到期時間,計算公式:(math.floor(clock.currentTime.toDouble / period) + 1).toLong * period,以當前的時間/除以間隔時間,再用math.floor求出它的上一個整數(即上一個週期的到期時間點),加上1,再乘以週期就等於下一個週期的到期時間。

  (3) 啟動DStreamGraph,呼叫graph.start方法,啟動時間比startTime早一個時間間隔,為什麼呢?求告知!!!

[java] view plain copy
  1. def start(time: Time) {  
  2.     this.synchronized {  
  3.       if (zeroTime != null) {  
  4.         throw new Exception("DStream graph computation already started")  
  5.       }  
  6.       zeroTime = time  
  7.       startTime = time  
  8.       outputStreams.foreach(_.initialize(zeroTime))//設定outputstream的zeroTime為time值  
  9.       outputStreams.foreach(_.remember(rememberDuration))//如果設定過rememberDuration,則設定outputstream的rememberDuration為該值  
  10.       outputStreams.foreach(_.validateAtStart)  
  11.       inputStreams.par.foreach(_.start())  
  12.     }  
  13.   }  
 (4) 呼叫timer.start方法,引數為startTime

這裡的timer為:

[java] view plain copy
  1. private val timer = new RecurringTimer(clock, ssc.graph.batchDuration.milliseconds,  
  2.     longTime => eventLoop.post(GenerateJobs(new Time(longTime))), "JobGenerator")  
內部包含一個定時器,每隔batchDuration的時間間隔就向eventLoop傳送一個GenerateJobs訊息,引數longTime為下一個間隔到來時的時間點

[java] view plain copy
  1. /** 
  2.  * Start at the given start time. 
  3.  */  
  4. def start(startTime: Long): Long = synchronized {  
  5.   nextTime = startTime  
  6.   thread.start()  
  7.   logInfo("Started timer for " + name + " at time " + nextTime)  
  8.   nextTime  
  9. }  
通過內部的thread.start方法,觸發timer內部的定時器執行。從而按時間間隔產生job。

5、GenerateJobs/ClearMetadata 事件處理介紹

JobGeneratorEvent型別事件的EventLoop,包含GenerateJobs,ClearMetadata,DoCheckpoint,ClearCheckpointData四個事件

 GenerateJobs:

[java] view plain copy
  1. /** Generate jobs and perform checkpoint for the given `time`.  */  
  2.   private def generateJobs(time: Time) {  
  3.     // Set the SparkEnv in this thread, so that job generation code can access the environment  
  4.     // Example: BlockRDDs are created in this thread, and it needs to access BlockManager  
  5.     // Update: This is probably redundant after threadlocal stuff in SparkEnv has been removed.  
  6.     SparkEnv.set(ssc.env)  
  7.     Try {  
  8.       jobScheduler.receiverTracker.allocateBlocksToBatch(time) // allocate received blocks to batch  
  9.       graph.generateJobs(time) // generate jobs using allocated block  
  10.     } match {  
  11.       case Success(jobs) =>  
  12.         val streamIdToInputInfos = jobScheduler.inputInfoTracker.getInfo(time)  
  13.         val streamIdToNumRecords = streamIdToInputInfos.mapValues(_.numRecords)  
  14.         jobScheduler.submitJobSet(JobSet(time, jobs, streamIdToNumRecords))  
  15.       case Failure(e) =>  
  16.         jobScheduler.reportError("Error generating jobs for time " + time, e)  
  17.     }  
  18.     eventLoop.post(DoCheckpoint(time, clearCheckpointDataLater = false))  
  19.   }  
(1)allocateBlocksToBatch:首先根據time的值獲取之前receiver接收到的並且通過AddBlock訊息傳遞給receiverTracker的block後設資料資訊。並且將time對應的blocks資訊對映儲存起來。

那麼,這裡的time是怎麼和每200ms間隔產生blocks對應起來的呢?答案就是time時間到後,將所有接收到但還未分配的blocks都劃為這個time間隔內的。

(2)generateJobs:根據一個outputStream生成一個job,最終每個outputStream都呼叫如下的方法,見下面程式碼註釋

注:這裡的generateJob實際呼叫的是根據outputStream過載的方法,比如print的方法是輸出一些值:

[java] view plain copy
  1. override def generateJob(time: Time): Option[Job] = {  
  2.     parent.getOrCompute(time) match {<span style="font-family: Tahoma, 'Microsoft Yahei', Simsun;">//這裡實際是手動呼叫了ReceiverInputDStream的compute方法,產生一個RDD,確切的說是BlockRDD。見下面介紹</span>  
  3.       case Some(rdd) =>  
  4.         val jobFunc = () => createRDDWithLocalProperties(time) {  
  5.           ssc.sparkContext.setCallSite(creationSite)  
  6.           foreachFunc(rdd, time)<span style="font-family: Tahoma, 'Microsoft Yahei', Simsun;">//這裡將上面的到的BlockRDD和一個在每個分割槽上執行的方法封裝成一個jobFunc,在foreachFunc方法內部通過runJob提交任務獲得輸出的值,從而輸出</span>  
  7.         }  
  8.         Some(new Job(time, jobFunc))<span style="font-family: Tahoma, 'Microsoft Yahei', Simsun;">//</span><span style="font-family: Tahoma, 'Microsoft Yahei', Simsun;">將time和jobFunc再次封裝成Job,返回,等待被排程執行</span>  
  9.       case None => None  
  10.     }  
  11.   }  
這裡需要解釋一下ReceiverInputDStream的compute方法

1)首先根據time值將之前對映的blocks後設資料資訊獲取出來

2) 獲取這些blocks的blockId,blockId其實就是streamId+唯一值,這個唯一值可以保證在一個流裡面產生的唯一的Id

3)將這個batchTime時間內的blocks元資訊彙總起來,儲存到inputInfoTracker中

4)將sparkContext和blockIds封裝成BlockRDD返回

至此,Job已經產生了。如果Job產生成功,就走Case Success(Jobs) =>分支

[java] view plain copy
  1. jobScheduler.submitJobSet(JobSet(time, jobs, streamIdToNumRecords))  
主要是根據time,jobs,以及streamId和每個streamId的記錄數的對映封裝成JobSet,呼叫submitJobSet

[java] view plain copy
  1. def submitJobSet(jobSet: JobSet) {  
  2.     if (jobSet.jobs.isEmpty) {  
  3.       logInfo("No jobs added for time " + jobSet.time)  
  4.     } else {  
  5.       listenerBus.post(StreamingListenerBatchSubmitted(jobSet.toBatchInfo))  
  6.       jobSets.put(jobSet.time, jobSet)  
  7.       jobSet.jobs.foreach(job => jobExecutor.execute(new JobHandler(job)))  
  8.       logInfo("Added jobs for time " + jobSet.time)  
  9.     }  
  10.   }  
可以看到,將jobSet儲存到jobSets這樣一個對映結構當中,然後將每個job通過JobHandler封裝之後,通過一個執行緒呼叫執行起來。這個執行緒就是通過“spark.streaming.concurrentJobs”引數設定的一個執行緒池,預設是1。

接著看JobHandler被執行緒處理時的邏輯,見程式碼註釋:

[java] view plain copy
  1. private class JobHandler(job: Job) extends Runnable with Logging {  
  2.     def run() {  
  3.       ssc.sc.setLocalProperty(JobScheduler.BATCH_TIME_PROPERTY_KEY, job.time.milliseconds.toString)  
  4.       ssc.sc.setLocalProperty(JobScheduler.OUTPUT_OP_ID_PROPERTY_KEY, job.outputOpId.toString)  
  5.       try {  
  6.         eventLoop.post(JobStarted(job))//這裡主要是設定這個job所處的jobset的processingStartTime為當時時刻  
  7.         // Disable checks for existing output directories in jobs launched by the streaming  
  8.         // scheduler, since we may need to write output to an existing directory during checkpoint  
  9.         // recovery; see SPARK-4835 for more details.  
  10.         PairRDDFunctions.disableOutputSpecValidation.withValue(true) {  
  11.           job.run()//這裡的run方法就是呼叫了封裝Job時的第二個引數,一個方法引數,就是上面的jobFunc  
  12.         }  
  13.         eventLoop.post(JobCompleted(job))//如果這個job所處的jobset都完成了,就設定processingEndTime,並向時間迴圈器傳送ClearMetadata訊息,後續講解  
  14.       } finally {  
  15.         ssc.sc.setLocalProperty(JobScheduler.BATCH_TIME_PROPERTY_KEY, null)  
  16.         ssc.sc.setLocalProperty(JobScheduler.OUTPUT_OP_ID_PROPERTY_KEY, null)  
  17.       }  
  18.     }  
  19.   }  
ClearMetadata:

當一個jobset完成後,就會處理ClearMetadata訊息

1、根據time的時間,過濾出在time之前的rdd,如果設定了rememberDuration,則過濾出小於(time-rememberDuration)的rdd

2、將過濾出的rdd呼叫unpersist

3、刪除在blockManager中的block

4、根據dependencies關係鏈依次刪除,從outputStream開始,根據鏈路依次進行

5、刪除其它記憶體紀錄資訊


至此,關於spark stream最重要的部分,排程及執行就分析結束了!

轉載: http://blog.csdn.net/yueqian_zhu/article/details/49023383

相關文章