Spark原理篇之Spark Streaming实现思路与模块概述

1 Spark Streaming概述

      和Spark基于RDD的概念相似,Spark Streaming使用离散化流作为抽象表示,叫作DStream。DStream是随着时间推移而收到的数据的序列。在内部,每个时间区间收到的数据都作为RDD的存在,而DStream是由这些RDD所组成的序列(因此得名“离散化”)。DStream可以从各种输入源创建,比如Flume、Kafka或者HDFS。创建出来的DStream支持两种操作,一种是转化操作(transformation),会生成一个新的DStream,另一种是输出操作(output operation),可以把数据写入外部系统中。DStream提供了许多与RDD所支持的操作相类似的操作支持,还增加了与时间相关的新操作,比如滑动窗口。和批处理程序不同,Spark Streaming应用需要进行额外配置来保证24/7不间断工作。

2 基于Spark做Spark Streaming的思路

(1)第一步,假设我们有一小块数据,那么通过RDD API,我们能够构造出一个进行数据处理的RDD DAG(如下图所示)。
在这里插入图片描述
(2)第二步,我们连续的Streaming Data进行切片处理,比如将最近200ms时间的event积攒一下,每个切片就是一个batch,然后使用第一步中的RDD DAG对这个batch的数据进行处理。所以,针对连续不断的 Streaming Data进行多次切片,就会形成多个batch,也就对应出来多个RDD DAG(每个RDD DAG针对一个batch的数据)。如此一来,这多个RDD DAG之间相互同构,却又是不同的实例。我们用下图来表示这个关系: 在这里插入图片描述
      所以,我们将需要:
      ① 一个静态的RDD DAG的模板,来表示处理逻辑;
      ② 一个动态的工作控制器,将连续的Streaming Data切分数据片段,并按照模板复制出新的RDD DAG的实例,对数据片段进行处理; 在这里插入图片描述
(3)我们回过头来看Streaming Data本身的产生。Hadoop MapReduce,Spark RDD API进行批处理时,一般默认数据已经在HDFS、HBase或其它存储上。而Streaming Data,比如Twitter流,又有可能是在系统外实时产生的,就需要能够将这些数据导入到Spark Streaming系统里,就像Apache Storm的Spout和Apache S4的Adapter能够把数据导入系统里的作用是一致的。所以,我们将需要:
      ③ 原始数据的产生和导入;
(4)我们考虑,有了以上①②③三部分,就可以顺利利用RDD API处理Streaming Data了吗?其实相对于batch job通常几个小时能够跑完来讲,Streaming job的运行时间是+∞(正无穷大)的,所以我们还将需要:
      ④ 对长时运行任务的保障,包括输入数据的失效后的重构,处理任务的失败后的重调。
      至此,Streaming Data的特点决定了,如果我们想基于Spark Core进行Streaming Data的处理,还需要在Spark Core的框架上解决刚才列出的①②③④这四点问题: 在这里插入图片描述

3 Spark Streaming的整体模块划分

      根据Spark Streaming解决这4个问题的不同focus,可以经Spark Streaming划分为四个大的模块:
      ① 模块1:DAG静态定义;
      ② Job动态生成;
      ③ 数据产生与导入;
      ④ 长时容错。
      其中每个模块设计到的主要的类,示意如下: 在这里插入图片描述

3.1 模块1:DAG静态定义

      通过前面的描述我们知道,应该首先对计算逻辑描述为一个RDD DAG的模板,在后面Job动态生成的时候,针对每个batch,Spark Streaming都将根据这个模板生成一个RDD DAG的实例。
(1)DStream和DStreamGraph
      其实在Spark Streaming里,这个RDD模板对应的具体的类是DStream,RDD DAG模板对应的具体类是DStreamGraph。而RDD本身也有很多子类,几乎每个子类都有一个对应的DStream,如UnionRDD的对应是UnionDStream。RDD通过transformation连接成RDD DAG(但RDD DAG在Spark Core里没有对应的具体类),DStream也通过transformation连接成DStreamGraph。
DStream 的全限定名是:org.apache.spark.streaming.dstream.DStream
DStreamGraph 的全限定名是:org.apache.spark.streaming.DStreamGraph
(2)DStream和RDD的关系
      既然DStream是RDD的模板,而且DStream和RDD具有相同的transformation操作,比如map()、filter()、reduce()…等等(正是这些相同的transformation使得DStreamGraph能够忠实记录RDD DAG的计算逻辑),那RDD和DSrtream有什么不一样吗?还真不一样。
      比如,DStream维护了对每个产出的RDD实例的引用。比如下图里,DStream A在3个batch里分别实例化了3个RDD,分别是a[1],a[2],a[3],那么DStream A就保留了一个batch所产出的RDD的哈希表,即包含batch 1->a[1],batch 2->a[2],batch 3->a[3]这3项。 在这里插入图片描述
      另外,能够进行流量控制的DStream子类,如ReceiverInputDStream,还会保存关于历次batch的源头数据条数、历次batch计算花费的时间等数值用来实时计算准确的流量控制信息,这些都是记在DStream里的,而RDD a[1]等则不会保存这些信息。
      我们在考虑的时候,可以认为,RDD加上batch维度就是DStream,DStream去掉batch维度就是RDD,就像是RDD=DStream at batch T。
      不过这里需要特别说明的是,在DStreamGraph的图里,DStream(即数据)是顶点,DStream之间的transformation(即计算)是边。这与Apache Storm等是相反的。
      在Apache Storm的topology里,计算是顶点,Stream(连续的tuple,即数据)是边。
在这里插入图片描述

3.2 模块2:Job动态生成

      现在有了DStreamGraph和DStream,也就是静态定义了的计算逻辑,下面我们来看Spark Streaming是如何调度的。
      在Spark Streaming程序的入口,我们都会定义一个batchDuration,就是需要每隔多长时间就比照静态的DStreamGraph来动态生成一个RDD DAG实例。在Spark Streaming里,总体负责动态作业调度的具体类是JobScheduler,在Spark Streaming程序开始运行的时候,会生成一个JobScheduler的实例,并被start()运行起来。
      JobScheduler有两个非常重要的成员:JobScheduler和ReceiverTracker。JobScheduler将每个batch的RDD DAG的具体生成工作委托给JobScheduler,而将源头数据的记录工作委托给ReceiverTracker。

JobScheduler    的全限定名是:org.apache.spark.streaming.scheduler.JobScheduler
JobGenerator    的全限定名是:org.apache.spark.streaming.scheduler.JobGenerator
ReceiverTracker 的全限定名是:org.apache.spark.streaming.scheduler.ReceiverTracker

      JobScheduler维护了一个定时器,周期就是我们刚刚提到的batchDuration,定时为每个batch生成RDD DAG的实例。具体的,每个RDD DAG实际生成包含5个步骤:
      ① 要求ReceiverTracker将目前已收到的数据进行一次allocate,即将上次batch切分后的数据切分到本次新的batch里;
      ② DStreamGraph复制出一套新的RDD DAG的实例,具体过程是:DStreamGraph将要求图里的尾DStream节点生成具体的RDD实例,并递归地调用尾DStream的上游DStream节点…以此遍历整个DStreamGraph,遍历结束也就正好生成了RDD DAG的实例;
      ③ 获取第1步ReceiverTracker分配到本batch的源头数据的meta信息;
      ④ 将第2步生成的本batch的RDD DAG,和第3步获取到的meta信息,一同提交给JobScheduler异步执行;
      ⑤ 只要提交结束(不管是否已开始异步执行),就马上对整个系统的当前运行状态做一个checkpoint。
      上述5个步骤的调用关系图如下: 在这里插入图片描述

3.3 模块3:数据产生与导入

      下面我们看Spark Streaming解决第三个问题的模块分析,即数据的产生与导入。
      DStream有一个重要而特殊的子类ReceiverInputDStream:它除了需要像其它DStream那样在某个batch里实例化RDD以外,还需要额外的Receiver为这个RDD生产数据!具体的,Spark Streaming在程序刚开始运行时:
(1)由Receiver的总指挥ReceiverTracker分发多个Job(每个Job1个Task),到多个Executor上分别启动ReceiverSupervisor实例;
(2)每个ReceiverSupervisor启动后马上生成一个用户提供的Receiver实现的实例,该Receiver实现可以持续产生或者持续接收系统外数据,比如TwitterReceiver可以实现爬取Twitter数据,并在Receiver实例生成后调用Receiver.onStart();
在这里插入图片描述

ReceiverSupervisor 的全限定名是:org.apache.spark.streaming.receiver.ReceiverSupervisor
Receiver           的全限定名是:org.apache.spark.streaming.receiver.Receiver	

      (1)(2)的过程由上图所示,这时Receiver启动工作已运行完毕。接下来ReceiverSupervisor将在Executor端作为主要角色,并且:
(3)Receiver在onStart()启动后,就将持续不断地接收外界数据,并持续交给ReceiverSupervisor进行数据转储;
(4)ReceiverSupervisor持续不断地收到Receiver转来的数据:
      ① 如果数据很细小,就需要BlockGenerator攒多条数据成一块(4a)、然后再成块存储(4b或4c)
      ② 反之就不用攒,直接成块存储(4b或4c)
      ③ 这里Spark Streaming目前支持两种块存储方式,一种是由BlockManagerBlockHandler直接到Executor的内存或硬盘,另一种由WriteAheadLogBasedBlockHandler同时写WAL(4c)和Executor的内存或硬盘。
(5)每次成块在Executor存储完毕后,ReceiverSupervisor就会及时上报块数据的meta信息给Driver端的ReceiverTracker;这里的meta信息包括数据的表示ID,数据的位置,数据的条数,数据的大小灯信息;
(6)ReceiverTracker再将收到的块数据meta信息直接转给自己的成员ReceivedBlockTracker,由ReceivedBlockTracker专门管理收到的块数据meta信息。
在这里插入图片描述

BlockGenerator                 的全限定名是:org.apache.spark.streaming.receiver.BlockGenerator
BlockManagerBasedBlockHandler  的全限定名是:org.apache.spark.streaming.receiver.BlockManagerBasedBlockHandler
WriteAheadLogBasedBlockHandler 的全限定名是:org.apache.spark.streaming.receiver.WriteAheadLogBasedBlockHandler
ReceivedBlockTracker           的全限定名是:org.apache.spark.streaming.scheduler.ReceivedBlockTracker
ReceiverInputDStream           的全限定名是:org.apache.spark.streaming.dstream.ReceiverInputDStream

      这里的(3)(4)(5)(6)的过程一直持续不断地发生的,我们也将其在上图里标识出来。
      后续在Driver端,就由ReceiverInputDStream在每个batch去检查ReceiverTracker收到的块数据meta信息,界定哪些新数据需要在本batch内处理,然后生成相应的RDD实例去处理这些块数据,这个过程在模块1:DAG静态定义和模块2:Job动态生成里描述过了。

3.4 模块4:长时容错

      以上我们简述完成Spark Streaming基于Spark Core所新增功能的3个模块,接下来我们看一看第4个模块将如何保障Spark Streaming的长时运行,也就是,如何与前3个模块长时运行。
      通过前3个模块的关键类的分析,我们可以知道,保障模块1和2需要在Driver端完成,保障模块3需要在Executor端和Driver端完成。
(1)Executor端长时容错
      在Executor端,ReceiverSupervisor和Receiver失效后直接重启就OK了,关键是保障收到的块数据的安全。保障了源头块数据,就能够保障RDD DAG(Spark Core的lineage)重做。Spark Streaming对源头块数据的保障,分为4个层次,全面相互补充,又可根据不同场景灵活设置:
      ① 热备:热备是指在存储块数据时,将其存储到本Executor、并同时replicate到另一个Executor上去。这样在一个replica失效后,可以立刻无感知切换到另一份replica进行计算。实现方式是,在实现自己的Receiver时,即指定一下StorageLevel为MEMORY_ONLY_2或MEMORY_AND_DISK_2就可以了。
      ② 冷备:冷备是每次存储块数据前,先把块数据作为log写出到WriteAheadLog里,再存储到本Executor。Executor失效时,就由另外的Executor去读WAL,再重做log来恢复块数据。WAL通常写到可靠存储如HDFS上,所以恢复时可能需要一段recover time。 在这里插入图片描述
      ③ 重放:如果上游支持重放,比如Apache Kafka,那么就可以选择不用热备或者冷备来另外存储数据了,而是在失效时换一个Executor进行数据重放即可。
      ④ 忽略:最后,如果应用的实时性需求大于准确性,那么一块数据丢失后我们也可以选择忽略、不恢复失效的源头数据。
      我们用一个表格来总结一下: 在这里插入图片描述
(2)Driver端长时容错
      前面我们讲过,块数据的meta信息上报到ReceiverTracker,然后交给ReceivedBlockTracker做具体的管理。
      ReceivedBlockTracker也采用WAL冷备进行备份,在Driver失效后,由新的ReceivedBlockTracker读取WAL并恢复block的meta信息。
      另外,需要定时对DStreamGraph和JobScheduler做Checkpoint,来记录整个DStreamGraph的变化,和每个batch的job的完成情况。
      注意到这里采用的是完整Checkpoint的方式,和之前的WAL的方式不一样。Checkpoint通常也是落地到可靠存储如HDFS。Checkpoint发起的间隔默认的是和batchDuration一致;即每次batch发起、提交了需要运行的job后就做Checkpoint,另外在job完成了更新任务状态的时候再次做一下Checkpoint。
      这样一来,在Driver失效并恢复后,可以读取最近一次的Checkpoint来恢复作业的DStreamGraph和job的运行及完成状态。
(3)总结
在这里插入图片描述
      总结一下“模块4:长时容错”的内容为上述表格,可以看到,Spark Streaming 的长时容错特性,能够提供不重、不丢,exactly-once 的处理语义。

4 Quick Example

import org.apache.spark._
import org.apache.spark.streaming._

// 首先配置一下本 quick example 将跑在本机,app name 是 NetworkWordCount
val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
// batchDuration 设置为 1 秒,然后创建一个 streaming 入口
val ssc = new StreamingContext(conf, Seconds(1))

// ssc.socketTextStream() 将创建一个 SocketInputDStream;这个 InputDStream 的 SocketReceiver 将监听本机 9999 端口
val lines = ssc.socketTextStream("localhost", 9999)

val words = lines.flatMap(_.split(" "))      // DStream transformation
val pairs = words.map(word => (word, 1))     // DStream transformation
val wordCounts = pairs.reduceByKey(_ + _)    // DStream transformation
wordCounts.print()                           // DStream output
// 上面 4 行利用 DStream transformation 构造出了 lines -> words -> pairs -> wordCounts -> .print() 这样一个 DStreamGraph
// 但注意,到目前是定义好了产生数据的 SocketReceiver,以及一个 DStreamGraph,这些都是静态的

// 下面这行 start() 将在幕后启动 JobScheduler, 进而启动 JobGenerator 和 ReceiverTracker
// ssc.start()
//    -> JobScheduler.start()
//        -> JobGenerator.start();    开始不断生成一个一个 batch
//        -> ReceiverTracker.start(); 开始往 executor 上分布 ReceiverSupervisor 了,也会进一步创建和启动 Receiver
ssc.start()

// 然后用户 code 主线程就 block 在下面这行代码了
// block 的后果就是,后台的 JobScheduler 线程周而复始的产生一个一个 batch 而不停息
// 也就是在这里,我们前面静态定义的 DStreamGraph 的 print(),才一次一次被在 RDD 实例上调用,一次一次打印出当前 batch 的结果
ssc.awaitTermination()

      所以我们看到,StreamingContext 是 Spark Streaming 提供给用户 code 的、与前述 4 个模块交互的一个简单和统一的入口。

参考文章:
[1] https://github.com/lw-lin/CoolplaySpark/blob/master/Spark Streaming 源码解析系列/0.1 Spark Streaming 实现思路与模块概述.md

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值