Structured Streaming 快速入门系列(一)一文带你了解Structured Streaming

全天目标

  • 回顾和展望
  • 入门案例
  • Stuctured Streaming 的体系和结构

回顾和展望

本章目标

  • Structured Streaming 是 Spark Streaming 的进化版, 如果了解了 Spark 的各方面的进化过程, 有助于理解 Structured Streaming 的使命和作用

本章过程

  • Spark 的 API 进化过程
  • Spark 的序列化进化过程
  • Spark Streaming 和 Structured Streaming

Spark 编程模型的进化过程

目标和过程

目标

  • Spark 的进化过程中, 一个非常重要的组成部分就是编程模型的进化, 通过编程模型可以看得出来内在的问题和解决方案

过程

  • 编程模型 RDD 的优点和缺陷
  • 编程模型 DataFrame 的优点和缺陷
  • 编程模型 Dataset 的优点和缺陷

在这里插入图片描述

RDD

rdd.flatMap(_.split(" "))
   .map((_, 1))
   .reduceByKey(_ + _)
   .collect
  • 针对自定义数据对象进行处理, 可以处理任意类型的对象, 比较符合面向对象
  • RDD 无法感知到数据的结构, 无法针对数据结构进行编程

DataFrame

spark.read
     .csv("...")
     .where($"name" =!= "")
     .groupBy($"name")
     .show()
  • DataFrame 保留有数据的元信息, API 针对数据的结构进行处理, 例如说可以根据数据的某一列进行排序或者分组
  • DataFrame 在执行的时候会经过 Catalyst 进行优化, 并且序列化更加高效, 性能会更好
  • DataFrame 只能处理结构化的数据, 无法处理非结构化的数据, 因为 DataFrame 的内部使用 Row 对象保存数据
  • Spark 为 DataFrame 设计了新的数据读写框架, 更加强大, 支持的数据源众多

Dataset

spark.read
     .csv("...")
     .as[Person]
     .where(_.name != "")
     .groupByKey(_.name)
     .count()
     .show()
  • Dataset 结合了 RDD 和 DataFrame 的特点, 从 API 上即可以处理结构化数据, 也可以处理非结构化数据
  • Dataset 和 DataFrame 其实是一个东西, 所以 DataFrame 的性能优势, 在 Dataset 上也有

总结

RDD 的优点

  • 面向对象的操作方式
  • 可以处理任何类型的数据

RDD 的缺点

  • 运行速度比较慢, 执行过程没有优化
  • API 比较僵硬, 对结构化数据的访问和操作没有优化

DataFrame 的优点

  • 针对结构化数据高度优化, 可以通过列名访问和转换数据
  • 增加 Catalyst 优化器, 执行过程是优化的, 避免了因为开发者的原因影响效率

DataFrame 的缺点

  • 只能操作结构化数据
  • 只有无类型的 API, 也就是只能针对列和 SQL 操作数据, API 依然僵硬

Dataset 的优点

  • 结合了 RDD 和 DataFrame 的 API, 既可以操作结构化数据, 也可以操作非结构化数据
  • 既有有类型的 API 也有无类型的 API, 灵活选择

Spark 的 序列化 的进化过程

目标

  • Spark 中的序列化过程决定了数据如何存储, 是性能优化一个非常重要的着眼点, Spark 的进化并不只是针对编程模型提供的 API, 在大数据处理中, 也必须要考虑性能

过程

  • 序列化和反序列化是什么
  • Spark 中什么地方用到序列化和反序列化
  • RDD 的序列化和反序列化如何实现
  • Dataset 的序列化和反序列化如何实现

Step 1: 什么是序列化和序列化

在 Java 中, 序列化的代码大概如下

public class JavaSerializable implements Serializable {
  NonSerializable ns = new NonSerializable();
}

public class NonSerializable {

}

public static void main(String[] args) throws IOException {
  // 序列化
  JavaSerializable serializable = new JavaSerializable();
  ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("/tmp/obj.ser"));
  // 这里会抛出一个 "java.io.NotSerializableException: cn.itcast.NonSerializable" 异常
  objectOutputStream.writeObject(serializable);
  objectOutputStream.flush();
  objectOutputStream.close();

  // 反序列化
  FileInputStream fileInputStream = new FileInputStream("/tmp/obj.ser");
  ObjectInputStream objectOutputStream = new ObjectInputStream(fileInputStream);
  JavaSerializable serializable1 = objectOutputStream.readObject();
}

序列化是什么

  • 序列化的作用就是可以将对象的内容变成二进制, 存入文件中保存
  • 反序列化指的是将保存下来的二进制对象数据恢复成对象

序列化对对象的要求

  • 对象必须实现 Serializable 接口
  • 对象中的所有属性必须都要可以被序列化, 如果出现无法被序列化的属性, 则序列化失败

限制

  • 对象被序列化后, 生成的二进制文件中, 包含了很多环境信息, 如对象头, 对象中的属性字段等, 所以内容相对较大
  • 因为数据量大, 所以序列化和反序列化的过程比较慢

序列化的应用场景

  • 持久化对象数据
  • 网络中不能传输 Java 对象, 只能将其序列化后传输二进制数据

Step 2: 在 Spark 中的序列化和反序列化的应用场景

Task 分发

在这里插入图片描述Task 是一个对象, 想在网络中传输对象就必须要先序列化

RDD 缓存

val rdd1 = rdd.flatMap(_.split(" "))
   .map((_, 1))
   .reduceByKey(_ + _)

rdd1.cache

rdd1.collect
  • RDD 中处理的是对象, 例如说字符串, Person 对象等
  • 如果缓存 RDD 中的数据, 就需要缓存这些对象
  • 对象是不能存在文件中的, 必须要将对象序列化后, 将二进制数据存入文件

广播变量

在这里插入图片描述

  • 广播变量会分发到不同的机器上, 这个过程中需要使用网络, 对象在网络中传输就必须先被序列化

Shuffle 过程

在这里插入图片描述

  • Shuffle 过程是由 Reducer 从 Mapper 中拉取数据, 这里面涉及到两个需要序列化对象的原因
    • RDD 中的数据对象需要在 Mapper 端落盘缓存, 等待拉取
    • Mapper 和 Reducer 要传输数据对象

Spark Streaming 的 Receiver

在这里插入图片描述

  • Spark Streaming 中获取数据的组件叫做 Receiver, 获取到的数据也是对象形式, 在获取到以后需要落盘暂存, 就需要对数据对象进行序列化

算子引用外部对象

class Unserializable(i: Int)

rdd.map(i => new Unserializable(i))
   .collect
   .foreach(println)
  • 在 Map 算子的函数中, 传入了一个 Unserializable 的对象
  • Map 算子的函数是会在整个集群中运行的, 那 Unserializable 对象就需要跟随 Map 算子的函数被传输到不同的节点上
  • 如果 Unserializable 不能被序列化, 则会报错

Step 3: RDD 的序列化

在这里插入图片描述
RDD 的序列化只能使用 Java 序列化器, 或者 Kryo 序列化器

为什么?

  • RDD 中存放的是数据对象, 要保留所有的数据就必须要对对象的元信息进行保存, 例如对象头之类的
  • 保存一整个对象, 内存占用和效率会比较低一些

Kryo 是什么

  • Kryo 是 Spark 引入的一个外部的序列化工具, 可以增快 RDD 的运行速度
  • 因为 Kryo 序列化后的对象更小, 序列化和反序列化的速度非常快
  • 在 RDD 中使用 Kryo 的过程如下
val conf = new SparkConf()
  .setMaster("local[2]")
  .setAppName("KyroTest")

conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
conf.registerKryoClasses(Array(classOf[Person]))

val sc = new SparkContext(conf)

rdd.map(arr => Person(arr(0), arr(1), arr(2)))

Step 4: DataFrame 和 Dataset 中的序列化

历史的问题

  • RDD 中无法感知数据的组成, 无法感知数据结构, 只能以对象的形式处理数据

DataFrame 和 Dataset 的特点

  • DataFrame 和 Dataset 是为结构化数据优化的
  • 在 DataFrame 和 Dataset 中, 数据和数据的 Schema 是分开存储的
spark.read
     .csv("...")
     .where($"name" =!= "")
     .groupBy($"name")
     .map(row: Row => row)
     .show()
  • DataFrame 中没有数据对象这个概念, 所有的数据都以行的形式存在于 Row 对象中, Row 中记录了每行数据的结构, 包括列名, 类型等

在这里插入图片描述

  • Dataset 中上层可以提供有类型的 API, 用以操作数据, 但是在内部, 无论是什么类型的数据对象 Dataset 都使用一个叫做 InternalRow 的类型的对象存储数据
val dataset: Dataset[Person] = spark.read.csv(...).as[Person]

优化点 1: 元信息独立

  1. RDD 不保存数据的元信息, 所以只能使用 Java Serializer 或者 Kyro Serializer 保存 整个对象
  2. DataFrame 和 Dataset 中保存了数据的元信息, 所以可以把元信息独立出来分开保存

在这里插入图片描述
3. 一个 DataFrame 或者一个 Dataset 中, 元信息只需要保存一份, 序列化的时候, 元信息不需要参与

在这里插入图片描述

  1. 在反序列化 ( InternalRow → Object ) 时加入 Schema 信息即可

在这里插入图片描述
元信息不再参与序列化, 意味着数据存储量的减少, 和效率的增加

优化点 2: 使用堆外内存

  • DataFrame 和 Dataset 不再序列化元信息, 所以内存使用大大减少. 同时新的序列化方式还将数据存入堆外内存中, 从而避免 GC 的开销.
  • 堆外内存又叫做 Unsafe, 之所以叫不安全的, 因为不能使用 Java 的垃圾回收机制, 需要自己负责对象的创建和回收, 性能很好, 但是不建议普通开发者使用, 毕竟不安全

总结

  • 当需要将对象缓存下来的时候, 或者在网络中传输的时候, 要把对象转成二进制, 在使用的时候再将二进制转为对象, 这个过程叫做序列化和反序列化
  • 在 Spark 中有很多场景需要存储对象, 或者在网络中传输对象
    • Task 分发的时候, 需要将任务序列化, 分发到不同的 Executor 中执行
    • 缓存 RDD 的时候, 需要保存 RDD 中的数据
    • 广播变量的时候, 需要将变量序列化, 在集群中广播
    • RDD 的 Shuffle 过程中 Map 和 Reducer 之间需要交换数据
    • 算子中如果引入了外部的变量, 这个外部的变量也需要被序列化
  • RDD 因为不保留数据的元信息, 所以必须要序列化整个对象, 常见的方式是 Java 的序列化器, 和 Kyro 序列化器
  • Dataset 和 DataFrame 中保留数据的元信息, 所以可以不再使用 Java 的序列化器和 Kyro 序列化器, 使用 Spark 特有的序列化协议, 生成 UnsafeInternalRow 用以保存数据, 这样不仅能减少数据量, 也能减少序列化和反序列化的开销, 其速度大概能达到 RDD 的序列化的 20 倍左右

Spark Streaming 和 Structured Streaming

目标

理解 Spark Streaming 和 Structured Streaming 之间的区别, 是非常必要的, 从这点上可以理解 Structured Streaming 的过去和产生契机

过程

  • Spark Streaming 时代
  • Structured Streaming 时代
  • Spark Streaming 和 Structured Streaming

Spark Streaming 时代

在这里插入图片描述

  • Spark Streaming 其实就是 RDD 的 API 的流式工具, 其本质还是 RDD, 存储和执行过程依然类似 RDD

Structured Streaming 时代

在这里插入图片描述

  • Structured Streaming 其实就是 Dataset 的 API 的流式工具, API 和 Dataset 保持高度一致

Spark Streaming 和 Structured Streaming

  • Structured Streaming 相比于 Spark Streaming 的进步就类似于 Dataset 相比于 RDD 的进步
  • 另外还有一点, Structured Streaming 已经支持了连续流模型, 也就是类似于 Flink 那样的实时流, 而不是小批量, 但在使用的时候仍然有限制, 大部分情况还是应该采用小批量模式
  • 在 2.2.0 以后 Structured Streaming 被标注为稳定版本, 意味着以后的 Spark 流式开发不应该在采用 Spark Streaming 了

Structured Streaming 入门案例

目标

了解 Structured Streaming 的编程模型, 为理解 Structured Streaming 时候是什么, 以及核心体系原理打下基础

步骤

  • 需求梳理
  • Structured Streaming 代码实现
  • 运行
  • 验证结果

需求梳理

目标

  • 理解接下来要做的案例, 有的放矢

步骤

  • 需求
  • 整体结构
  • 开发方式

需求

在这里插入图片描述

  • 编写一个流式计算的应用, 不断的接收外部系统的消息
  • 对消息中的单词进行词频统计
  • 统计全局的结果

整体结构

在这里插入图片描述

  • Socket Server 等待 Structured Streaming 程序连接
  • Structured Streaming 程序启动, 连接 Socket Server, 等待 Socket Server 发送数据
  • Socket Server 发送数据, Structured Streaming 程序接收数据
  • Structured Streaming 程序接收到数据后处理数据
  • 数据处理后, 生成对应的结果集, 在控制台打印

开发方式和步骤

Socket server 使用 Netcat nc 来实现

Structured Streaming 程序使用 IDEA 实现, 在 IDEA 中本地运行

  • 编写代码
  • 启动 nc 发送 Socket 消息
  • 运行代码接收 Socket 消息统计词频

总结
简单来说, 就是要进行流式的词频统计, 使用 Structured Streaming

代码实现

目标

  • 实现 Structured Streaming 部分的代码编写

步骤

  • 创建文件
  • 创建 SparkSession
  • 读取 Socket 数据生成 DataFrame
  • 将 DataFrame 转为 Dataset, 使用有类型的 API 处理词频统计
  • 生成结果集, 并写入控制台
object SocketProcessor {

  def main(args: Array[String]): Unit = {

    // 1. 创建 SparkSession
    val spark = SparkSession.builder()
      .master("local[6]")
      .appName("socket_processor")
      .getOrCreate()

    spark.sparkContext.setLogLevel("ERROR")   //1

    import spark.implicits._

    // 2. 读取外部数据源, 并转为 Dataset[String]
    val source = spark.readStream
      .format("socket")
      .option("host", "127.0.0.1")
      .option("port", 9999)
      .load()
      .as[String]        //2                     

    // 3. 统计词频
    val words = source.flatMap(_.split(" "))
      .map((_, 1))
      .groupByKey(_._1)
      .count()

    // 4. 输出结果
    words.writeStream
      .outputMode(OutputMode.Complete())      //3
      .format("console")                      //4
      .start()                                //5
      .awaitTermination()                     //6
  }
}
  1. 调整 Log 级别, 避免过多的 Log 影响视线
  2. 默认 readStream 会返回 DataFrame, 但是词频统计更适合使用
  3. Dataset 的有类型 API
  4. 统计全局结果, 而不是一个批次
  5. 将结果输出到控制台
  6. 开始运行流式应用
  7. 阻塞主线程, 在子线程中不断获取数据

总结

  • Structured Streaming 中的编程步骤依然是先读, 后处理, 最后落地
  • Structured Streaming 中的编程模型依然是 DataFrame 和 Dataset
  • Structured Streaming 中依然是有外部数据源读写框架的, 叫做 readStream 和 writeStream
  • Structured Streaming 和 SparkSQL 几乎没有区别, 唯一的区别是, readStream 读出来的是流, writeStream 是将流输出, 而 SparkSQL 中的批处理使用 read 和 write

运行和结果验证

目标

  • 代码已经编写完毕, 需要运行, 并查看结果集, 因为从结果集的样式中可以看到 Structured Streaming 的一些原理

步骤

  • 开启 Socket server
  • 运行程序
  • 查看数据集

开启 Socket server 和运行程序

  • 在虚拟机 node01 中运行 nc -lk 9999
  • 在 IDEA 中运行程序
  • 在 node01 中输入以下内容
hello world
hello spark
hello hadoop
hello spark
hello spark

查看结果集

-------------------------------------------
Batch: 4
-------------------------------------------
+------+--------+
| value|count(1)|
+------+--------+
| hello|       5|
| spark|       3|
| world|       1|
|hadoop|       1|
+------+--------+

从结果集中可以观察到以下内容

  • Structured Streaming 依然是小批量的流处理
  • Structured Streaming 的输出是类似 DataFrame 的, 也具有 Schema, 所以也是针对结构化数据进行优化的
  • 从输出的时间特点上来看, 是一个批次先开始, 然后收集数据, 再进行展示, 这一点和 Spark Streaming 不太一样

总结

  • 运行的时候需要先开启 Socket server
  • Structured Streaming 的 API 和运行也是针对结构化数据进行优化过的

Stuctured Streaming 的体系和结构

目标

  • 了解 Structured Streaming 的体系结构和核心原理, 有两点好处, 一是需要了解原理才好进行性能调优, 二是了解原理后, 才能理解代码执行流程, 从而更好的记忆, 也做到知其然更知其所以然

步骤

  • WordCount 的执行原理
  • Structured Streaming 的体系结构

无限扩展的表格

目标

  • Structured Streaming 是一个复杂的体系, 由很多组件组成, 这些组件之间也会进行交互, 如果无法站在整体视角去观察这些组件之间的关系, 也无法理解 Structured Streaming 的全局

步骤

  • 了解 Dataset 这个计算模型和流式计算的关系
  • 如何使用 Dataset 处理流式数据?
  • WordCount 案例的执行过程和原理

Dataset 和流式计算

可以理解为 Spark 中的 Dataset 有两种, 一种是处理静态批量数据的 Dataset, 一种是处理动态实时流的 Dataset, 这两种 Dataset 之间的区别如下

  • 流式的 Dataset 使用 readStream 读取外部数据源创建, 使用 writeStream 写入外部存储
  • 批式的 Dataset 使用 read 读取外部数据源创建, 使用 write 写入外部存储

如何使用 Dataset 这个编程模型表示流式计算?

在这里插入图片描述

  • 可以把流式的数据想象成一个不断增长, 无限无界的表
  • 无论是否有界, 全都使用 Dataset 这一套 API
  • 通过这样的做法, 就能完全保证流和批的处理使用完全相同的代码, 减少这两种处理方式的差异

WordCount 的原理

在这里插入图片描述
整个计算过程大致上分为如下三个部分

  • Source, 读取数据源
  • Query, 在流式数据上的查询
  • Result, 结果集生成

整个的过程如下

  • 随着时间段的流动, 对外部数据进行批次的划分
  • 在逻辑上, 将缓存所有的数据, 生成一张无限扩展的表, 在这张表上进行查询
  • 根据要生成的结果类型, 来选择是否生成基于整个数据集的结果

总结

在这里插入图片描述

  • Dataset 不仅可以表达流式数据的处理, 也可以表达批量数据的处理
  • Dataset 之所以可以表达流式数据的处理, 因为 Dataset 可以模拟一张无限扩展的表, 外部的数据会不断的流入到其中

体系结构

目标

Structured Streaming 是一个复杂的体系, 由很多组件组成, 这些组件之间也会进行交互, 如果无法站在整体视角去观察这些组件之间的关系, 也无法理解 Structured Streaming 的核心原理

步骤

  • 体系结构
  • StreamExecution 的执行顺序

体系结构

在 Structured Streaming 中负责整体流程和执行的驱动引擎叫做 StreamExecution

在这里插入图片描述
StreamExecution 在流上进行基于 Dataset 的查询, 也就是说, Dataset 之所以能够在流上进行查询, 是因为 StreamExecution 的调度和管理

StreamExecution 如何工作?

在这里插入图片描述

StreamExecution 分为三个重要的部分

  • Source, 从外部数据源读取数据
  • LogicalPlan, 逻辑计划, 在流上的查询计划
  • Sink, 对接外部系统, 写入结果

StreamExecution 的执行顺序

在这里插入图片描述
1,根据进度标记, 从 Source 获取到一个由 DataFrame 表示的批次, 这个 DataFrame 表示数据的源头

val source = spark.readStream
  .format("socket")
  .option("host", "127.0.0.1")
  .option("port", 9999)
  .load()
  .as[String]

这一点非常类似 val df = spark.read.csv() 所生成的 DataFrame, 同样都是表示源头

2,根据源头 DataFrame 生成逻辑计划

val words = source.flatMap(_.split(" "))
  .map((_, 1))
  .groupByKey(_._1)
  .count()

上述代码表示的就是数据的查询, 这一个步骤将这样的查询步骤生成为逻辑执行计划

3,优化逻辑计划最终生成物理计划

在这里插入图片描述
这一步其实就是使用 Catalyst 对执行计划进行优化, 经历基于规则的优化和基于成本模型的优化

4,执行物理计划将表示执行结果的 DataFrame / Dataset 交给 Sink,整个物理执行计划会针对每一个批次的数据进行处理, 处理后每一个批次都会生成一个表示结果的 Dataset,Sink 可以将每一个批次的结果 Dataset 落地到外部数据源

5,执行完毕后, 汇报 Source 这个批次已经处理结束, Source 提交并记录最新的进度

增量查询

  • 核心问题

在这里插入图片描述
上图中清晰的展示了最终的结果生成是全局的结果, 而不是一个批次的结果, 但是从 StreamExecution 中可以看到, 针对流的处理是按照一个批次一个批次来处理的

那么, 最终是如何生成全局的结果集呢?

  • 状态记录

在这里插入图片描述

在 Structured Streaming 中有一个全局范围的高可用 StateStore, 这个时候针对增量的查询变为如下步骤

  • 从 StateStore 中取出上次执行完成后的状态
  • 把上次执行的结果加入本批次, 再进行计算, 得出全局结果
  • 将当前批次的结果放入 StateStore 中, 留待下次使用

在这里插入图片描述
总结

  • StreamExecution 是整个 Structured Streaming 的核心, 负责在流上的查询
  • StreamExecution 中三个重要的组成部分, 分别是 Source 负责读取每个批量的数据, Sink 负责将结果写入外部数据源, Logical Plan 负责针对每个小批量生成执行计划
  • StreamExecution 中使用 StateStore 来进行状态的维护
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值