本节主要内容:

一、SparkStreaming Job生成深度思考

二、SparkStreaming Job生成源码解析

JobScheduler的地位非常的重要,所有的关键都在JobScheduler,它的重要性就相当于是Spark Core当中的DAGScheduler,因此,我们要花重点在JobScheduler上面。

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

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

// 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.大括号中的代码作为一个匿名函数在新的线程中执行。Sparkstreaming运行时至少需要两条线程,其中一条用于一直循环接收数据,现在所说的至少两条线程和上边开辟一条新线程运行scheduler.start()并没有关系。Sparkstreaming运行时至少需要两条线程是用于作业处理的,上边的代码开辟新的线程是在调度层面的中,不论Sparkstreaming程序运行时指定多少线程,这里都会开辟一条新线程,之间没有一点关系。

2.每一条线程都有自己私有的属性,在这里给新的线程设置私有的属性,这些属性不会影响主线程中的。

sparkContext.setCallSite(startSite.get)
  sparkContext.clearJobGroup()
sparkContext.setLocalProperty(SparkContext.SPARK_JOB_INTERRUPT_ON_CANCEL, "false")

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

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

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开发中和数据库、hbase、radis、javaEE交互一般都采用java,所以开发大型spark项目大部分都是scala+java的方式进行开发。

JobGenerator和线程池在JobSchedule在实例化的时候就已经实例化了,而eventloop和receiverTracker是在调用JobGenerator的start方法时才被实例化。def start(): Unit = synchronized {


  if (eventLoop != null) return // scheduler has already been started
  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()
  receiverTracker= new ReceiverTracker(ssc)
  receiverTracker.start()
  jobGenerator.start()
}

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

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

Dstream的action级别的操作转过来还是会调用foreachRDD这个方法,生动的说明在对Dstream操作的时候其实还是对RDD的操作。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()
    }
  }
  foreachRDD(context.sparkContext.clean(foreachFunc), displayInnerRDDOps = false)
}

上边代码中foreachFunc这个方法是对Dstream action级别的方法的进一步封装,增加了如下代码,在运行spark streaming程序时对这些输出很熟悉。

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

foreachRDD方法,转过来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 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的计算,调用Job的run方法时,真正的作业就触发了。foreachFunc(rdd, time)中的rdd其实就是通过DstreamGraph中最后一个Dstream来决定的。

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

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

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

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

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

generateJobs方法的代码:

private def generateJobs(time: Time) {
  SparkEnv.set(ssc.env)
  Try {
    jobScheduler.receiverTracker.allocateBlocksToBatch(time)
    graph.generateJobs(time) // generate jobs using allocated block

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

defgenerateJobs(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就是前面说ForEachDstream,generateJob(time)方法就是ForEachDstream中的generateJob(time)方法。

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

再来看看JobGenerator的generateJobs方法:

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(time, jobs, streamIdToInputInfos),其中streamIdToInputInfos就是接收的数据的元数据。

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

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

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

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

_eventLoop.post(JobStarted(job, clock.getTimeMillis()))

PairRDDFunctions.disableOutputSpecValidation.withValue(true) {

  job.run()
}
_eventLoop = eventLoop
if (_eventLoop != null) {
  _eventLoop.post(JobCompleted(job, clock.getTimeMillis()))
}

Spark发行版笔记7

新浪微博:http://weibo.com/ilovepains

微信公众号:DT_Spark

博客:http://blog.sina.com.cn/ilovepains

手机:18610086859

QQ:1740415547

邮箱:18610086859@vip.126.com

 

贯通Spark Streaming JobScheduler内幕实现和深入思考的更多相关文章

  1. 7.spark Streaming 技术内幕 : 从DSteam到RDD全过程解析

    原创文章,转载请注明:转载自 听风居士博客(http://www.cnblogs.com/zhouyf/)   上篇博客讨论了Spark Streaming 程序动态生成Job的过程,并留下一个疑问: ...

  2. 贯通Spark Streaming流计算框架的运行源码

    本章节内容: 一.在线动态计算分类最热门商品案例回顾 二.基于案例贯通Spark Streaming的运行源码 先看代码(源码场景:用户.用户的商品.商品的点击量排名,按商品.其点击量排名前三): p ...

  3. Dream_Spark-----Spark 定制版:005~贯通Spark Streaming流计算框架的运行源码

    Spark 定制版:005~贯通Spark Streaming流计算框架的运行源码   本讲内容: a. 在线动态计算分类最热门商品案例回顾与演示 b. 基于案例贯通Spark Streaming的运 ...

  4. Spark Streaming揭秘 Day15 No Receivers方式思考

    Spark Streaming揭秘 Day15 No Receivers方式思考 在前面也有比较多的篇幅介绍了Receiver在SparkStreaming中的应用,但是我们也会发现,传统的Recei ...

  5. Spark Streaming源码解读之JobScheduler内幕实现和深度思考

    本期内容 : JobScheduler内幕实现 JobScheduler深度思考 JobScheduler 是整个Spark Streaming调度的核心,需要设置多线程,一条用于接收数据不断的循环, ...

  6. 基于案例贯通 Spark Streaming 流计算框架的运行源码

    本期内容 : Spark Streaming+Spark SQL案例展示 基于案例贯穿Spark Streaming的运行源码 一. 案例代码阐述 : 在线动态计算电商中不同类别中最热门的商品排名,例 ...

  7. Spark streaming技术内幕6 : Job动态生成原理与源码解析

    原创文章,转载请注明:转载自 周岳飞博客(http://www.cnblogs.com/zhouyf/)  Spark streaming 程序的运行过程是将DStream的操作转化成RDD的操作,S ...

  8. 9. Spark Streaming技术内幕 : Receiver在Driver的精妙实现全生命周期彻底研究和思考

        原创文章,转载请注明:转载自 听风居士博客(http://www.cnblogs.com/zhouyf/)       Spark streaming 程序需要不断接收新数据,然后进行业务逻辑 ...

  9. 6.Spark streaming技术内幕 : Job动态生成原理与源码解析

    原创文章,转载请注明:转载自 周岳飞博客(http://www.cnblogs.com/zhouyf/)   Spark streaming 程序的运行过程是将DStream的操作转化成RDD的操作, ...

随机推荐

  1. python中的is, ==与对象的相等判断

    在java中,对于两个对象啊a,b,若a==b表示,a和b不仅值相等,而且指向同一内存位置,若仅仅比较值相等,应该用equals.而在python中对应上述两者的是‘is’ 和‘==’. (1) py ...

  2. Ubuntu 16.04 apt-get搭建LAMP环境

    本文重点介绍在Ubuntu中使用apt-get安装LAMP(Ubuntu 16.04,Apache2.4.18,mysql5.7.12,php7.0.4)环境,所以不再介绍如何安装Ubuntu. 安装 ...

  3. 【JBPM4】查询流程实例当前所在节点

    示例代码: ProcessEngine processEngine = Configuration.getProcessEngine(); ExecutionService executionServ ...

  4. 【转载】Web Service 的工作原理

    http://www.cnblogs.com/Jessy/p/3528341.html Web Service基本概念 Web Service也叫XML Web Service WebService是 ...

  5. 走进 Cake for .NET

    一.什么是 Cake Cake(C# Make) 是一个使用 C#  DSL 面向 Task 的跨平台构建自动化系统,像编译代码,复制文件和文件夹,运行单元测试,压缩文件和构建 NuGet 包. 更多 ...

  6. 基于kbengine 0.4.20

    前言: v0.0.1 2015-04-10 誉小痕(shawhen2012@hotmail.com) v0.0.2 2015-04-12 誉小痕(shawhen2012@hotmail.com) ch ...

  7. 1. Maven - 开始

    今天开始记录Maven的学习过程. ☞ 相关明细 操作系统:win7 参考书籍:<maven-definitive-guide_zh>(Maven权威指南) maven版本:3.3.3 ☞ ...

  8. 线段树+二进制位拆分【CF242E】XOR on Segment

    Description 给定一个长为\(n(n<=10^5)\)的数组 数组里的数不超过\(10^6\) 有两种操作: 1:求\(sum[l,r]\); 2:对\([l,r]\)中的所有数和\( ...

  9. VB查询数据库之登陆窗体——机房收费总结(一)

    机房收费系统已经做了很长一段时间了,虽然到目前为止,仍然没有结束,但已经结节尾声了.我感觉现在有必要回首总结一下整个机房收费系统. 除了结账做了一半,报表接触一点之外,其他的都基本上差不多了.从做过的 ...

  10. 【20181024T1】小C的数组【二分+dp】

    题面 [正解] 题目求最大的最小,可以二分 设\(f_i\)表示第i个数不改满足条件需要改多少个 可以从j转移,那么[j+1,i]的均匀摊开后的差值应该在范围内 容易推出方程: \(f_i=min_{ ...