在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
中核心部分主要就是JobScheduler
和DStreamGraph
,以上代码部分是启动了一个叫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()
...
很显然,核心是通过线程调用了JobScheduler
的start()
方法,而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
中关于outputStream
和inputStream
的配置,注意由于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())
}
}
...
这个类的构造其实很简单,核心就是inputStreams
和outputStreams
这两个数组,这两兄弟是构造DStreamGraph
模版的主力军,不过一开始在SparkStreamingContext
创建DStreamGraph
的时候很显然这些内容都是空的,而我们编写的各种map
、filter
、print
、updateStateByKey
等等算子,就是来进行这两兄弟填充的,那么是如何填充的呢?这里我们先看看几个常见算子的源码:
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
的实现类主要是:DirectKafkaInputDStream
、FileInputDStream
、SocketInputDStream
等,很显然这些都是数据来源的DStream
,所以就是说在刚开始声明这些类的时候就会自动被加入到DStreamGraph
中的inputStream
。
至此,我们需要注意到其实inputStream
和outputStream
分别就是我们处理逻辑的数据入口和最终输出的出口,DStreamGraph
中记录了入口和出口,而且之前已经说过每个DStream
之间通过dependencies
已经建立过了联系,所以知道了开头和结尾,中间的关系又有了,那么至此一张无形的关系网模版已经悄然形成!!!
以上就是关于DStreamGraph
在应用启动时的创建过程了,再加上说明的JobScheduler
的逻辑,我们的关于项目启动的主体流程已经搞定~
个人收获
看完这一部分源码,体会最深的是内部的各种生产者消费者模式的使用对事件的处理,DStreamGraph
的设计也很巧妙,各种类的封装抽象很到位,不过巧妙带来的问题就是作为源码阅读者我看起来有点费劲,到现在还没有找到InputStream
是在什么时候设置的缓存的???至于对具体优化和编写sparkstreaming应用时的一些感悟,貌似并没有任何关系。。。算是提示自己的思想吧,训练思维!