Spark学习笔记(6)源码解读之Job动态生成和深度思考

来源:互联网 发布:nodejs怎么运行js文件 编辑:程序博客网 时间:2024/05/29 18:35
本期内容:
1 Spark Streaming Job生成深度思考
2 Spark Streaming Job生成源码解析

1 Spark Streaming Job生成深度思考
  前面的课程中已经讲了,Spark Streaming的Job是通过JobGenerator生成。这里说的Job和Spark Core中的Job不是一回事。Spark Streaming中的Job相当于Java中线程要处理的Runnable的业务逻辑的封装。Spark Core上的Job是一个运行的作业。
  JobGenerator会利用DStream来生成Job。从流程上看,DStream一般分三种类型:
  1、输入的DStreams。
  2、输出的DStreams,是一个逻辑级别的Action,它是SparkStreaming框架提出的,它的底层还是会被翻译成物理级别的Action。所谓物理级别的Action是RDD的Action。
  3、中间的业务处理的通过Transformation(转换)产生的DStream。也就是业务处理逻辑的中间过程。
  产生DStream也可以说有两类:一类是根据数据源产生DStream,另一类是通过前面的DStream转换后生成DStream。
  所以说,JobGenerator是根据DStream的依赖关系,或者说根据DStreamGraph来产生Job。
  当然从时间维度上讲,JobGenarator会不断地产生Job。我们做连续不断的大数据任务时,如果不采用流式处理,一般会用定时任务。其实我们也是变相地在做流处理。不管是定时1分钟、1小时,甚至是1天,都类似在做流处理。应该更深刻的理解此前说过的一句话吧:一切没有经过流处理或近似流处理的数据,都将是没有价值的数据。原来的定时任务实际上是在做变相的流处理。我们相信,一切处理终将会被流处理完全统一。

2 Spark Streaming Job生成源码解析

  其实,Spark Streaming的流处理实际上就是以时间作为触发器的。这和基于事件触发的Storm不同。
  看看Spark Streaming应用程序代码的最开始的共同之处:定义SparkStreamingContext。给个例子:

val ssc = new StreamingContext(conf, Seconds(5))

  这个5秒钟的Batch Duration(批处理时间间隔)就是用来设置定时器的时间长度。
  系统翻译基于DStream的依赖关系,成为了RDD之间的依赖关系,而最后的处理是一个action,但由于这个处理是定义在一个方法中,而此方法没有被马上调用,也就没有马上执行。这是为了便于管理,要形成队列来依次处理。
  Spark Streaming应用程序代码的最后面的共同之处:运行SparkStreamingContext的start():
   ssc.start()
  这里才是真正启动流处理执行的地方。启动SparkStreamingContext,会启动JobScheduler。

  Spark Streaming作业的三大核心是:
  1. JobGenerator:负责Job的生成。
  2. ReceiverTracker:负责数据的接收。
  3. JobScheduler:负责Job的调度执行。

  JobGenerator、ReceiverTracker其实是在JobScheduler的start()中被启动。
  先提供Job生成的总体轮廓。本期剖析的Job生成的主流程图如下所示:


  先从JobScheduler.start开始剖析。
  JobScheduler.start的代码片段:

class JobScheduler(val ssc: StreamingContext) extends Logging {
  ...
  private val jobGenerator = new JobGenerator(this)
  ...
  def start(): Unit = synchronized {
    ...
    receiverTracker = new ReceiverTracker(ssc)
    ...
    receiverTracker.start()
    jobGenerator.start()
    ...
  }
  ...
}

  我们先看JobGenerator的start():

/** Start generation of jobs */
def start(): Unit = synchronized {
  ...
  eventLoop = new EventLoop[JobGeneratorEvent]("JobGenerator") {
    override protected def onReceive(event: JobGeneratorEvent): Unit = processEvent(event)
    ...
    }
  }
  eventLoop.start()

  if (ssc.isCheckpointPresent) {
    restart()
  } else {
    startFirstTime()
  }
}

  注释也说明这是生成jobs。
  代码中生成了EventLoop,并启动它。EventLoop是个消息接收器,其中有个消息队列,用于接收消息;它启动后,其中会有个线程不断地从该队列中取消息进行处理。
  代码中覆盖了EventLoop的onReceive方法,OnReceive()里,一般不要做复杂耗时的操作,应该交给别的线程去处理。按照定义,OnReceive也就是调用了processEvent方法。
  JobGenerator.processEvent的代码:

/** Processes all events */
private def processEvent(event: JobGeneratorEvent) {
  ...
  event match {
    case GenerateJobs(time) => generateJobs(time)
    ...
  }
}

  一般情况下,会执行 GenerateJobs(time)这种类型的事件消息。其相应的会执行generateJobs(time):
  JobGenerator.generateJobs的代码:

/** Generate jobs and perform checkpoint for the given `time`.  */
private def generateJobs(time: Time) {
  ...
  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))
      ...
  }
  ...
}

  主要是先获取数据,然后对这些数据来生成相应的jobs,生成工作是调用DStreamGraph的generateJobs。
  DStreamGraph.generateJobs的代码:

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
}

  这里的outputDStream是DStream中最后处理的Dstream,根据DStreamGraph的最后面的outputDStream,从后往前推,
  DStream.generateJob的代码:
/**
 * Generate a SparkStreaming job for the given time. This is an internal method that
 * should not be called directly. This default implementation creates a job
 * that materializes the corresponding RDD. Subclasses of DStream may override this
 * to generate their own jobs.
 */
 private[streaming] def generateJob(time: Time): Option[Job] = {
  getOrCompute(time) match {
    case Some(rdd) => {
      val jobFunc = () => {
        val emptyFunc = { (iterator: Iterator[T]) => {} }
        context.sparkContext.runJob(rdd, emptyFunc)
      }
      Some(new Job(time, jobFunc))
    }
    case None => None
  }
}
  代码的注释也说明了DStream间的操作是逻辑级别的,RDD之间的操作是物理的。
  getOrCompute是获得指定时间的RDD。
  其中的Job是Spark Streaming中定义的。这里并不是真正执行了Spark Core的作业。而是用函数jobFunc封装了Spark作业的执行本身。
  这个Job类其实就是相当于Java中的Bean,并没有做Spark的作业的执行。

  至此,JobGenarator.start中的消息处理的定义就剖析完了。再看其中的后续代码。
  JobGenarator.start的代码:

/** Start generation of jobs */
def start(): Unit = synchronized {
  ...
  eventLoop = new EventLoop[JobGeneratorEvent]("JobGenerator") {
    override protected def onReceive(event: JobGeneratorEvent): Unit = processEvent(event)
    ...
    }
  }
  eventLoop.start()

  if (ssc.isCheckpointPresent) {
    restart()
  } else {
    startFirstTime()
  }
}

  restart是利用checkPoint重新启动JobGenerator。我们主要看看startFirstTime。
  JobGenerator.startFirstTime代码:

/** Starts the generator for the first time */
private def startFirstTime() {
  val startTime = new Time(timer.getStartTime())
  graph.start(startTime - graph.batchDuration)
  timer.start(startTime.milliseconds)
  logInfo("Started JobGenerator at " + startTime)
}

  其中启动了DStreamGraph和RecurringTimer。
  DStreamGraph.start对DStreamGraph中的各DStream做初始化。
  RecurringTimer.start代码:

/**
 * Start at the given start time.
 */
def start(startTime: Long): Long = synchronized {
  nextTime = startTime
  thread.start()
  logInfo("Started timer for " + name + " at time " + nextTime)
  nextTime
}

  实际上是启动一个线程来定时调用回调函数。再回过头看timer的定义:

private val timer = new RecurringTimer(clock, ssc.graph.batchDuration.milliseconds,
  longTime => eventLoop.post(GenerateJobs(new Time(longTime))), "JobGenerator")

  其中的匿名的回调函数是发送GenerateJobs消息给eventLoop。
  原来JobGenerator.processEvent中获得的消息是RecurringTimer定时触发的。

  JobGenerator中的generateJobs剖析完了,再看后面的代码。
  JobGenerator.generateJobs的代码:

/** Generate jobs and perform checkpoint for the given `time`.  */
private def generateJobs(time: Time) {
  ...
  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))
      ...
  }
  ...
}

  JobScheduler.submitJobSet的代码:

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

jobExecutor是线程池,JobHandler是Runnable的子类。
JobHandler.run的代码:

private class JobHandler(job: Job) extends Runnable with Logging {
  import JobScheduler._
   def run() {
    try {
      ...
      if (_eventLoop != null) {
        ...
        PairRDDFunctions.disableOutputSpecValidation.withValue(true) {
          job.run()
        }
        ...
      } 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)
    }
  }
}

  Job.run的代码:

def run() {
  _result = Try(func())
}

  此处,执行了Job中封装的业务逻辑。
阅读全文
0 0
原创粉丝点击