Spark Streaming (一)| Spark,从入门到精通

Spark Streaming 是 Spark 0.7 推出的流处理库,代表 Spark 正式进入流处理领域,距今已有快 6 年的时间。在这段时间中,随着 Spark 不断完善,Spark Streaming 在业界已得到广泛应用,应该算是目前最主要的流处理解决方案之一。

Spark Streaming 有三个特点:

  • 基于 Spark Core Api,因此其能够与 Spark 中的其他模块保持良好的兼容性,为编程提供了良好的可扩展性;
  • 粗粒度的准实时处理框架,一次读取完成,或异步读完数据之后,再处理数据,且其计算可基于大内存进行,因而具有较高的吞吐量;
  • 采用统一的 DAG 调度以及 RDD,对实时计算有很好的容错支持;

总览

Spark Streaming 是批处理的流式实时计算框架,支持从多种数据源获取数据,如 Kafka、TCP sockets、文件系统等。它可以使用诸如 map、reduce、join 等高级函数进行复杂算法的处理,最后还可以将处理结果存储到文件系统,数据库等。
在这里插入图片描述

在内部,它的工作方式如下,Spark Streaming接收实时输入数据流,并将数据分为批次,然后由Spark引擎进行处理,以生成批次的最终结果流。

Spark Streaming 的关键抽象 DStream,DStream 意指 Discretized Stream(离散化流),它大体上来说是一个 RDD 流(序列),其元素(RDD)可以理解为从输入流生成的批。在流处理的过程中,用户其实就是在对 DStream 进行各种变换,最后再输出。如下图所示,可以看出 Spark Streaming 的输入是连续的,经过 Spark Streaming 接收后,会变成一个 RDD 序列,之后的处理逻辑是基于 DStream 来操作的。

在这里插入图片描述

DStream 的生成依据是按照时间间隔切分,该时间间隔的数据会生成一个微批(mini-batch),即一个 RDD,所以该间隔也被称为批次间隔,DStream 里面持有对所有产生的 RDD 的引用,虽然 RDD 和 DStream 非常像,在种类上基本都是一一对应的,如 UnionDStream 与 UnionRDD,但是 DStream 还是和 RDD 有本质不同,如下图所示。

在这里插入图片描述

RDD 是 DStream 中某个批次的数据,而 DStream 代表了一段时间所产生的 RDD。所以通过这种方式,Spark Streaming 把对连续流的处理,变成了对批序列 DStream 的处理。我们会在 StreamingContext 设置批次间隔大小,一般大于 200ms。

关键抽象与架构

Spark Streaming 在架构上与 Spark 离线计算架构非常相似,主要分为 Driver 与 Executor,同样可以运行在 Yarn、Mesos 上,也能够以 standalone 和 local 模式运行。它们之间的关系仍旧是 Driver 负责调度,Executor 执行任务。如下图所示。

在这里插入图片描述

在 Driver 中,有几个关键模块,SparkStreamingContext 、DStreamGraph、JobScheduler、Checkpoint、ReceiverTracker,下面我将就这几个模块分别介绍。

  • SparkStreamingContext:SparkStreamingContext是一开始在用户代码中初始化完成的。它主要的工作是对作业进行一些配置,例如 DStream 切分的批次间隔(Duration),以及与其他模块进行交互,如 DStreamGraph 和 JobScheduler 等。

  • DStreamGraph:既然 Spark Streaming 最后还是对批的处理,那么批处理中根据计算逻辑生成的 RDD DAG 也是存在的,它由 DStreamGraph 生成。DStreamGraph 维护了输入 DStream 与输出 DStream 的实例,还会通过 generateJobs 方法生成一个作业集合(RDD DAG),它会由 JobScheduler 调度启动执行任务。

  • JobScheduler:JobScheduler 顾名思义是 Spark Streaming 的作业调度器,在创建 SparkStreamingContext 的同时,JobScheduler 也会作为它的一部分被创建,所有的任务都是最后由它调度 Executor 来执行。

  • Checkpoint:在 Spark 中,任何一个 RDD 丢失,都可以通过依赖关系重新计算得到, Checkpoint 是 Spark Streaming 容错机制的核心,会定时对已算好的中间结果以及其他中间状态进行存储,避免了依赖链过长的问题。这样就算某个 DStream 丢失了,也不用从头开始计算,只需从最近的依赖关系开始计算即可。

  • ReceiverTracker:ReceiverTracker 通过 Executor 上的 ReceiverSupvisor 来管理所有的 Receiver。主要功能是把需要计算的数据发送给 Executor。当 Executor 接收完毕后,也会将数据块的元数据上报给 ReceiverTracker。

从上面这几个组件的关系上来说,SparkStreamingContext 负责与其他组件交互,DStreamGraph 与 JobScheduler 负责调度,Checkpoint 负责容错,ReceiverTracker 负责与 Executor 进行数据交互。

Executor 是具体的任务执行者,其中重要的组件有 Receiver、ReceiverSupvisor、ReceiveredBlockHandler,ReceiverTracker 会和 Executor 通信,启动 ReceiverSupvisor 实例,ReceiverSupvisor 会马上启动 Receiver 开始接收数据。Receiver 接收到数据后,用 ReceiverdBlockHandler 以块的方式写到 Executor 的磁盘或者内存,对应的实现是 BlockManagerBasedBlockHandler 和 WriteAheadLogBasedBlockHandler,前者是根据 Executor 的 StorageLevel 写到相应的存储层,后者会先进行预写日志(Write Ahead Log),其中,后者能对流式数据源提供更好的容错性。数据接收完毕后,会根据调度开始计算任务。

Spark Streaming 的作业初始化与提交和 Spark SQL 作业有些不同,我们还是通过初始化 SparkSession 的方式得到 StreamingContext 的引用,再对其设置一个关键参数:批次间隔后,就可以进行数据接收和数据处理的动作。

运行原理详解

在这里插入图片描述

如上图 所示是 Spark 的整体架构图,它主要分为四个模块:

  • 静态的 RDD DAG 模版,表示处理逻辑;
  • 动态的工作控制器,将连续的 streaming data 切分为数据片段,并按照模板复制出新的 RDD DAG 的实例,对数据片段进行处理;
  • 原始数据的产生和导入
  • 对长时运行任务的保障,包括输入数据的失效后的重构和处理任务的失败后的重调。

DAG 静态定义

DAG 静态定义是将整个计算逻辑描述为一个 RDD DAG 的「模版」,在后面 Job 动态生成的时候,针对每个 batch,Spark Streaming 都将根据这个「模板」生成一个 RDD DAG 的实例。

在这里插入图片描述

接下来我们了解下 RDD 和 DStream 的关系。DStream 维护了对每个产出的 RDD 实例的引用,如图 2 所示,DStream 在 3 个 batch 里分别实例化了 3 个 RDD, a[1]、a[2]、a[3],然后 DStream 就保留了 batch 所产出的 RDD 的哈希表。

我们在考虑的时候,可以认为 RDD 加上 batch 维度就是 DStream,DStream 去掉 batch 维度就是 RDD。Spark 定义静态的计算逻辑后,通过动态的工作控制来调度。

Job 动态生成

在 Spark Streaming 程序的入口我们都会定义一个 batchDuration,即每隔固定时间就比照静态的 DStreamGraph 来动态生成一个 RDD DAG 实例。在 Spark Streaming 内整体负责动态作业调度的具体类是 JobScheduler,由 start() 运行。

JobScheduler 有两个非常重要的成员:JobGenerator 和 ReceiverTracker。JobScheduler 将每个 batch 的 RDD DAG 具体生成工作委托给 JobGenerator,而将源头输入数据的记录工作委托给 ReceiverTracker。

在这里插入图片描述

JobGenerator 维护了一个定时器,周期就是上文提到的 batchDuration,定时为每个 batch 生成 RDD DAG 的实例,其中每次 RDD DAG 实际生成包含 5 个步骤:

  • 要求 ReceiverTracker 将目前已收到的数据进行一次分配,即将上个批次切分后的数据,切分到到本次新的批次里;
  • 要求 DStreamGraph 复制出一套新的 RDD DAG 的实例, DStreamGraph 将要求图里的尾 DStream 节点生成具体的 RDD 实例,并递归的调用尾 DStream 的上游 DStream 节点……以此遍历整个 DStreamGraph,遍历结束也就正好生成了 RDD DAG 的实例;
  • 获取第 1 步 ReceiverTracker 分配到本 batch 的源头数据的 meta 信息;
  • 将第 2 步生成的本 batch 的 RDD DAG,和第 3 步获取到的 meta 信息,一同提交给 JobScheduler 异步执行;
  • 只要提交结束(不管是否已开始异步执行),就马上对整个系统的当前运行状态做一个 checkpoint。

数据产生与导入

DStream 的子类 ReceiverInputDStream 在某个 batch 里实例化 RDD,通过 Receiver 为这个 RDD 生产数据。Spark Streaming 在程序刚开始运行时:

在这里插入图片描述

  • 由 Receiver 的总指挥 ReceiverTracker 分发多个 job,到多个 executor 上分别启动 ReceiverSupervisor 实例;
  • 每个 ReceiverSupervisor 启动后将马上生成一个用户提供的 Receiver 实现的实例并在 Receiver 实例生成后调用 Receiver.onStart(),这时 Receiver 启动工作已经运行完毕。

在这里插入图片描述

  • Receiver 在 onStart() 启动后,就将持续不断地接收外界数据,并持续交给 ReceiverSupervisor 进行数据转储;
  • ReceiverSupervisor 持续不断地接收到 Receiver 转来的数据,如果数据很细小,就需要 BlockGenerator 攒多条数据成一块(4a)、然后再成块存储(4b 或 4c);反之就不用攒,直接成块存储(4b 或 4c);
  • 每次成块在 executor 存储完毕后,ReceiverSupervisor 就会及时上报块数据的 meta 信息给 driver 端的 ReceiverTracker,这里的 meta 信息包括数据的标识 id、数据的位置、数据的条数、数据的大小等信息;
  • ReceiverTracker 再将收到的块数据 meta 信息直接转给自己的成员 ReceivedBlockTracker,由 ReceivedBlockTracker 专门管理收到的块数据 meta 信息。

后续在 driver 端,就由 ReceiverInputDStream 在每个 batch 去检查 ReceiverTracker 收到的块数据 meta 信息,界定哪些新数据需要在本 batch 内处理,然后生成相应的 RDD 实例去处理这些块数据。

简单的例子

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

object example{

 def main(args:Array[String]):Unit = {
// Create a local StreamingContext with two working thread and batch interval of 1   second. The master requires 2 cores to prevent a starvation scenario.
     
   val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
   val ssc = new StreamingContext(conf, Seconds(1))

   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

   ssc.start()
   ssc.awaitTermination()
 }
}

如以上代码所示:

  • 启动 Spark Streamingg 实例后将 batchDuration 设置为 1 秒;
  • ssc.socketTextStream() 将创建一个 SocketInputDStream,这个 InputDStream 的 SocketReceiver 将监听本机 9999 端口;
  • 接下来几行利用 DStream transformation 构造出了 lines -> words -> pairs -> wordCounts -> .print() 从lines 到 wordCounts print()的一个 DStreamGraph;
  • 到目前只是是定义好了产生数据的 SocketReceiver 及 DStreamGraph,这些都是静态的;
  • 下面这行 start() 将在幕后启动 JobScheduler,进而启动 JobGenerator 和 ReceiverTracker,其中 JobGenerator 开始不断的生成一个一个 batch,ReceiverTracker 创建和启动 Receiver;
  • 然后用户 code 主线程就 block 在awaitTermination了,block 的效果就是,后台的 JobScheduler 开始不断的生成一个一个 batch,也就是在这里,我们前面静态定义的 DStreamGraph 的 print(),才一次一次被在 RDD 实例上调用,一次一次打印出当前 batch 的结果;

小结

本文介绍了 Spark Streaming 的关键抽象与架构,Spark Streaming 沿用了 RDD 原有的抽象,将流处理变成了连续的微批处理。如果把原有的数据处理看成是一维的,那么流处理无疑是二维的:它加入了一个很重要的时间维度,并且处理的需求往往与时间维度紧密相关,这就使得虽然 Spark Streaming 仍然使用了 RDD 的抽象,但转换算子分为了有状态和无状态之分,下篇文章将讲解Spark Streaming 中具体的转换算子。

参考文章

https://kaiwu.lagou.com/course/courseInfo.htm?courseId=71#/detail/pc?id=1990

猜你喜欢

转载自blog.csdn.net/lomodays207/article/details/107666682