全天目标
- 回顾和展望
- 入门案例
- 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: 元信息独立
- RDD 不保存数据的元信息, 所以只能使用 Java Serializer 或者 Kyro Serializer 保存 整个对象
- DataFrame 和 Dataset 中保存了数据的元信息, 所以可以把元信息独立出来分开保存
3. 一个 DataFrame 或者一个 Dataset 中, 元信息只需要保存一份, 序列化的时候, 元信息不需要参与
- 在反序列化 ( 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
}
}
- 调整 Log 级别, 避免过多的 Log 影响视线
- 默认 readStream 会返回 DataFrame, 但是词频统计更适合使用
- Dataset 的有类型 API
- 统计全局结果, 而不是一个批次
- 将结果输出到控制台
- 开始运行流式应用
- 阻塞主线程, 在子线程中不断获取数据
总结
- 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 来进行状态的维护