Spark Streaming运行流程及源码解析(二)

目录

  • 写在前面
  • 开干
  • 启动流处理引擎
    • StreamingContext的创建
    • outputOperator算子注册
    • StreamingContext的启动
  • 接收并存储数据
    • Driver端ReceiverTracker的操作
    • Executor端ReceiverSupervisor的操作
    • 开始接收数据、存储数据
  • 生成job、执行job
    • JobGenerator介绍
    • 生成job
    • 提交执行job
  • 输出数据

Spark Streaming源码流程解析。

写在前面

以下是我自己梳理了一遍Spark Streaming程序运行的流程,过程可能有点细、有点乱。

大家可以一边看我写的流程、一边跟着步骤点进去看源码,这样就不会太乱了。

跟着源码走一遍以后,对Spark Streaming的理解也就很清晰了。

这篇文章是自己看源码过程的记录,如果有理解偏差的部分,欢迎交流指正。

开干

以如下的WordCount代码展开叙述:

// 创建SparkConf,配置master为local
val conf = new SparkConf()
    .setMaster("local[2]")
    .setAppName("socket-streaming")
// 实例化StreamingContext
val ssc = new StreamingContext(conf, Seconds(2))
// 创建一个ReceiverInputDStream对象
val lines = ssc socketTextStream("localhost", 1234)
// 进行逻辑处理、输出
lines
    .flatMap(_.split(" "))
    .map((_, 1))
    .reduceByKey(_ + _)
    .print()
// 启动
ssc.start()
// 等待执行停止
ssc.awaitTermination()

以上代码启动后,可以接受1234端口收到的消息,然后按空格将句子切分成单词,之后对单词进行计数,每隔两秒计算输出一次结果。

接下来以我们写的WordCount代码为辅,从启动流处理引擎、接收并存储数据、处理数据、输出数据依次走一遍源码。

启动流处理引擎

StreamingContext的创建

val ssc = new StreamingContext(conf, Seconds(2))开始,这里会实例化StreamingContext对象。

先看一下StreamingContext中的一些重要的变量。

// SparkContext实例,Spark上下文,可以通过直接传参获得,
// 也可以通过sparkConf创建,或从checkpoint中取到
private[streaming] val sc: SparkContext = {
    if (_sc != null) {
        _sc
    } else if (isCheckpointPresent) {
        SparkContext.getOrCreate(_cp.createSparkConf())
    } else {
        throw new SparkException("Cannot create StreamingContext without a SparkContext")
    }
}

// DStreamGraph用来管理DStream的依赖,
// 创建时将StreamingContext实例绑定到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
    }
}

// JobScheduler用来生成和调度任务,
// 也会将StreamingContext实例绑定到自己身上
private[streaming] val scheduler = new JobScheduler(this)

// 批处理间隔
batchDuration

实例化StreamingContext时,这些变量都将会被实例化。

既然这样,就顺势也看一下DStreamGraph和JobScheduler中一些重要的变量。

先看一下DStreamGraph中的重要变量:

// inputStreams是输入数据源的集合,
// 输入数据源中有对应的receive方法用来接收数据
private val inputStreams = new ArrayBuffer[InputDStream[_]]()

// outputStreams就是DStream的集合,
// 我们调用的各个算子最终都会根据依赖生成的DStream,
// outputOperator型的算子都会注册到这里来
private val outputStreams = new ArrayBuffer[DStream[_]]()

再看看JobScheduler中的重要变量:

// 生成的job集合,以time为key,jobset为value的Map
private val jobSets: java.util.Map[Time, JobSet] = new ConcurrentHashMap[Time, JobSet]

// 一个线程池,用来执行job
private val jobExecutor =
    ThreadUtils.newDaemonFixedThreadPool(numConcurrentJobs, "streaming-job-executor")

// JobGenerator用来生成job
private val jobGenerator = new JobGenerator(this)

// Driver端用于管理Receiver的总管家
var receiverTracker: ReceiverTracker = null

// 事件循环,用来处理JobScheduler相关的事件
// 本质是以LinkedBlockingDeque一个队列
private var eventLoop: EventLoop[JobSchedulerEvent] = null

接下来执行val lines = ssc.socketTextStream("localhost", 1234)

如下所示,socketTextStream()会调用socketStream(),socketStream方法中会new一个SocketInputDStream,SocketInputDStream用于接收数据

def socketTextStream(
    hostname: String,
    port: Int,
    storageLevel: StorageLevel = StorageLevel.MEMORY_AND_DISK_SER_2
): ReceiverInputDStream[String] = withNamedScope("socket text stream") {
    socketStream[String](hostname, port, SocketReceiver.bytesToLines, storageLevel)
}

def socketStream[T: ClassTag](
    hostname: String,
    port: Int,
    converter: (InputStream) => Iterator[T],
    storageLevel: StorageLevel
): ReceiverInputDStream[T] = {
    new SocketInputDStream[T](this, hostname, port, converter, storageLevel)
}

追踪一下SocketInputDStream的继承关系,发现它继承于ReceiverInputDStream,ReceiverInputDStream又继承于InputDStream。

InputDStream中有ssc.graph.addInputStream(this)这么一行代码,将InputDStream添加到DStreamGraph中的inputStreams中。

所以在new SocketInputDStream时,InputDStream就添加到DStreamGraph中了。(这个找了挺久才找见的,之前一直不知道InputDStream什么时候添加进去的)

outputOperator算子注册

接着执行如下几行代码

lines
    .flatMap(_.split(" "))
    .map((_, 1))
    .reduceByKey(_ + _)
    .print()

上面每个算子的调用会生成相互依赖的DStream: FlatMappedDStream、MappedDStream、ShuffledDStream。

只有到print()(outputOperator类算子)调用的时候,才会将DStream注册到DStreamGraph中的outputStreams中,之后DStreamGraph才能根据依赖关系生成job。

接下来跟进一下print()

// 以下的方法是依次调用的
def print(): Unit = ssc.withScope {
    print(10)
}

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(s"Time: $time")
            println("-------------------------------------------")
            firstNum.take(num).foreach(println)
            if (firstNum.length > num) println("...")
            println()
            // scalastyle:on println
        }
    }
    foreachRDD(context.sparkContext.clean(foreachFunc), displayInnerRDDOps = false)
}

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

private[streaming] def register(): DStream[T] = {
    ssc.graph.addOutputStream(this)
    this
}

这里调用了register()将DStream注册到DStreamGraph的outputStreams中

到这里就将我们的业务逻辑什么的都封装到DStream中了

StreamingContext的启动

接下来走ssc.start()启动StreamingContext

StreamingContext的start方法中主要就是调用scheduler.start()启动了JobScheduler

接下来在看看JobScheduler的start方法

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

    logDebug("Starting JobScheduler")
    // 事件环主要接收调度JobSchedulerEvent事件
    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()
    // 添加监听
    for {
        inputDStream <- ssc.graph.getInputStreams
        rateController <- inputDStream.rateController
    } ssc.addStreamingListener(rateController)

    // 监听总线启动
    listenerBus.start()
    receiverTracker = new ReceiverTracker(ssc)
    inputInfoTracker = new InputInfoTracker(ssc)

    val executorAllocClient: ExecutorAllocationClient = ssc.sparkContext.schedulerBackend match {
        case b: ExecutorAllocationClient => b.asInstanceOf[ExecutorAllocationClient]
        case _ => null
    }
    // 管理分配Executor
    executorAllocationManager = ExecutorAllocationManager.createIfEnabled(
        executorAllocClient,
        receiverTracker,
        ssc.conf,
        ssc.graph.batchDuration.milliseconds,
        clock)
    executorAllocationManager.foreach(ssc.addStreamingListener)
    // 启动ReceiverTracker
    receiverTracker.start()
    // 启动JobGenerator
    jobGenerator.start()
    executorAllocationManager.foreach(_.start())
    logInfo("Started JobScheduler")
}

JobScheduler中主要启动了ReceiverTracker和JobGenerator。

ReceiverTracker通知Executor启动Receiver,管理Receiver的执行,与Receiver交互。

JobGenerator用于生成job,执行job。

这两个类分别代表了接收并存储数据生成job、执行job

接下来先看接收并存储数据

接收并存储数据

Driver端ReceiverTracker的操作

先从ReceiverTracker.start()说起。

def start(): Unit = synchronized {
    if (isTrackerStarted) {
        throw new SparkException("ReceiverTracker already started")
    }

    if (!receiverInputStreams.isEmpty) {
        // 建立RPC终端
        endpoint = ssc.env.rpcEnv.setupEndpoint(
            "ReceiverTracker", new ReceiverTrackerEndpoint(ssc.env.rpcEnv))
        // 加载Receiver
        if (!skipReceiverLaunch) launchReceivers()
        logInfo("ReceiverTracker started")
        trackerState = Started
    }
}

// 加载Receiver
private def launchReceivers(): Unit = {
    // 从inputStreams中获取receivers
    val receivers = receiverInputStreams.map { nis =>
        val rcvr = nis.getReceiver()
        rcvr.setReceiverId(nis.id)
        rcvr
    }

    runDummySparkJob()
    // 发送StartAllReceivers的消息
    logInfo("Starting " + receivers.length + " receivers")
    endpoint.send(StartAllReceivers(receivers))
}

ReceiverTracker先建立RPC终端点准备通信,监听、回复与Receiver相关的信息。

然后调用launchReceivers(),launchReceivers中的receiverInputStreams是从DStreamGraph中获取的InputStream的集合。通过InputStream获取Receiver,然后发送StartAllReceivers消息。

这里的StartAllReceivers是发给endpoint的,也就是发给ReceiverTrackerEndpoint实例,也就相当于是发给自己的。

ReceiverTrackerEndpoint的receive方法通过模式匹配进行消息的接收,在收到StartAllReceivers后,会根据资源调度分配适合启动Receiver的位置,然后调用本类的startReceiver()

override def receive: PartialFunction[Any, Unit] = {
    // Local messages
    case StartAllReceivers(receivers) =>
    // 分配适合的位置
    val scheduledLocations = schedulingPolicy.scheduleReceivers(receivers, getExecutors)
    for (receiver <- receivers) {
        val executors = scheduledLocations(receiver.streamId)
        updateReceiverScheduledExecutors(receiver.streamId, executors)
        receiverPreferredLocations(receiver.streamId) = receiver.preferredLocation
        startReceiver(receiver, executors)
    }
}

接下来看看startReceiver方法

private def startReceiver(
    receiver: Receiver[_],
    scheduledLocations: Seq[TaskLocation]): Unit = {
    def shouldStartReceiver: Boolean = {
        !(isTrackerStopping || isTrackerStopped)
    }
    val receiverId = receiver.streamId
    if (!shouldStartReceiver) {
        onReceiverJobFinish(receiverId)
        return
    }
    val checkpointDirOption = Option(ssc.checkpointDir)
    val serializableHadoopConf =
    new SerializableConfiguration(ssc.sparkContext.hadoopConfiguration)
    // 封装在worker节点启动receiver的方法
    val startReceiverFunc: Iterator[Receiver[_]] => Unit =
    (iterator: Iterator[Receiver[_]]) => {
        if (!iterator.hasNext) {
            throw new SparkException(
                "Could not start receiver as object not found.")
        }
        if (TaskContext.get().attemptNumber() == 0) {
            val receiver = iterator.next()
            assert(iterator.hasNext == false)
            val supervisor = new ReceiverSupervisorImpl(
                receiver, SparkEnv.get, serializableHadoopConf.value, checkpointDirOption)
            supervisor.start()
            supervisor.awaitTermination()
        } else {
        }
    }
    // 使用ScheduledLocations创建RDD以在Spark作业中运行接收器
    val receiverRDD: RDD[Receiver[_]] =
    if (scheduledLocations.isEmpty) {
        ssc.sc.makeRDD(Seq(receiver), 1)
    } else {
        val preferredLocations = scheduledLocations.map(_.toString).distinct
        ssc.sc.makeRDD(Seq(receiver -> preferredLocations))
    }
    // 提交启动receiver的job到spark核心进行启动
    val future = ssc.sparkContext.submitJob[Receiver[_], Unit, Unit](
        receiverRDD, startReceiverFunc, Seq(0), (_, _) => Unit, ())
    // We will keep restarting the receiver job until ReceiverTracker is stopped
    future.onComplete {
        ...
    }(ThreadUtils.sameThread)
    logInfo(s"Receiver ${receiver.streamId} started")
}

stratReceiver方法先封装了启动receiver的方法和RDD,然后提交给spark核心进行执行。

上面代码startReceiverFunc中,封装了创建和启动ReceiverSupervisor的操作。

ReceiverSupervisor是Executor端Receiver的管理者,负责监督和管理Executor中的Receiver的运行

Executor端ReceiverSupervisor的操作

接下来追踪ReceiverSupervisor的start方法。

/** Start the supervisor */
def start() {
    onStart()
    startReceiver()
}

// ReceiverSupervisorImpl中的onStart方法
override protected def onStart() {
    registeredBlockGenerators.asScala.foreach { _.start() }
}

// ReceiverSupervisor的方法,用于启动Receiver
def startReceiver(): Unit = synchronized {
    try {
        if (onReceiverStart()) {
            receiverState = Started
            // 启动receiver,开始接收数据
            receiver.onStart()
        } else {
            ...
        }
    } catch {
    }
}

在onStart方法中,可以看到一个registeredBlockGenerators集合,它是BlockGenerator的集合。

BlockGenerator是Receiver中比较重要的一个类,用于将我们收到的单条数据写入buffer,然后定时将buffer封装为块,进行存储和汇报给Driver。

接下来详细看一下它的变量和方法

// listener创建BlockGenerator时传进来的监听器,
// 用来监听块相关事件:onAddData、onGenerateBlock、onPushBlock
listener: BlockGeneratorListener

// 是一个ArrayBuffer,用来暂存接收到的数据
@volatile private var currentBuffer = new ArrayBuffer[Any]

// 一个队列,用来存取封装好的Block块
private val blocksForPushing = new ArrayBlockingQueue[Block](blockQueueSize)

// 定时器,定时将currentBuffer中的数据封装为Block,然后推到blocksForPushing里面
private val blockIntervalTimer =
new RecurringTimer(clock, blockIntervalMs, updateCurrentBuffer, "BlockGenerator")

// blocksForPushing队列的大小
private val blockQueueSize = conf.getInt("spark.streaming.blockQueueSize", 10)

// 这是一个线程,用来从blocksForPushing中取出Block,然后进行存储,汇报ReceiverTracker
private val blockPushingThread = new Thread() { override def run() { keepPushingBlocks() } }

// 按照时间生成块,然后将块推到blocksForPushing中
private def updateCurrentBuffer(time: Long): Unit = {
    try {
        var newBlock: Block = null
        synchronized {
            if (currentBuffer.nonEmpty) {
                val newBlockBuffer = currentBuffer
                currentBuffer = new ArrayBuffer[Any]
                val blockId = StreamBlockId(receiverId, time - blockIntervalMs)
                listener.onGenerateBlock(blockId)
                newBlock = new Block(blockId, newBlockBuffer)
            }
        }
        if (newBlock != null) {
            blocksForPushing.put(newBlock)  // put is blocking when queue is full
        }
    } catch {
    }
}

// 推送块
private def keepPushingBlocks() {
    ...
    while (!blocksForPushing.isEmpty) {
        val block = blocksForPushing.take()
        logDebug(s"Pushing block $block")
        // 调用本类的pushBlock方法
        pushBlock(block)
        logInfo("Blocks left to push " + blocksForPushing.size())
    }
    logInfo("Stopped block pushing thread")
} catch {
    case ie: InterruptedException =>
    logInfo("Block pushing thread was interrupted")
    case e: Exception =>
    reportError("Error in block pushing thread", e)
}
}

// 推送块
private def pushBlock(block: Block) {
    listener.onPushBlock(block.id, block.buffer)
    logInfo("Pushed block " + block.id)
}

大体来说,BlockGenerator中使用了一个ArrayBuffer来不断的接收存储数据,然后会按时将ArrayBuffer中的数据封装为Block。另有一个队列ArrayBlockingQueue来存取Block,一边存一边取,这样实现了单条数据的接收与存储。

再接着看pushBlock的操作。其中调用了listener.onPushBlock()。

listener是构造BlockGenerator时传进来的,使用的是ReceiverSupervisorImpl中的defaultBlockGeneratorListener。

private val defaultBlockGeneratorListener = new BlockGeneratorListener {
    def onAddData(data: Any, metadata: Any): Unit = { }

    def onGenerateBlock(blockId: StreamBlockId): Unit = { }

    def onError(message: String, throwable: Throwable) {
        reportError(message, throwable)
    }
    // 推块的时候调用,它又会调用ReceiverSupervisorImpl.pushArrayBuffer()
    def onPushBlock(blockId: StreamBlockId, arrayBuffer: ArrayBuffer[_]) {
        pushArrayBuffer(arrayBuffer, None, Some(blockId))
    }
}

// 将接收到的数据的ArrayBuffer作为数据块存储到Spark的内存中
def pushArrayBuffer(
    arrayBuffer: ArrayBuffer[_],
    metadataOption: Option[Any],
    blockIdOption: Option[StreamBlockId]
) {
    // 调用pushAndReportBlock()
    pushAndReportBlock(ArrayBufferBlock(arrayBuffer), metadataOption, blockIdOption)
}

// 将块数据进行存储,然后汇报给Driver
def pushAndReportBlock(
    receivedBlock: ReceivedBlock,
    metadataOption: Option[Any],
    blockIdOption: Option[StreamBlockId]
) {
    val blockId = blockIdOption.getOrElse(nextBlockId)
    val time = System.currentTimeMillis
    // 这步会真正的存储数据
    val blockStoreResult = receivedBlockHandler.storeBlock(blockId, receivedBlock)
    logDebug(s"Pushed block $blockId in ${(System.currentTimeMillis - time)} ms")
    val numRecords = blockStoreResult.numRecords
    val blockInfo = ReceivedBlockInfo(streamId, numRecords, metadataOption, blockStoreResult)
    // 将存储结果报告Driver
    if (!trackerEndpoint.askSync[Boolean](AddBlock(blockInfo))) {
        throw new SparkException("Failed to add block to receiver tracker.")
    }
    logDebug(s"Reported block $blockId")
}

listener.onPushBlock会调用pushArrayBuffer(),pushArrayBuffer方法会调用pushAndReportBlock()将数据进行存储,然后汇报给Driver。

这里需要注意一下:BlockGenerator负责单条数据的接收与生成快。这个一会会再说。

开始接收数据、存储数据

BlockGenerator的内部看完以后,接着回到ReceiverSupervisor.start()中来

def start() {
    onStart()
    startReceiver()
}

onStart()方法中启动BlockGenerator,启动块生成的定时器和推送块的线程

def start(): Unit = synchronized {
    if (state == Initialized) {
        state = Active
        blockIntervalTimer.start()
        blockPushingThread.start()
        logInfo("Started BlockGenerator")
    } else {
        throw new SparkException(
            s"Cannot start BlockGenerator as its not in the Initialized state [state = $state]")
    }
}

startReceiver()方法中,调用receiver.onStart(),开始接收数据

def startReceiver(): Unit = synchronized {
    try {
        if (onReceiverStart()) {
            receiverState = Started
            // 启动receiver开始接收数据
            receiver.onStart()
        } else {
            stop("Registered unsuccessfully because Driver refused to start receiver " + streamId, None)
        }
    } catch {
    }
}

以我们一开写的demo中的SocketInputDStream为例,它会生成一个SocketReceiver实例,以下是SocketReceiver的onStart方法。

def onStart() {
    try {
        // 启动socket,开始监听
        socket = new Socket(host, port)
    } catch {
    }
    new Thread("Socket Receiver") {
        setDaemon(true)
        override def run() { receive() }
    }.start()
}

def receive() {
    try {
        // 接收数据
        val iterator = bytesToObjects(socket.getInputStream())
        while(!isStopped && iterator.hasNext) {
            // 将接收到的数据进行存储
            store(iterator.next())
        }
    } catch {
        ...
    } finally {
        onStop()
    }
}

可以看到,onStart中启动了一个线程,开始不断的接收数据,之后会调用store()将接收到的数据进行存储。

这里的store()方法是Receiver中定义的,我们跟进一下。

def store(dataItem: T) {
    supervisor.pushSingle(dataItem)
}

/** Store an ArrayBuffer of received data as a data block into Spark's memory. */
def store(dataBuffer: ArrayBuffer[T]) {
    supervisor.pushArrayBuffer(dataBuffer, None, None)
}

/**
* Store an ArrayBuffer of received data as a data block into Spark's memory.
* The metadata will be associated with this block of data
* for being used in the corresponding InputDStream.
*/
def store(dataBuffer: ArrayBuffer[T], metadata: Any) {
    supervisor.pushArrayBuffer(dataBuffer, Some(metadata), None)
}

/** Store an iterator of received data as a data block into Spark's memory. */
def store(dataIterator: Iterator[T]) {
    supervisor.pushIterator(dataIterator, None, None)
}

会发现有好几个重载的方法,参数不尽相同。

SocketReceiver中调用的是store(dataItem: T)这个方法,它会调用pushSingle将数据添加到BlockGenerator中的currentBuffer中。BlockGenerator再定时将currentBuffer封装为Block,然后调用pushBlock、pushArrayBuffer、pushAndReportBlock对数据进行存储、汇报Driver。

store(dataItem: T)就相当于之前说的接收单条数据进行存储的操作。

另外几个重载方法也都会最终也都会调用pushAndReportBlock数据进行存储,然后报告Driver。这里就不再跟下去了。

数据的接收与存储到这里就结束了。接下来我们在回到JobGenerator解析一下job的生成和执行。

生成job、执行job

JobGenerator介绍

视线在跳回到JobGenerator这边来,先看看JobGenerator中几个重要变量

// job生成消息的事件环
private var eventLoop: EventLoop[JobGeneratorEvent] 

// 定时器,按照批处理间隔定时向eventLoop发送生成job的消息
private val timer = new RecurringTimer(
    clock, ssc.graph.batchDuration.milliseconds,
    longTime => eventLoop.post(GenerateJobs(new Time(longTime))),
    "JobGenerator"
)

接下来看看JobGenerator的start方法

def start(): Unit = synchronized {
    if (eventLoop != null) return
    checkpointWriter
    // eventLoop的回调方法onReceive会调用processEvent(event)进行事件的处理
    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()
    }
}

start方法中会启动eventLoop和调用startFirstTime()。

eventLoop启动后,会启动一个线程来不断的接收消息,根据接收到的消息作出相应的操作

看一下startFirstTime(),startFirstTime中启动了DStreamGraph 和 用于定时发送生成job消息的定时器

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

DStreamGraph的start方法就不跟进了,没有很重要的东西。

timer启动后,会定时发送GenerateJobs(new Time(longTime))的消息。eventLoop在收到消息后,调用processEvent方法进行处理,如下:

private def processEvent(event: JobGeneratorEvent) {
    logDebug("Got event " + event)
    event match {
        case GenerateJobs(time) => generateJobs(time)
        case ClearMetadata(time) => clearMetadata(time)
        case DoCheckpoint(time, clearCheckpointDataLater) =>
        doCheckpoint(time, clearCheckpointDataLater)
        case ClearCheckpointData(time) => clearCheckpointData(time)
    }
}

生成job

接下来就开始generateJobs的旅程了。

首先processEvent会将GenerateJobs消息通过调用JobGenerator.generateJobs()进行处理。

以下是JobGenerator的generateJobs方法:

// 根据时间生成job
private def generateJobs(time: Time) {

    ssc.sparkContext.setLocalProperty(RDD.CHECKPOINT_ALL_MARKED_ANCESTORS, "true")
    Try {
        // 调用receiverTracker给批分配数据
        jobScheduler.receiverTracker.allocateBlocksToBatch(time)
        // 在DStreamGraph中根据分配的块生成job
        graph.generateJobs(time)
    } match {
        // 如果job生成成功,调用jobScheduler.submitJobSet提交job
        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)
        PythonDStream.stopStreamingContextIfPythonProcessIsDead(e)
    }
    // 完成后进行checkpoint
    eventLoop.post(DoCheckpoint(time, clearCheckpointDataLater = false))
}

首先会调用receiverTracker.allocateBlocksToBatch()给当前批分配需要处理的数据,之后调用DStreamGraph.generateJobs()生成job序列,如果生成成功,调用jobScheduler.submitJobSet提交job。

先跟进一下DStreamGraph.generateJobs():

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

发现这里会遍历outputStreams生成job,outputStreams中存放的是我们调用的outputOperation算子对应的DStream,也就是之前说的调用outputOperation算子将DStream注册到DStreamGraph中的outputStreams中。

以我们最开始的WordCount代码为例,我们的代码最终会添加一个ForEachDStream到outputStreams中去。

所以就会调用这里就调用ForEachDStream.generateJob()来生成job。

以下是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
    }
}

generateJob方法会调用parent.getOrCompute()生成RDD,如果生成成功,以RDD和我们定义的逻辑处理函数构造Job,并返回job。

需要注意一下这里的parent,parent其实就是它所依赖的上一个DStream的引用,

lines
    .flatMap(_.split(" "))
    .map((_, 1))
    .reduceByKey(_ + _)
    .print()

以我们写的代码为例,这里的parent就是由reduceByKey算子生成的ShuffledDStream的引用,ShuffledDStream中的parent是map生成的MappedDStream的引用,MappedDStream中的parent是flatMap生成的FlatMappedDStream的引用。

FlatMappedDStream中的parent就是SocketInputDStream的引用

跟进一下parent.getOrCompute(),现在的parent是ShuffledDStream的引用

private[streaming] final def getOrCompute(time: Time): Option[RDD[T]] = {
    // 已经生成的RDD集合,是以时间为key,rdd为value的HashMap
    generatedRDDs.get(time).orElse {
        if (isTimeValid(time)) {
            val rddOption = createRDDWithLocalProperties(time, displayInnerRDDOps = false) {
                // 执行compute方法,生成rdd,几乎每个DStream子类都会实现这个方法
                SparkHadoopWriterUtils.disableOutputSpecValidation.withValue(true) {
                    compute(time)
                }
            }
            // 对生成的rdd缓存或checkpoint,添加到已经生成的RDD集合中
            rddOption.foreach { case newRDD =>
                if (storageLevel != StorageLevel.NONE) {
                    newRDD.persist(storageLevel)
                }
                if (checkpointDuration != null && (time - zeroTime).isMultipleOf(checkpointDuration)) {
                    newRDD.checkpoint()
                }
                generatedRDDs.put(time, newRDD)
            }
            rddOption
        } else {
            None
        }
    }
}

DStream中定义了一个generatedRDDs用来存储已经生成的RDD。

会先去generatedRDDs中获取当前批的RDD,如果不存在则执行compute()生成RDD。

按我们写的代码来走的话,调用的是ShuffledDStream的compute方法。

override def compute(validTime: Time): Option[RDD[(K, C)]] = {
    parent.getOrCompute(validTime) match {
        case Some(rdd) => Some(rdd.combineByKey[C](
            createCombiner, mergeValue, mergeCombiner, partitioner, mapSideCombine))
        case None => None
    }
}

发现又调用了parent.getOrCompute生成RDD。

我们就可以发现它是根据依赖关系,循环的去调用getOrCompute和compute,直到最开始的DStream。

我们代码中最开始的是SocketInputDStream,会调用SocketInputDStream实例的compute方法,SocketInputDStream没有compute方法,这里调用的是他的父类ReceiverInputDStream的compute方法。

override def compute(validTime: Time): Option[RDD[T]] = {
    val blockRDD = {
        if (validTime < graph.startTime) {
            new BlockRDD[T](ssc.sc, Array.empty)
        } else {
            // 获取当前分配给当前批的块信息
            val receiverTracker = ssc.scheduler.receiverTracker
            val blockInfos = receiverTracker.getBlocksOfBatch(validTime).getOrElse(id, Seq.empty)
            val inputInfo = StreamInputInfo(id, blockInfos.flatMap(_.numRecords).sum)
            ssc.scheduler.inputInfoTracker.reportInfo(validTime, inputInfo)
            // 根据批时间和块信息创建RDD,并返回
            createBlockRDD(validTime, blockInfos)
        }
    }
    Some(blockRDD)
}

一系列操作生成RDD完成后,回到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
    }
}

根据生成的RDD和业务处理函数封装成job,返回job到DStream.generateJobs()

DStream.generateJobs()再将job返回到JobGenerator.generateJobs()中来

此刻,我们的job就生成完成了。

提交执行job

接下来JobGenerator.generateJobs()中会执行jobScheduler.submitJobSet(JobSet(time, jobs, streamIdToInputInfos)),将job进行提交。

  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是一个线程类,其中会执行job.run运行job。

以下是Job的run方法,其中的func()就是我们封装进来的业务处理函数。

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

将JobHandler扔到线程池中执行,我们的job就跑起来了。

输出数据

job跑起来后,会根据我们封装的func(),执行对应的输出。

end...

至此,Spark Streaming源码流程解析就over了。

多敲、多看、多搬砖、加油。




个人公众号:码农峰,定时推送行业资讯,持续发布原创技术文章,欢迎大家关注。

原文地址:https://www.cnblogs.com/upupfeng/p/12325201.html

时间: 2024-10-06 10:10:35

Spark Streaming运行流程及源码解析(二)的相关文章

grunt源码解析:整体运行机制&amp;grunt-cli源码解析

前端的童鞋对grunt应该不陌生,前面也陆陆续续的写了几篇grunt入门的文章.本篇文章会更进一步,对grunt的源码进行分析.文章大体内容内容如下: grunt整体设计概览 grunt-cli源码分析 grunt-cli模块概览 grunt-cli源码分析 写在后面 grunt整体设计概览 grunt主要由三部分组成.其中,grunt-cli是本文的讲解重点 grunt-cli:命令行工具,调用本地安装的grunt来运行任务,全局安装. grunt:本地grunt,一般安装在项目根目录下.主要

CBV-2-CBV流程-view源码解析-面向对象-继承

CBV-2-CBV流程-view源码解析-面向对象-继承 CBV,基于反射实现根据请求方式不同,执行不同的方法. 请求流程:view源码解析 1.urls.py :请求一定来执行视图下的as_view方法. 2.views.py 视图内没有as_view方法,则找父级的as_view方法. 3.源码:as_view返回自己下面的view方法 4.as_view执行了自己view方法,放回值是dispatch方法. 5.dispatch方法判断请求方式. 6.所以请求已经来,第一步先执行的都是di

Spring 源码解析之HandlerAdapter源码解析(二)

Spring 源码解析之HandlerAdapter源码解析(二) 前言 看这篇之前需要有Spring 源码解析之HandlerMapping源码解析(一)这篇的基础,这篇主要是把请求流程中的调用controller流程单独拿出来了 解决上篇文章遗留的问题 getHandler(processedRequest) 这个方法是如何查找到对应处理的HandlerExecutionChain和HandlerMapping的,比如说静态资源的处理和请求的处理肯定是不同的HandlerMapping ge

CBV-2-CBV流程-View源码解析

CBV是基于反射实现根据请求方式不同,执行不同的方法. 请求流程:view源码解析 1.urls.py :请求一定来执行视图下的as_view方法.也可以直接点击as_view()来找源码. 2.views.py 视图内没有as_view方法,则找父级的as_view方法. 3.源码:as_view返回自己下面的view方法,as_view执行了自己view方法,返回值是dispatch方法. 4,dispatch方法判断请求方式. 5,所以请求已经来,第一步先执行的都是dispatch方法.

erlang下lists模块sort(排序)方法源码解析(二)

上接erlang下lists模块sort(排序)方法源码解析(一),到目前为止,list列表已经被分割成N个列表,而且每个列表的元素是有序的(从大到小) 下面我们重点来看看mergel和rmergel模块,因为我们先前主要分析的split_1_*对应的是rmergel,我们先从rmergel查看,如下 ....................................................... split_1(X, Y, [], R, Rs) -> rmergel([[Y, X

chenglei1986/DatePicker源码解析(二)

接上一篇文章chenglei1986/DatePicker源码解析(一),我们继续将剩余的部分讲完,其实剩余的内容,就是利用Numberpicker来组成一个datePicker,代码非常的简单 为了实现自定义布局的效果,我们给Datepciker定制了一个layout,大家可以定制自己的layout <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="h

Spark Core 1.3.1源码解析及个人总结

本篇源码基于赵星对Spark 1.3.1解析进行整理.话说,我不认为我这下文源码的排版很好,不能适应的还是看总结吧. 虽然1.3.1有点老了,但对于standalone模式下的Master.Worker和划分stage的理解是很有帮助的.=====================================================总结: master和worker都要创建ActorSystem来创建自身的Actor对象,master内部维护了一个保存workerinfo的hashSe

Mybatis 源码解析(二) - Configuration.xml解析

文章个人学习源码所得,若存在不足或者错误之处,请大家指出. 上一章中叙述了Configuration.xml流化到Mybatis内存中的过程,那么接下来肯定就是Configuration.xml文件解析操作,在Mybatis中,这个解析的操作由SqlSesssionFactoryBuilder负责.接下来我们看看SqlSessionFactoryBuilder的方法签名: SqlSessionFactoryBuilder提供了9个签名方法,其中前8个方法都是Configuration.xml的解

AFNetworking2.0源码解析&lt;二&gt;

本篇我们继续来看看AFNetworking的下一个模块 — AFURLRequestSerialization. AFURLRequestSerialization用于帮助构建NSURLRequest,主要做了两个事情: 1.构建普通请求:格式化请求参数,生成HTTP Header. 2.构建multipart请求. 分别看看它在这两点具体做了什么,怎么做的. 1.构建普通请求 A.格式化请求参数 一般我们请求都会按key=value的方式带上各种参数,GET方法参数直接加在URL上,POST方