SparkStream例子HdfsWordCount--Streaming的Job是如何調度的

上一篇“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調度完成。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章