Structured Streaming
1. 回顾和展望
1.1. Spark 编程模型的进化过程
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
, 灵活选择
1.2. Spark 的 序列化 的进化过程
什么是序列化和序列化
在 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
对象, 只能将其序列化后传输二进制数据
在 Spark 中的序列化和反序列化的应用场景
Task分发
Task
是一个对象, 想在网络中传输对象就必须要先序列化
RDD 缓存
val rdd1 = rdd.flatMap(_.split(" "))
.map((_, 1))
.reduceByKey(_ + _)
rdd1.cache
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
不能被序列化, 则会报错
RDD 的序列化
RDD 的序列化只能使用 Java 序列化器, 或者 Kryo 序列化器
- RDD 中存放的是数据对象, 要保留所有的数据就必须要对对象的元信息进行保存, 例如对象头之类的
- 保存一整个对象, 内存占用和效率会比较低一些
Kryo 是什么
Kryo
是Spark
引入的一个外部的序列化工具, 可以增快RDD
的运行速度- 因为
Kryo
序列化后的对象更小, 序列化和反序列化的速度非常快 - 在
RDD
中使用Kryo
的过程如下
val conf = new SparkConf()
.setMaster("local[6]")
.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)))
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]
元信息独立
RDD
不保存数据的元信息, 所以只能使用 Java Serializer
或者 Kyro Serializer
保存 整个对象
DataFrame
和 Dataset
中保存了数据的元信息, 所以可以把元信息独立出来分开保存
一个 DataFrame
或者一个 Dataset
中, 元信息只需要保存一份, 序列化的时候, 元信息不需要参与
在反序列化 ( InternalRow → Object
) 时加入 Schema
信息即可
元信息不再参与序列化, 意味着数据存储量的减少, 和效率的增加
使用堆外内存
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
倍左右
1.3. Spark Streaming 和 Structured Streaming
Spark Streaming 时代
Spark Streaming
其实就是 RDD
的 API
的流式工具, 其本质还是 RDD
, 存储和执行过程依然类似 RDD
Structured Streaming 时代
Structured Streaming
其实就是 Dataset
的 API
的流式工具, API
和 Dataset
保持高度一致
Structured Streaming
相比于 Spark Streaming
的进步就类似于 Dataset
相比于 RDD
的进步
另外还有一点, Structured Streaming
已经支持了连续流模型, 也就是类似于 Flink
那样的实时流, 而不是小批量, 但在使用的时候仍然有限制, 大部分情况还是应该采用小批量模式
在 2.2.0
以后 Structured Streaming
被标注为稳定版本, 意味着以后的 Spark
流式开发不应该在采用 Spark Streaming
了
2. Structured Streaming 入门案例
2.1. 需求梳理
需求
编写一个流式计算的应用, 不断的接收外部系统的消息
对消息中的单词进行词频统计
统计全局的结果
整体结构
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
消息统计词频
2.2. 代码实现
object SocketProcessor {
def main(args: Array[String]): Unit = {
// 1. 创建 SparkSession
val spark = SparkSession.builder()
.master("local[6]")
.appName("socket_processor")
.getOrCreate()
spark.sparkContext.setLogLevel("WARN")
import spark.implicits._
// 2. 读取外部数据源, 并转为 Dataset[String]
val source = spark.readStream
.format("socket")
.option("host