Spark Streaming启动&DStreamGraph源码分析

  在github上看到一个十分好的总结:https://github.com/lw-lin/CoolplaySpark, 对Spark Streaming整体的设计思想讲的算是个人见过十分好的了,看完之后有种原来如此,看完之后对整体的架构有了较为清晰的认识,不过由于篇幅问题,这个项目文档中是偏总结和思想的灌输,没有过于追究一些细节内容,本文以及后续将在此基础上进行源码的阅读,对细节进行更多的研究,刚好最近项目中产生的一些疑问通过阅读源码也得到了很好的解释,这种感觉真的是不要太美好的呢呢呢!!!强烈建议先看完这个项目文档中的内容,再看源码,有种胸有成竹事半功倍之效。自己建了一个源码交流群:936037639,如果你也在看Spark或是大数据相关框架的源码,可以进群大家互相交流哦,一个人看源码有些细节是真的不容易弄明白的,人多力量大!
  这里先从本人最近项目中遇到的一个场景的简化代码开始进入本次的源码探索之旅:

val updateFunc = (values: Seq[Int], state: Option[Int]) => {
      if (checkDelTime()) {
        if (values.isEmpty) {
          None
        } else {
          Some(values.sum)
        }
      } else {
        val currentCount = values.sum
        val previousCount = state.getOrElse(0)
        Some(currentCount + previousCount)
      }
    }
    
val newUpdateFunc = (iterator: Iterator[(String, Seq[Int], Option[Int])]) => {
      iterator.flatMap(t => updateFunc(t._2, t._3).map(s => (t._1, s)))
    }
    
val sparkConf = new SparkConf().setAppName("KafkaReceiverWordCount").setMaster("local[2]")
val ssc = new StreamingContext(sparkConf,Seconds(5))

val kafkaParmas = Map[String,Object](
  "bootstrap.servers"->"hadoop001:9092",
  "key.deserializer"->classOf[StringDeserializer],
  "value.deserializer"->classOf[StringDeserializer],
  "group.id"->"test-consumer-group",
  "auto.offset.reset"->"latest",
  "enable.auto.commit"->(false: java.lang.Boolean)
)

val topics = Array("hello_topic","flume_kafka_streaming_topic")
val kafkaStream = KafkaUtils.createDirectStream(ssc,PreferConsistent,Subscribe[String,String](topics,kafkaParmas))
val fDStream  = kafkaStream.map(x => {
  val outputs = "topic = "+x.topic() + "value = " + x.value()
  println("|"+outputs+"|")
  x.value()
})
val wc = fDStream.flatMap(_.split(" ")).map((_,1)).updateStateByKey(newUpdateFunc, new HashPartitioner(ssc.sparkContext.defaultParallelism), true)
wc.foreachRDD(rdd=>{
    ...
})

ssc.start()
ssc.awaitTermination()

  以上代码是从kafka消费数据,然后持续统计所有批次的wordcount例子,这一串代码中,只有ssc.start()是发起任务的入口,这一行代码前的所有代码均可以理解为是对应用启动后,所需要执行的任务进行配置,其中有应用的配置即sparkconf相关的配置,还有就是后续实例化DAG Graph所需静态模版的配置(这一部分内容请看开篇提供的spark streaming的相关介绍),即我们在代码中写调用的map、filter、flatmap、updateStateByKey等等这些算子,其实是在配置静态DAG Graph模版,每个批次的数据进行处理的时候,其实就是依据我们编写代码生成的静态DAG Graph模版来生成当前批次真正的DAG Graph实例,通过这个实例进行从后向前的追溯计算,来得到结果,以上这段话是较为笼统的介绍,理解起来可能不是十分清晰,我们还是直接进入ssc.start()中来窥探一番其中的神秘~~
  ssc.start()的源码如下:

def start(): Unit = synchronized {
    state match {
      case INITIALIZED =>
        startSite.set(DStream.getCreationSite())
        StreamingContext.ACTIVATION_LOCK.synchronized {
          StreamingContext.assertNoOtherContextIsActive()
          try {
            validate()

            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 {
            ...
          }
          StreamingContext.setActiveContext(this)
        }
        ...
      case ACTIVE =>
        logWarning("StreamingContext has already been started")
      case STOPPED =>
        throw new IllegalStateException("StreamingContext has already been stopped")
    }
  }

  初次启动时,state默认值就是INITIALIZED,进入匹配的代码块中,先进行一些校验工作忽略,SparkStreamingContext中核心部分主要就是JobSchedulerDStreamGraph,以上代码部分是启动了一个叫streaming-start线程,这段代码后面的{...}部分其实就相当于java中的Runnable中的run方法中的内容,属于一个代码块,我们直接看runInNewThread的源码:

def runInNewThread[T](
      threadName: String,
      isDaemon: Boolean = true)(body: => T): T = {
    @volatile var exception: Option[Throwable] = None
    @volatile var result: T = null.asInstanceOf[T]

    val thread = new Thread(threadName) {
      override def run(): Unit = {
        try {
          result = body
        } catch {
          case NonFatal(e) =>
            exception = Some(e)
        }
      }
    }
    thread.setDaemon(isDaemon)
    thread.start()
    thread.join()
    ...

  很显然,核心是通过线程调用了JobSchedulerstart()方法,而SparkContext中的这个JobScheduler的作用见名知意就是用来做任务调度的,是driver端进行任务调度的东厂大总管!

JobScheduler启动流程分析

  JobScheduler的内部结构其实相对简单,我们通过代码注释来进行介绍:

----JobScheduler.scala

//所有待处理job的容器
private val jobSets: java.util.Map[Time, JobSet] = new ConcurrentHashMap[Time, JobSet] 
//能够同时执行job的并发数设置
private val numConcurrentJobs = ssc.conf.getInt("spark.streaming.concurrentJobs", 1)
//线程池
private val jobExecutor =
ThreadUtils.newDaemonFixedThreadPool(numConcurrentJobs, "streaming-job-executor")
//负责job生成,核心类,JobScheduler是大总管负责调度,这个jobGenerator就是小太监,负责实施总管发布的命令。
private val jobGenerator = new JobGenerator(this)
//时钟
val clock = jobGenerator.clock
//生产消费者模式,进行消息发布,提供给UI相关处理数据,不在本次讨论范围内,后续再进行研究
val listenerBus = new StreamingListenerBus()

// These two are created only when scheduler starts.
// eventLoop not being null means the scheduler has been started and not stopped
//数据处理追踪,在DAG Graph中有所谓的input stream的DStream,这个类是用来记录这些input stream接收数据的处理情况
var receiverTracker: ReceiverTracker = null
// A tracker to track all the input stream information as well as processed record number
// 这个是追踪整体输入数据处理情况的
var inputInfoTracker: InputInfoTracker = null

//进行事件接收和处理
private var eventLoop: EventLoop[JobSchedulerEvent] = null

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

logDebug("Starting JobScheduler")
//实例化eventLoop并启动进行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
// 对每个DStream的处理进度进行追踪的监听 不在本次研究范围内,pass
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
jobGenerator.start()
logInfo("Started JobScheduler")
}
 ...

  这里关于核心还是大总管派出了小太监JobGenerator去执行自己给他赋予的命令,而命令就是告诉他每隔一个批次就产生一批jobs,我们来具体看JobGenerator的源码,关键内容看注释部分:

JobGenerator分析
----JobGenerator

//开篇先声明五个事件类型,不同事件不同的处理逻辑(废话。。。)
private[scheduler] sealed trait JobGeneratorEvent
private[scheduler] case class GenerateJobs(time: Time) extends JobGeneratorEvent
private[scheduler] case class ClearMetadata(time: Time) extends JobGeneratorEvent
private[scheduler] case class DoCheckpoint(
    time: Time, clearCheckpointDataLater: Boolean) extends JobGeneratorEvent
private[scheduler] case class ClearCheckpointData(time: Time) extends JobGeneratorEvent

/**
 * This class generates jobs from DStreams as well as drives checkpointing and cleaning
 * up DStream metadata.
 */
//注意这里JobGenerator的构造函数中入参是有JobScheduler的,小太监办事是会受到大总管的监督的,
//针对小太监处理的结果,大总管需要作出对应的自己的处理。
private[streaming]
class JobGenerator(jobScheduler: JobScheduler) extends Logging {

  private val ssc = jobScheduler.ssc
  private val conf = ssc.conf
  private val graph = ssc.graph

  val clock = ...

  //核心成员,这是一个定时器,就是通过他JobGenerator会不断的定期去产生job,时间就是一个批次
  private val timer = new RecurringTimer(clock, ssc.graph.batchDuration.milliseconds,
    longTime => eventLoop.post(GenerateJobs(new Time(longTime))), "JobGenerator")

  // This is marked lazy so that this is initialized after checkpoint duration has been set
  // in the context and the generator has been started.
  private lazy val shouldCheckpoint = ssc.checkpointDuration != null && ssc.checkpointDir != null

  private lazy val checkpointWriter = if (shouldCheckpoint) {
    new CheckpointWriter(this, ssc.conf, ssc.checkpointDir, ssc.sparkContext.hadoopConfiguration)
  } else {
    null
  }

  // eventLoop is created when generator starts.
  // This not being null means the scheduler has been started and not stopped
  //又见到了熟悉的eventLoop,事件容器
  private var eventLoop: EventLoop[JobGeneratorEvent] = null

  // last batch whose completion,checkpointing and metadata cleanup has been completed
  private var lastProcessedBatch: Time = null

  /** 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,实现对应的processEvent和onError方法
    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()
    
    //如果之前checkpoint过了,再次重启直接加载上次checkpoint的文件进行不想恢复,checkpoint看似美好,其实实际使用的时候是会有问题的,
    //如果上次关闭是由于bug导致,而这次我们修改了代码逻辑,那么如果再次启动还是会用之前的checkpoint的内容,导致其实运行的代码还是老的代码,
    //想要新代码生效必须删除checkpoint文件,但是这样会导致其它的数据消费记录也GG从而无法从上次失败开始消费数据,还是会有数据损失。
    if (ssc.isCheckpointPresent) {
      restart()
    } else {
    //初次见面启动~
      startFirstTime()
    }
  }
  private def startFirstTime() {
    val startTime = new Time(timer.getStartTime())
    //启动DAG Graph静态模版配置
    graph.start(startTime - graph.batchDuration)
    //开启无敌小陀螺,不知疲惫的定时器
    timer.start(startTime.milliseconds)
    logInfo("Started JobGenerator at " + startTime)
  }
...

  以上代码为主要的关于JobGenerator的成员和start方法的介绍,主要就是启动定时器的工作,定时器线程启动后会每个批次提交一个生成Job的时间插入到eventLoop中,然后对事件处理后生成对应jobs,返回生成的jobs插入到JobScheduler中的jobSets中,提交到线程池中等待处理。

DStreamGraph解析

  回到上面jobGenerator启动方法中,初次启动既然调用了graph.start(...)那么我们继续看DStreamGraph中关于start()的源码:

def start(time: Time) {
    this.synchronized {
      if (zeroTime != null) {
        throw new Exception("DStream graph computation already started")
      }
      zeroTime = time
      startTime = time
      outputStreams.foreach(_.initialize(zeroTime))
      outputStreams.foreach(_.remember(rememberDuration))
      outputStreams.foreach(_.validateAtStart)
      inputStreams.par.foreach(_.start())
    }
  }

  很显然这里是关于DStreamGraph中关于outputStreaminputStream的配置,注意由于DStream也是一种模版,所以这里既是对DStream模版内容的配置,主要配置的就是缓存时间之类的内容。
代码到这里,已经基本介绍了关于SparkStreamingContext中的两巨头之JobScheduler启动的流程,但是另外一个重点关于DStreamGraph的描述只是它的作用和功能等,但是它又是什么时候产生的呢?接下来对此进行解释,我们先继续回到SparkStreamingContext的源代码,其中关于DStreamGraph的声明为:

private[streaming] val graph: DStreamGraph = {
    if (isCheckpointPresent) {
      cp_.graph.setContext(this)
      cp_.graph.restoreCheckpointData()
      cp_.graph
    } else {
      require(batchDur_ != null, "Batch duration for StreamingContext cannot be null")
      val newGraph = new DStreamGraph()
      newGraph.setBatchDuration(batchDur_)
      newGraph
    }
  }

  这里只是在创建SparkStreamingContext的时候顺带创建的一个空的DStreamGraph对象,然后设置了一下它的批处理时间(直接从上次恢复的不看)。
  文章最开头说的,关于我们平时写的sparkstreaming的代码中的各种transformation以及action操作,只是在编写静态DAG Graph的模版,那么这个模版是如何随着我们编写的代码来生成的呢?这里我们需要先看DStreamGraph这个类的成员的基本情况:

final private[streaming] class DStreamGraph extends Serializable with Logging {

  private val inputStreams = new ArrayBuffer[InputDStream[_]]()
  private val outputStreams = new ArrayBuffer[DStream[_]]()

  var rememberDuration: Duration = null
  var checkpointInProgress = false

  var zeroTime: Time = null
  var startTime: Time = null
  var batchDuration: Duration = null

  def start(time: Time) {
    this.synchronized {
      if (zeroTime != null) {
        throw new Exception("DStream graph computation already started")
      }
      zeroTime = time
      startTime = time
      outputStreams.foreach(_.initialize(zeroTime))
      outputStreams.foreach(_.remember(rememberDuration))
      outputStreams.foreach(_.validateAtStart)
      inputStreams.par.foreach(_.start())
    }
  }
...

  这个类的构造其实很简单,核心就是inputStreamsoutputStreams这两个数组,这两兄弟是构造DStreamGraph模版的主力军,不过一开始在SparkStreamingContext创建DStreamGraph的时候很显然这些内容都是空的,而我们编写的各种mapfilterprintupdateStateByKey等等算子,就是来进行这两兄弟填充的,那么是如何填充的呢?这里我们先看看几个常见算子的源码:

map算子: val c = rdda.map(…)

def map[U: ClassTag](mapFunc: T => U): DStream[U] = ssc.withScope {
    new MappedDStream(this, context.sparkContext.clean(mapFunc))
  }

得到的是一个MappedDStream对象,继续看这个对象的代码:

class MappedDStream[T: ClassTag, U: ClassTag] (
    parent: DStream[T],
    mapFunc: T => U
  ) extends DStream[U](parent.ssc) {

  override def dependencies: List[DStream[_]] = List(parent)

  override def slideDuration: Duration = parent.slideDuration

  override def compute(validTime: Time): Option[RDD[U]] = {
    parent.getOrCompute(validTime).map(_.map[U](mapFunc))
  }
}

  这个类继承自DStream,我们先注意看MappedDStream中的dependencies,这里的parent就是声明MappedDStream时的对应DStream,这里就是代码rdda.map(...)rdda的类型,也即每次调用map算子的时候,生成的新的DStream均会在自己的dependencies自己的老父亲是谁,类似的其它所有的算子,都是这种结构,只要由一个DStream生成一个新的DStream,均会记录之间的关系,这样下来我们编写的处理逻辑所有算子之间就形成了一条龙了,从数据源到最后的foreachRDD这种算子。
  不过以上说的DStream之间记录老父亲互相之间建立联系的方式,只是在DStream之间,并没有看到和DStreamGraph有半毛钱关系。和DStreamGraph建立关系其实是在最后调用action类型算子的时候,例如上述的foreachRDD算子,我们来看看它和普通的transformation有什么区别呢,方法的源码如下:

def foreachRDD(foreachFunc: RDD[T] => Unit): Unit = ssc.withScope {
    val cleanedF = context.sparkContext.clean(foreachFunc, false)
    this.foreachRDD((r: RDD[T], t: Time) => cleanedF(r))
  }
def foreachRDD(foreachFunc: (RDD[T], Time) => Unit): Unit = ssc.withScope {
    new ForEachDStream(this, context.sparkContext.clean(foreachFunc, false)).register()
  }

  可以看到,这里得到的是一个ForEachDStream类,然而仔细发现它还有一个register()小尾巴,我们看看它偷偷做了什么:

private[streaming] def register(): DStream[T] = {
    ssc.graph.addOutputStream(this)
    this
  }
def addOutputStream(outputStream: DStream[_]) {
    this.synchronized {
      outputStream.setGraph(this)
      outputStreams += outputStream
    }
  }

  这里就发现原来这里出现了DStreamGraph的身影!这里调用graph.addOutputStream(this)将我们进行foreachRDD得到的DStream插入到了outputStream数组中,如果再看其它action类算子和transformation类算子,会发现他们的逻辑和这里都一样,进行到这里就会发现只要调用了action的算子,都会被作为outputStream加入到DStreamGraph中。
  以上DStreamGraph中的两兄弟outputStream已经知道如何得到数据的了,那么另外一个inputStream又是如何得到填充的呢?让我们先看看对应的DStreamGraph中给inputStream插入数据的方法为:

def addInputStream(inputStream: InputDStream[_]) {
    this.synchronized {
      inputStream.setGraph(this)
      inputStreams += inputStream
    }
  }

  再通过Find usage让我们看看哪里会调用这个方法,发现只有在一个InputDStream的类中有调用,而且是在这个类创建时自动调用的,代码如下:

ssc.graph.addInputStream(this)

  而这个InputDStream的实现类主要是:DirectKafkaInputDStreamFileInputDStreamSocketInputDStream等,很显然这些都是数据来源的DStream,所以就是说在刚开始声明这些类的时候就会自动被加入到DStreamGraph中的inputStream
至此,我们需要注意到其实inputStreamoutputStream分别就是我们处理逻辑的数据入口和最终输出的出口,DStreamGraph中记录了入口和出口,而且之前已经说过每个DStream之间通过dependencies已经建立过了联系,所以知道了开头和结尾,中间的关系又有了,那么至此一张无形的关系网模版已经悄然形成!!!
  以上就是关于DStreamGraph在应用启动时的创建过程了,再加上说明的JobScheduler的逻辑,我们的关于项目启动的主体流程已经搞定~

个人收获

  看完这一部分源码,体会最深的是内部的各种生产者消费者模式的使用对事件的处理,DStreamGraph的设计也很巧妙,各种类的封装抽象很到位,不过巧妙带来的问题就是作为源码阅读者我看起来有点费劲,到现在还没有找到InputStream是在什么时候设置的缓存的???至于对具体优化和编写sparkstreaming应用时的一些感悟,貌似并没有任何关系。。。算是提示自己的思想吧,训练思维!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值