Spark定制班第7课:Spark Streaming源码解读之JobScheduler内幕实现和深度思考

来源:互联网 发布:网络现金赌博官网 编辑:程序博客网 时间:2024/04/30 06:20
解读Spark Streaming源码时,不要把代码看成高深的东西,只要把它看成是JVM上的普通应用程序。要有信心搞定它。

本期内容
1. JobScheduler内幕实现
2. JobScheduler深度思考

1. JobScheduler内幕实现

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

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

  StreamingContext.start的代码片段:

  def start(): Unit = synchronized {
    state match {
      case INITIALIZED =>
        ...
          try {
            ...
            ThreadUtils.runInNewThread("streaming-start") {
              sparkContext.setCallSite(startSite.get)
              sparkContext.clearJobGroup()
              sparkContext.setLocalProperty(SparkContext.SPARK_JOB_INTERRUPT_ON_CANCEL, "false")
              scheduler.start()
            }
            state = StreamingContextState.ACTIVE
          } catch {
            ...
          }
          ...
        }
        ...
      case ACTIVE =>
        ...
      case STOPPED =>
        ...
    }
  }

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

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

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


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)
  private val jobExecutor =
    ThreadUtils.newDaemonFixedThreadPool(numConcurrentJobs, "streaming-job-executor")
  private val jobGenerator = new JobGenerator(this)
  ...
}

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

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

  ReceiverTracker、JobGeneratorJobScheduler实例化的时候实例化了。

  JobScheduler.start的代码:


  def start(): Unit = synchronized {
    if (eventLoop != null) return // scheduler has already been started

    logDebug("Starting JobScheduler")
    eventLoop = new EventLoop[JobSchedulerEvent]("JobScheduler") {
      override protected def onReceive(event: JobSchedulerEvent): Unit = processEvent(event)

      override protected def onError(e: Throwable): Unit = reportError("Error in job scheduler", e)
    }
    eventLoop.start()

    // attach rate controllers of input streams to receive batch completion updates
    for {
      inputDStream <- ssc.graph.getInputStreams
      rateController <- inputDStream.rateController
    } ssc.addStreamingListener(rateController)

    listenerBus.start(ssc.sparkContext)
    receiverTracker = new ReceiverTracker(ssc)
    inputInfoTracker = new InputInfoTracker(ssc)
    receiverTracker.start()
    jobGenerator.start()
    logInfo("Started JobScheduler")
  }

  Eventloop是在调用JobGeneratorstart方法时实例化。


  /** 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()
    }
  }

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

  def start(): Unit = {
    if (stopped.get) {
      throw new IllegalStateException(name + " has already been stopped")
    }
    // Call onStart before starting the event thread to make sure it happens before onReceive
    onStart()
    eventThread.start()
  }

   DStreamaction级别的操作转过来还是会调用foreachRDD这个方法,生动的说明在对DStream操作的时候其实还是对RDD的操作。

  /**
   * Print the first num elements of each RDD generated in this DStream. This is an output
   * operator, so this DStream will be registered as an output stream and there materialized.
   */
  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)
  }
  上边代码中foreachFunc这个方法是对DStream action级别的方法的进一步封装,增加了如下代码,在运行Spark Streaming程序时对这些输出很熟悉。

        println("-------------------------------------------")
        println("Time: " + time)
        println("-------------------------------------------")

  foreachRDD方法,转过来new ForEachDstream


 private def foreachRDD(
                          foreachFunc: (RDD[T], Time) => Unit,
                          displayInnerRDDOps: Boolean): Unit = {
    new ForEachDStream(this,
      context.sparkContext.clean(foreachFunc, false), displayInnerRDDOps).register()
  }

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

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

  ForEachDStream.generateJob的代码:


  override def generateJob(time: Time): Option[Job] = {
    parent.getOrCompute(time) match {
      case Some(rdd) =>
        val jobFunc = () => createRDDWithLocalProperties(time, displayInnerRDDOps) {
          foreachFunc(rdd, time)
        }
        Some(new Job(time, jobFunc))
      case None => None
    }
  }


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

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

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


  private val timer = new RecurringTimer(clock, ssc.graph.batchDuration.milliseconds,
    longTime => eventLoop.post(GenerateJobs(new Time(longTime))), "JobGenerator")
  ...
  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()
    }
  }

  generateJobs方法的代码:
  
/** 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))
  }

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


  def generateJobs(time: Time): Seq[Job] = {
    logDebug("Generating jobs for time " + time)
    val jobs = this.synchronized {
      outputStreams.flatMap { outputStream =>
        val jobOption = outputStream.generateJob(time)
        jobOption.foreach(_.setCallSite(outputStream.creationSite))
        jobOption
      }
    }
    logDebug("Generated " + jobs.length + " jobs for time " + time)
    jobs
  }

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


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

  JobGenerator的generateJobs方法的代码:


  /** 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))
  }

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

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

  JobSet提交给JobScheduler后,会放入jobSets数据结构中,jobSets.put(jobSet.timejobSet) ,所以JobScheduler就拥有了每个batch中的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放入线程池中时,采用JobHandler进行封装。JobHandler是一个Runable接口的实例。

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

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


    def run() {
      try {
        val formattedTime = UIUtils.formatBatchTime(
          job.time.milliseconds, ssc.graph.batchDuration.milliseconds, showYYYYMMSS = false)
        val batchUrl = s"/streaming/batch/?id=${job.time.milliseconds}"
        val batchLinkText = s"[output operation ${job.outputOpId}, batch time ${formattedTime}]"

        ssc.sc.setJobDescription(
          s"""Streaming job from <a href="$batchUrl">$batchLinkText</a>""")
        ssc.sc.setLocalProperty(BATCH_TIME_PROPERTY_KEY, job.time.milliseconds.toString)
        ssc.sc.setLocalProperty(OUTPUT_OP_ID_PROPERTY_KEY, job.outputOpId.toString)

        // We need to assign `eventLoop` to a temp variable. Otherwise, because
        // `JobScheduler.stop(false)` may set `eventLoop` to null when this method is running, then
        // it's possible that when `post` is called, `eventLoop` happens to null.
        var _eventLoop = eventLoop
        if (_eventLoop != null) {
          _eventLoop.post(JobStarted(job, clock.getTimeMillis()))
          // Disable checks for existing output directories in jobs launched by the streaming
          // scheduler, since we may need to write output to an existing directory during checkpoint
          // recovery; see SPARK-4835 for more details.
          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)
      }

    } 


2. JobScheduler深度思考


  JobScheduler是整个Spark Streming的调度的核心,其地位相当于Spark Core中的DAGScheduler。
因此,我们务必彻底掌握JobScheduler

0 0
原创粉丝点击