写点什么

Spark Streaming 源码分析:DStream 的内置和运行

  • 2019-12-11
  • 本文字数:3873 字

    阅读完需:约 13 分钟

Spark Streaming源码分析:DStream的内置和运行

贝壳实时计算平台已承接公司各种实时数据任务 100 多个,日处理消费总量接近 250 亿,采用的底层引擎包括 Spark-Streaming 和 Flink。为了提升数据处理速度和稳定性,我们对各种引擎的处理特性进行了深入研究。在 Spark-Streaming 中,对流的抽象是使用 DStream 来定义的,所以想要理解 Spark-Streaming 的流处理模型,理解 DStream 的内部实现以及其如何构建和运行是很有必要的。

DStream 的构建

我们在定义一个流的处理逻辑时,首先从一个数据的流入源开始,这个数据源使用 InputDStream 定义,它是 DStream 的一个子类,之后我们会在其上调用一些 tranform 类型算子,像 map,reduce,filter 等等,每调用一个算子,都会创建一个新的 DStream,每一个新创建的 DStream 都保留着当前节点所依赖的上一个节点和当前节点的执行逻辑这两个部分,这样,多个 DStream 节点就构成了一个逻辑执行链。


比如如下代码会生成图 1 的执行链


stream.map(...).filter(...).foreachRDD(...)
复制代码



虚线箭头描述的是依赖关系


最后当调用 action 类型的算子时,所有 action 类型的算子,底层都是通过 ForEachDStream 实现的。


我们来看 ForEachDStream 的源码


private[streaming] class ForEachDStream[T: ClassTag] (    parent: DStream[T],    foreachFunc: (RDD[T], Time) => Unit,    displayInnerRDDOps: Boolean  ) extends DStream[](parent.ssc) {
override def dependencies: List[DStream[_]] = List(parent)
override def slideDuration: Duration = parent.slideDuration
override def compute(validTime: Time): Option[RDD[Unit]] = None
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 方法,这里调用了它依赖的父 DStream 的 getOrCompute 来生成一个需要它来处理的 RDD,然后对该 RDD 做该节点本身需要做的一些操作,即 foreachFunc 闭包,其实所有 DStream 的 getOrCompute 方法底层都会调用 compute 方法,该方法会返回 RDD,即所有的 DStream 的 compute 方法中,要么其本身能够从外部拉取数据,即 InputDStream 作为 DStream 链的第一个节点,要么其本身调用依赖的上游 DStream 的 compute 方法,再对生成的 RDD 做其本节点所定义的一些操作作为其返回值。


如此,当 DStream 链的最后一个节点被调用了 compute 方法时,它能够依次递归的调用逐节点的 compute 方法,最后调用第一个 InputDStream 节点的 compute 方法生成一个能够拉取外部数据的 RDD。


其调用的时序图如下



以上只是为了直观的理解 DStream 链是如何工作的,具体体现在分布式环境上时,是由 RDD 来定义操作,切分成 task 后由 Executor 来执行。


另外需要说的是如果我们在单个流上定义一系列除 window 外的操作,其和我们直接调用 InputDStream 的 foreachRDD 后,在 rdd 上定义操作是等效的。

DStream 的运行

除了上面介绍的 DStream 之外,在 Spark-Streaming 内部还有一些保存作业处理逻辑的模块和用于根据时间生成和管理每个批次数据的模块。下面是在 SparkStreaming 中一些比较核心的类,他们是构成一个流式作业,和使其运行起来的框架。


  1. InputDStream 管理流的数据源的通用抽象类

  2. JobGenerator 作业生成器

  3. JobScheduler 作业调度器,用于提交作业到集群运行

  4. DStreamGraph 管理创建的所有 InputDStream 的初始化和启动,但不负责 InputDStream 间依赖关系的管理,InputDStream 间依赖关系由其子类实现管理


首先看一下这四个类交互的时序图



图中只画了一些比较重要和核心的类和逻辑。JobGenerator 每隔我们设定的时间间隔会生成一个 JobGeneratorEvent 事件用于触发生成一个作业。其内部是通过 RecurringTimer 类和 EventLoop 实现的。


代码如下:初始化 timer 和 eventLoop。


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


这里 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)    }  }
复制代码


startFirstTime 中调用了 timer 和 DStreamGraph 的 start 方法,将二者内部的事件循环线程启动起来。


到这里我们知道当 timer 根据时间生成 GenerateJobs 事件时,会触发 generateJobs 函数的调用。


我们来看 generateJobs 的代码


  private def generateJobs(time: Time) {    // Checkpoint all RDDs marked for checkpointing to ensure their lineages are    // truncated periodically. Otherwise, we may run into stack overflows (SPARK-6847).    ssc.sparkContext.setLocalProperty(RDD.CHECKPOINT_ALL_MARKED_ANCESTORS, "true")    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)        PythonDStream.stopStreamingContextIfPythonProcessIsDead(e)    }    eventLoop.post(DoCheckpoint(time, clearCheckpointDataLater = false))  }
复制代码


其内部又调用了 DStreamGraph 的 generateJobs 方法用来生成多个 Job,之后通过 JobScheduler 对这些 Job 进行提交。


DStreamGraph 底层生成作业的过程是 DStreamGraph 实现的,它会遍历所有注册过的 ForEachDStream,并分别调用他们的 generateJob 方法,返回一个 Job 对象,这就跟我们上面讲过的 ForEachDStream 部分关联上了。


Job 里面包含了一个需要在其上执行计算的 RDD,包括所有计算逻辑的闭包,而这个闭包真正执行,是在 JobScheduler 将这个 Job 对象提交到一个线程池之后,其会在线程池内执行这个 Job 对象内的闭包逻辑,将其转换成分布式计算的 task 分发到不同的节点上去执行。


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 就是那个线程池,其队列长度可通过 spark.streaming.concurrentJobs 进行配置,默认等于 1。该参数决定了能够并发执行的 Job 数量。


JobHandler 是 Job 的封装,会在执行 Job 的逻辑前后分别发布 JobStarted 和 JobCompleted 事件。而 Job 对象真正执行的逻辑就是在 ForEachDStream 类中的创建 Job 时 foreachFunc 闭包。

Spark-Streaming 在贝壳的使用

目前贝壳的实时计算平台上运行着 143 个实时任务,其中 121 个为 Spark 任务,62 个为 Spark-Streaming 任务,59 个为 Structured Streaming 任务。为了实现组内任务的快速开发,我们在 Spark 实时技术栈的基础上抽象出了 Chronus 框架,将流计算抽象出 Source,Sink 和 Pipeline,其中 Source 和 Sink 可通过配置直接定义,开发时能够将精力集中在 Pipeline 中的业务逻辑上。


框架对监控,offset 管理都做了封装。同时,我们基于 Chronus 框架也开发除了很多任务模板,能够让用户通过一些简单的可视化配置直接进行流式任务的开发,其中最典型的要属 SQL 模板:



上述的那 59 个 Structured Streaming 就是基于该 SQL 模板开发的实时任务。


作者介绍


顾渊离,目前负责贝壳大数据部实时计算平台底层引擎和实时场景模板相关开发工作。


本文转载自公众号贝壳产品技术。


原文链接


https://mp.weixin.qq.com/s/rkzIo8sweI2UOOnmOj6SQQ


2019-12-11 14:052290

评论

发布
暂无评论
发现更多内容

最佳代码扫描工具,实现自动化代码扫描服务

阿里云云效

云计算 阿里云 开发 代码扫描 代码安全

王者荣耀商城异地多活架构设计

王大胖

《LeetCode刷题报告》题解内容Ⅰ

謓泽

3月月更

哈尔滨市消防救援支队:用宜搭打造消防智慧大脑,守护冰城平安

一只大光圈

低代码 数字化 消防 钉钉宜搭

DDoS攻击与防范策略

喀拉峻

网络安全 信息安全 DDoS

java培训JVM面试题分享

@零度

JVM JAVA开发

大数据培训Hive 提高查询效率的方法

@零度

hive 大数据开发

RocketMQ 开源爱好者请注意邀您共探行业应用与生产实践

阿里巴巴云原生

阿里代码依赖漏洞检测服务,高效杜绝代码安全隐患

阿里云云效

云计算 阿里云 云原生 代码安全 依赖漏洞检测

钉钉协同引擎与应用场景技术探索

阿里巴巴终端技术

分布式 跨平台 客户端 开发框架

go 1.18 bufio 包中的 Writer.AvailableBuffer

蓬蒿

go 1.18 Writer.AvailableBuffer

Apache ShardingSphere 首篇论文被 ICDE 收录,全球数据库发展迎来新局面

SphereEx

数据库 ShardingSphere SphereEx apache 社区

成本管理系统解决方案

低代码小观

企业管理 资产管理 成本优化 低成本 CRM系统

洞见科技参编的中国信通院《联邦学习场景应用研究报告》正式发布

洞见科技

联邦学习 隐私计算 中国信通院 洞见科技

实时渲染大赛结果将于3月31日晚8点B站直播公布,敬请期待!

3DCAT实时渲染

实时渲染 3D动画

适合 Kubernetes 初学者的一些实战练习(二)

汪子熙

Kubernetes 云原生 Kubernetes 集群 Serverless Kubernetes 3月月更

TDengine 在蔚来能源系统的落地实践

TDengine

数据库 tdengine

车联网平台百万级消息吞吐架构设计|车联网系列专题 05

EMQ映云科技

车联网 物联网 IoT emq 3月月更

博文推荐|使用 Apache Pulsar 和 Scala 进行事件流处理

Apache Pulsar

开源 分布式 云原生 Apache Pulsar Apache Pulsar 社区

艾特网能全面参与数据中心蒸发冷却空调系列标准编制

科技热闻

多终端运行小程序,快速丰富智慧社区使用场景

Speedoooo

智慧社区 小程序管理平台 智慧家居 智能家居生态平台 生态平台

教你识别一些sequence的相关问题

华为云开发者联盟

序列 GaussDB(DWS) sequence GTM bind关系

web前端培训使用 Vue3来实现文章目录功能

@零度

Vue 前端开发

重视软件开发的黑匣子

菜根老谭

bug 系统安全 程序日志

大咖说|网易数帆论道 PolarDB 数据库开源 & 存储生态

大咖说

数据库 阿里云 开源 网易

ModStartCMS模块化建站系统 v3.6.0 内容标签增强,电脑手机适配

ModStart开源

95 后新生代 Committer 贺张俭:谈谈年轻人的开源观

Apache Pulsar

开源 云原生 物联网 Apache Pulsar Apache Pulsar 社区

【等保】二级等保是什么意思?哪些企业要做?多少钱?

行云管家

网络安全 等保 二级等保

多云管理需求分析与好用的工具介绍

行云管家

云计算 混合云 多云 云管理

企业和团队如何创建高效实用的知识管理体系

小炮

知识管理 企业

直播回顾|TGIP-CN 036:Apache Pulsar 最新技术进展与动态

Apache Pulsar

开源 云原生 中间件 Apache Pulsar Apache Pulsar 社区

Spark Streaming源码分析:DStream的内置和运行_大数据_顾渊离_InfoQ精选文章