文章目录
Structured Streaming
1、 回顾
Structured Streaming
是 Spark Streaming
的进化版, 如果了解了 Spark
的各方面的进化过程, 有助于理解 Structured Streaming
的使命和作用
Spark
的API
进化过程Spark
的序列化进化过程Spark Streaming
和Structured Streaming
1.1、Spark 编程模型的进化过程
目标
Spark 的进化过程中, 一个非常重要的组成部分就是编程模型的进化, 通过编程模型可以看得出来内在的问题和解决方案
过程
- 编程模型 RDD 的优点和缺陷
- 编程模型 DataFrame 的优点和缺陷
- 编程模型 Dataset 的优点和缺陷
分析 | 编程模型 |
---|---|
RDD | rdd.flatMap(_.split(" ")) .map((_, 1)) .reduceByKey(_ + _) .collect 1. 针对自定义数据对象进行处理, 可以处理任意类型的对象, 比较符合面向对象 2. RDD 无法感知到数据的结构, 无法针对数据结构进行编程 |
DataFrame | spark.read .csv(“…”) .where( " a g e " = ! = " " ) < b r / > . g r o u p B y ( "age" =!= "")<br /> .groupBy( "age"=!="")<br/>.groupBy(“age”) .show() 1. DataFrame 保留有数据的元信息, API 针对数据的结构进行处理, 例如说可以根据数据的某一列进行排序或者分组 2. DataFrame 在执行的时候会经过 Catalyst 进行优化, 并且序列化更加高效, 性能会更好3. DataFrame 只能处理结构化的数据, 无法处理非结构化的数据, 因为 DataFrame 的内部使用 Row 对象保存数据 4. Spark 为 DataFrame 设计了新的数据读写框架, 更加强大, 支持的数据源众多 |
DataSet | spark.read .csv(“…”) .as[Person] .where(_.age!= “”) .groupByKey(_.age) .count() .show() 1. Dataset 结合了 RDD 和 DataFrame 的特点, 从 API 上即可以处理结构化数据, 也可以处理非结构化数据 2. Dataset 和 DataFrame 其实是一个东西, 所以 DataFrame 的性能优势, 在 Dataset 上也有 |
总结
RDD
的优点
1. 面向对象的操作方式
2. 可以处理任何类型的数据RDD
的缺点
1. 运行速度比较慢, 执行过程没有优化
2.API
比较僵硬, 对结构化数据的访问和操作没有优化DataFrame
的优点- 针对结构化数据高度优化, 可以通过列名访问和转换数据
- 增加
Catalyst
优化器, 执行过程是优化的, 避免了因为开发者的原因影响效率
DataFrame
的缺点
1. 只能操作结构化数据
2. 只有无类型的API
, 也就是只能针对列和SQL
操作数据,API
依然僵硬Dataset
的优点
1. 结合了RDD
和DataFrame
的API
, 既可以操作结构化数据, 也可以操作非结构化数据
2. 既有有类型的API
也有无类型的API
, 灵活选择
1.2、Spark 的 序列化 的进化过程
`Spark` 中的序列化过程决定了数据如何存储, 是性能优化一个非常重要的着眼点, `Spark` 的进化并不只是针对编程模型提供的 `API`, 在大数据处理中, 也必须要考虑性能
-
问题
- 序列化和反序列化是什么 ?
Spark
中什么地方用到序列化和反序列化 ?RDD
的序列化和反序列化如何实现 ?Dataset
的序列化和反序列化如何实现 ?
1.2.1、 什么是序列化和序列化?
- 序列化是什么
1. 序列化的作用就是可以将对象的内容变成二进制, 存入文件中保存
2. 反序列化指的是将保存下来的二进制对象数据恢复成对象 - 序列化对对象的要求
1. 对象必须实现Serializable
接口
2. 对象中的所有属性必须都要可以被序列化, 如果出现无法被序列化的属性, 则序列化失败 - 限制
1. 对象被序列化后, 生成的二进制文件中, 包含了很多环境信息, 如对象头, 对象中的属性字段等, 所以内容相对较大
2. 因为数据量大, 所以序列化和反序列化的过程比较慢 - 序列化的应用场景
1. 持久化对象数据
2. 网络中不能传输Java
对象, 只能将其序列化后传输二进制数据
1.2.2、 在 Spark
中的序列化和反序列化的应用场景
-
Task
分发[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pcp8Q8SM-1607519404816)(assets/1597389899574.png)]
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 userserializable(i: Int)
rdd.map(i => new Unserializable(i))
.collect
.foreach(println)
- 在
Map
算子的函数中, 传入了一个Unserializable
的对象 Map
算子的函数是会在整个集群中运行的, 那Unserializable
对象就需要跟随Map
算子的函数被传输到不同的节点上- 如果
Unserializable
不能被序列化, 则会报错
1.2.3、 RDD 的序列化
-
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)))
1.2.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]
总结
- 当需要将对象缓存下来的时候, 或者在网络中传输的时候, 要把对象转成二进制, 在使用的时候再将二进制转为对象, 这个过程叫做序列化和反序列化
- 在
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
和 Structured 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 已经支持了连续流模型(trigger), 也就是类似于 Flink 那样的实时流, 而不是小批量, 但在使用的时候仍然有限制, 大部分情况还是应该采用小批量模式
在 2.2.0 以后 Structured Streaming 被标注为稳定版本, 意味着以后的 Spark 流式开发不应该在采用 Spark Streaming 了
2、 Structured Streaming 入门案例
了解 Structured Streaming
的编程模型, 为理解 Structured Streaming
时候是什么, 以及核心体系原理打下基础
步骤
- 需求梳理
Structured Streaming
代码实现- 运行
- 验证结果
2.1、需求梳理
1、编写一个流式计算的应用, 不断的接收外部系统的消息
2、对消息中的单词进行词频统计
3、统计全局的结果
整体结构
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()
// 调整 Log 级别, 避免过多的 Log 影响视线
spark.sparkContext.setLogLevel("ERROR")
import spark.implicits._
// 2. 读取外部数据源, 并转为 Dataset[String]
val source = spark.readStream
.format("socket")
.option("host", "datanode01")
.option("port", 10086)
.load()
.as[String]//默认 readStream 会返回 DataFrame, 但是词频统计更适合使用 Dataset 的有类型 API
// 3. 统计词频
val words = source.flatMap(_.split(" "))
.map((_, 1))
.groupByKey(_._1)
.count()
// 4. 输出结果
words.writeStream
.outputMode(OutputMode.Complete()) //统计全局结果, 而不是一个批次
.format("console") // 将结果输出到控制台
.start() // 开始运行流式应用
.awaitTermination() // 阻塞主线程, 在子线程中不断获取数据
}
}
错误提示:
ERROR StreamMetadata: Error writing stream metadata StreamMetadata
需要配置本机的Hadoop环境变量,内部缺少dll文件。
总结
1、Structured Streaming 中的编程步骤依然是先读, 后处理, 最后落地
2、Structured Streaming 中的编程模型依然是 DataFrame 和 Dataset
3、Structured Streaming 中依然是有外部数据源读写框架的, 叫做 readStream 和 writeStream
4、Structured Streaming 和 SparkSQL 几乎没有区别, 唯一的区别是, readStream 读出来的是流, writeStream 是将流输出, 而 SparkSQL 中的批处理使用 read 和 write
2.3 结果输出
1、在虚拟机 datanode01
中运行 nc -lk 10086
2、IDEA结果输出如下
-------------------------------------------
Batch: 4
-------------------------------------------
+------+--------+
| value|count(1)|
+------+--------+
| hello| 5|
| spark| 3|
| world| 1|
|hadoop| 1|
+------+--------+
3、Stuctured Streaming 的体系和结构
了解 Structured Streaming
的体系结构和核心原理, 有两点好处, 一是需要了解原理才好进行性能调优, 二是了解原理后, 才能理解代码执行流程, 从而更好的记忆, 也做到知其然更知其所以然
步骤
WordCount
的执行原理Structured Streaming
的体系结构
3.1. 无限扩展的表格
问题
- 了解
Dataset
这个计算模型和流式计算的关系 - 如何使用
Dataset
处理流式数据? WordCount
案例的执行过程和原理
Dataset
和流式计算
可以理解为 Spark
中的 Dataset
有两种, 一种是处理静态批量数据的 Dataset
, 一种是处理动态实时流的 Dataset
, 这两种 Dataset
之间的区别如下
- 流式的
Dataset
使用readStream
读取外部数据源创建, 使用writeStream
写入外部存储 - 批式的
Dataset
使用read
读取外部数据源创建, 使用write
写入外部存储
如何使用 Dataset
这个编程模型表示流式计算?
1、可以把流式的数据想象成一个不断增长, 无限无界的表
2、无论是否有界, 全都使用 Dataset 这一套 API
3、通过这样的做法, 就能完全保证流和批的处理使用完全相同的代码, 减少这两种处理方式的差异
WordCount
的原理
整个计算过程大致上分为如下三个部分
1、Source, 读取数据源
2、Query, 在流式数据上的查询
3、Result, 结果集生成
整个的过程如下
1、随着时间段的流动, 对外部数据进行批次的划分
2、在逻辑上, 将缓存所有的数据, 生成一张无限扩展的表, 在这张表上进行查询
3、根据要生成的结果类型, 来选择是否生成基于整个数据集的结果
总结
Dataset
不仅可以表达流式数据的处理, 也可以表达批量数据的处理Dataset
之所以可以表达流式数据的处理, 因为Dataset
可以模拟一张无限扩展的表, 外部的数据会不断的流入到其中
3.2、体系结构
在 Structured Streaming
中负责整体流程和执行的驱动引擎叫做 StreamExecution
StreamExecution
如何工作?
StreamExecution
分为三个重要的部分
Source
, 从外部数据源读取数据LogicalPlan
, 逻辑计划, 在流上的查询计划Sink
, 对接外部系统, 写入结果
总结
StreamExecution
是整个Structured Streaming
的核心, 负责在流上的查询StreamExecution
中三个重要的组成部分, 分别是Source
负责读取每个批量的数据,Sink
负责将结果写入外部数据源,Logical Plan
负责针对每个小批量生成执行计划StreamExecution
中使用StateStore
来进行状态的维护
4、Streaming -> Source
流式计算一般就是通过数据源读取数据, 经过一系列处理再落地到某个地方, 所以这一小节先了解一下如何读取数据, 可以整合哪些数据源
4.1. 从 HDFS 中读取数据
4.1.1. 案例分析
以上两种场景有两个共同的特点
- 会产生大量小文件在
HDFS
上 - 数据需要处理
实现步骤
- 案例结构
- 产生小文件并推送到
HDFS
- 流式计算统计
HDFS
上的小文件 - 运行和总结
案例流程
- 编写
Python
小程序, 在某个目录生成大量小文件Python
是解释型语言, 其程序可以直接使用命令运行无需编译, 所以适合编写快速使用的程序, 很多时候也使用Python
代替Shell
- 使用
Python
程序创建新的文件, 并且固定的生成一段JSON
文本写入文件 - 在真实的环境中, 数据也是一样的不断产生并且被放入
HDFS
中, 但是在真实场景下, 可能是Flume
把小文件不断上传到HDFS
中, 也可能是Sqoop
增量更新不断在某个目录中上传小文件
- 使用
Structured Streaming
汇总数据HDFS
中的数据是不断的产生的, 所以也是流式的数据- 数据集是
JSON
格式, 要有解析JSON
的能力 - 因为数据是重复的, 要对全局的流数据进行汇总和去重, 其实真实场景下的数据清洗大部分情况下也是要去重的
- 使用控制台展示数据
- 最终的数据结果以表的形式呈现
- 使用控制台展示数据意味着不需要在修改展示数据的代码, 将
Sink
部分的内容放在下一个大章节去说明 - 真实的工作中, 可能数据是要落地到
MySQL
,HBase
,HDFS
这样的存储系统中
总结
整个案例运行的逻辑是
Python
程序产生数据到HDFS
中Structured Streaming
从HDFS
中获取数据Structured Streaming
处理数据- 将数据展示在控制台
整个案例的编写步骤
Python
程序Structured Streaming
程序- 运行
4.1.2、产生小文件并推送到 HDFS
随便在任一目录中创建文件 files.py
, 编写以下内容
import os
for index in range(10):
content = """
{"name":"Michael"}
{"name":"Andy", "age":30}
{"name":"Justin", "age":19}
"""
file_name = "/root/text{0}.json".format(index)
with open(file_name, "w") as file:
file.write(content)
os.system("/usr/local/hadoop/bin/hdfs dfs -mkdir -p /structrue/dataset/")
os.system("/usr/local/hadoop/bin/hdfs dfs -put {0} /structrue/dataset/".format(file_name))
4.1.3. 流式计算统计 HDFS 上的小文件
代码
val spark = SparkSession.builder()
.appName("hdfs_source")
.master("local[6]")
.getOrCreate()
spark.sparkContext.setLogLevel("WARN")
val userSchema = new StructType()
.add("name", "string")
.add("age", "integer")
val source = spark
.readStream // 指明读取的是一个流式的 Dataset
.schema(userSchema) // 指定读取到的数据的 Schema
.json("hdfs://datanode01:8020/dataset/dataset") // 指定目录位置, 以及数据格式
val result = source.distinct()
result.writeStream
.outputMode(OutputMode.Update())
.format("console")
.start()
.awaitTermination()
4.1.4. 运行和流程总结
步骤
- 运行
Python
程序 - 运行
Spark
程序 - 总结
运行 Python 程序
-
上传
Python
源码文件到服务器中 -
运行
Python
脚本
# 进入 Python 文件被上传的位置
cd ~
# 创建放置生成文件的目录
mkdir -p /export/dataset
# 运行程序
python gen_files.py
运行 Spark 程序
- 使用
Maven
打包
-
上传至服务器
-
运行
Spark
程序
# 进入保存 Jar 包的文件夹
cd ~
# 运行流程序
spark-submit --class cn.qf.structured.HDFSSource ./original-streaming-0.0.1.jar
总结
Python
生成文件到HDFS
, 这一步在真实环境下, 可能是由Flume
和Sqoop
收集并上传至HDFS
Structured Streaming
从HDFS
中读取数据并处理Structured Streaming
讲结果表展示在控制台
4.2. 从 Kafka 中读取数据
步骤
Structured Streaming
整合Kafka
- 读取
JSON
格式的内容 - 读取多个
Topic
的数据
4.2.1. Kafka 和 Structured Streaming 整合的结构
-
分析
`Structured Streaming` 中使用 `Source` 对接外部系统, 对接 `Kafka` 的 `Source` 叫做 `KafkaSource` `KafkaSource` 中会使用 `KafkaSourceRDD` 来映射外部 `Kafka` 的 `Topic`, 两者的 `Partition` 一一对应
-
结论
Structured Streaming
会并行的从Kafka
中获取数据
Structured Streaming 读取 Kafka 消息的三种方式
Earliest
从每个Kafka
分区最开始处开始获取Assign
手动指定每个Kafka
分区中的Offset
Latest
不再处理之前的消息, 只获取流计算启动后新产生的数据
4.2.2. 需求介绍
- 有一个智能家居品牌叫做
Nest
, 他们主要有两款产品, 一个是恒温器, 一个是摄像头 - 恒温器的主要作用是通过感应器识别家里什么时候有人, 摄像头主要作用是通过学习算法来识别出现在摄像头中的人是否是家里人, 如果不是则报警
- 所以这两个设备都需要统计一个指标, 就是家里什么时候有人, 此需求就是针对这个设备的一部分数据, 来统计家里什么时候有人
Kafka生产者的数据格式
{
"devices": {
"cameras": {
"device_id": "awJo6rH",
"last_event": {
"has_sound": true,
"has_motion": true,
"has_person": true,
"start_time": "2016-12-29T00:00:00.000Z",
"end_time": "2016-12-29T18:42:00.000Z"
}
}
}
}
使用 Structured Streaming 来过滤出来家里有人的数据
把数据转换为 时间 → 是否有人
这样类似的形式
4.2.3 代码实现
创建 Topic 并输入数据到 Topic
- 使用命令创建
Topic
bin/kafka-topics.sh --create --topic streaming-test --replication-factor 1 --partitions 3 --zookeeper qianfeng01:2181
- 开启
Producer
bin/kafka-console-producer.sh --broker-list qianfeng01:9092,qianfeng02:9092,qianfeng03:9092 --topic streaming-test
- 把
JSON
转为单行输入
{"devices":{"cameras":{"device_id":"awJo6rH","last_event":{"has_sound":true,"has_motion":true,"has_person":true,"start_time":"2016-12-29T00:00:00.000Z","end_time":"2016-12-29T18:42:00.000Z"}}}}
因为需要和 Kafka
整合, 所以在启动的时候需要加载和 Kafka
整合的包 spark-sql-kafka-0-10
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql-kafka-0-10_2.11</artifactId>
<version>2.2.0</version>
</dependency>
Spark代码
package com.qf.sparkstreaming.day04
import org.apache.spark.sql.streaming.OutputMode
import org.apache.spark.sql.types.{DataTypes, StructType}
import org.apache.spark.sql.{DataFrame, SparkSession}
/**
* {
* "devices": {
* "cameras": {
* "device_id": "awJo6rH",
* "last_event": {
* "has_sound": true,
* "has_motion": true,
* "has_person": true,
* "start_time": "2016-12-29T00:00:00.000Z",
* "end_time": "2016-12-29T18:42:00.000Z"
* }
* }
* }
* }
*/
object _03KafkaSourceJson {
def main(args: Array[String]): Unit = {
val session: SparkSession = SparkSession.builder().appName("test1").master("local[*]").getOrCreate()
session.sparkContext.setLogLevel("ERROR")
//作为消费者,从kafka读取数据,获取到的数据有schema,
// 分别是 key|value|topic|partition|offset|timestamp|timestampType|
val frame: DataFrame = session.readStream.format("kafka")
.option("kafka.bootstrap.servers","datanode01:9092,datanode01:9092,datanode01:9092")
.option("startingOffsets","earliest")
.option("subscribe","pet").load()
//处理kafka中的数据
val last_event = new StructType()
.add("has_sound",DataTypes.BooleanType)
.add("has_motion",DataTypes.BooleanType)
.add("has_person",DataTypes.BooleanType)
.add("start_time",DataTypes.DateType)
.add("end_time",DataTypes.DateType)
val cameras = new StructType()
.add("device_id",DataTypes.StringType)
.add("last_event",last_event)
val devices = new StructType()
.add("cameras",cameras)
val schema = new StructType()
.add("devices",devices)
//映射时间格式
val jsonOptions = Map("timestampFormat" -> "yyyy-MM-dd'T'HH:mm:ss.sss'Z'")
import session.implicits._
import org.apache.spark.sql.functions._
//处理value是json的数据,然后返回的是字段value的数据是一个json数据
val frame1: DataFrame = frame.selectExpr("cast(value as String)")
.select(from_json('value, schema, jsonOptions).alias("value"))
//查询value里的has_person ,start_time,end_time
val frame2: DataFrame = frame1.
selectExpr("value.devices.cameras.last_event.has_person",
"value.devices.cameras.last_event.start_time",
"value.devices.cameras.last_event.end_time"
)
.filter($"has_person"===true)
.groupBy($"has_person",$"start_time")
.count()
frame2.writeStream
.outputMode(OutputMode.Update())
.format("console")
.start()
.awaitTermination()
}
}
测试
-
进入服务器中, 启动
Kafka
-
启动
Kafka
的Producer
bin/kafka-console-producer.sh --broker-list qianfeng01:9092,qianfeng02:9092,qianfeng03:9092 --topic streaming-test
- 启动代码进行测试
5. Streaming -> Sink
HDFS Sink
Kafka Sink
MySQL Sink
- 自定义
Sink
Tiggers
- 错误恢复和容错语义
5.1. HDFS Sink
案例需求
从 Kafka
接收数据, 从给定的数据集中, 裁剪部分列, 落地于 HDFS
实现步骤
- 从
Kafka
读取数据, 生成源数据集- 连接
Kafka
生成DataFrame
- 从
DataFrame
中取出表示Kafka
消息内容的value
列并转为String
类型
- 连接
- 对源数据集选择列
- 解析
CSV
格式的数据 - 生成正确类型的结果集
- 解析
- 落地
HDFS
代码实现
package com.qf.sparkstreaming.day04
import org.apache.spark.sql.{DataFrame, Dataset, SparkSession}
object _04SinkHdfs {
def main(args: Array[String]): Unit = {
val session: SparkSession = SparkSession.builder()
.appName("test1")
.master("local[*]").getOrCreate()
session.sparkContext.setLogLevel("ERROR")
//作为消费者,从kafka读取数据,获取到的数据有schema,
// 分别是 key|value|topic|partition|offset|timestamp|timestampType|
val frame: DataFrame = session.readStream.format("kafka")
.option("kafka.bootstrap.servers",
"datanode01:9092,datanode01:9092,datanode01:9092")
.option("startingOffsets","earliest")
.option("subscribe","pet").load()
//处理一下数据
val frame1: DataFrame = frame.selectExpr("cast(value as String)")
//保存到本地磁盘
frame1.writeStream
.format("text")
// .option("path","out4") //存储到本地磁盘
.option("path","hdfs://datanode01/hdfssink")
.option("checkpointLocation", "checkpoint")
.start()
.awaitTermination()
}
}
5.2. Kafka Sink
案例需求
从 Kafka
中获取数据, 简单处理, 再次放入 Kafka
实现步骤
- 从
Kafka
读取数据, 生成源数据集- 连接
Kafka
生成DataFrame
- 从
DataFrame
中取出表示Kafka
消息内容的value
列并转为String
类型
- 连接
- 对源数据集选择列
- 解析
CSV
格式的数据 - 生成正确类型的结果集
- 解析
- 再次落地
Kafka
代码实现
package com.qf.sparkstreaming.day04
import org.apache.spark.sql.{DataFrame, SparkSession}
object _05SinkKafka {
def main(args: Array[String]): Unit = {
val session: SparkSession = SparkSession.builder()
.appName("test1").master("local[*]").getOrCreate()
session.sparkContext.setLogLevel("ERROR")
//作为消费者,从kafka读取数据,获取到的数据有schema,
// 分别是 key|value|topic|partition|offset|timestamp|timestampType|
val frame: DataFrame = session.readStream.format("kafka")
.option("kafka.bootstrap.servers",
"datanode01:9092,datanode01:9092,datanode01:9092")
// .option("startingOffsets","earliest")
.option("subscribe","pet").load()
//处理一下数据
val frame1: DataFrame = frame.selectExpr("cast(value as String)")
//保存到kafka中
frame1.writeStream
.format("kafka")
.option("checkpointLocation", "checkpoint")
.option("topic","good")
.option("kafka.bootstrap.servers",
"datanode01:9092,datanode01:9092,qianfeng03:9092")
.start()datanode01.awaitTermination()
}
}
5.3. MySQL Writer(Foreach)
需求分析
从 Kafka
中获取数据, 处理后放入 MySQL
实现步骤
- 创建
DataFrame
表示Kafka
数据源 - 在源
DataFrame
中选择三列数据 - 创建
ForeachWriter
接收每一个批次的数据落地MySQL
Foreach
落地数据
package com.xxx.StructruedStreaming.Day01
import org.apache.spark.sql.{DataFrame, Dataset, ForeachWriter, Row, SparkSession}
import java.sql.{Connection, DriverManager, PreparedStatement}
object _05StructruedStreamingSinkMysql {
def main(args: Array[String]): Unit = {
val sparkSession: SparkSession = SparkSession.builder().appName("mysql").master("local[*]").getOrCreate()
import sparkSession.implicits._
val df: DataFrame = sparkSession.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "datanode01:9092")
.option("subscribe", "pet")
.load()
import org.apache.spark.sql.functions._
//处理一下数据
val ds: Dataset[String] = df.selectExpr("cast(value as string)").as[String]
val df1: DataFrame = ds.map(row => {
val arr: Array[String] = row.split("::")
(arr(0).toInt, arr(1), arr(2))
}).filter(_._3.contains("Comedy")).toDF("id", "name", "info")
df1.writeStream
.foreach(new MyWriter)
.start()
.awaitTermination()
}
}
class MyWriter extends ForeachWriter[Row]{
private var connction:Connection = _
private var statement:PreparedStatement = _
//连接mysql,打开连接
override def open(partitionId: Long, version: Long): Boolean = {
//加载驱动
Class.forName("com.mysql.jdbc.Driver")
connction = DriverManager.getConnection("" +
"jdbc:mysql://localhost:3306/xiao","root","000000")
statement = connction.prepareStatement("" +
"insert into movie values(?,?,?)")
if (connction.equals(null)) false else true
}
//处理方法,用于向mysql中赋值
override def process(value: Row): Unit = {
//给占位符赋值
statement.setInt(1,value.getAs("id"))
statement.setString(2,value.getAs("name"))
statement.setString(3,value.get(2).toString)
//执行
statement.execute()
}
//释放连接
override def close(errorOrNull: Throwable): Unit = {
connction.close()
}
}
5.4. 自定义 Sink
-
Spark
会自动加载所有DataSourceRegister
的子类, 所以需要通过DataSourceRegister
加载Source
和Sink
-
Spark 提供了
StreamSinkProvider
用以创建Sink
, 提供必要的依赖 -
所以如果要创建自定义的
Sink
, 需要做两件事- 创建一个注册器, 继承
DataSourceRegister
提供注册功能, 继承StreamSinkProvider
获取创建Sink
的必备依赖 - 创建一个
Sink
子类
- 创建一个注册器, 继承
自定义 Sink 步骤
- 读取
Kafka
数据 - 简单处理数据
- 创建
Sink
- 创建
Sink
注册器 - 使用自定义
Sink
import org.apache.spark.sql.SparkSession
val spark = SparkSession.builder()
.master("local[6]")
.appName("kafka integration")
.getOrCreate()
import spark.implicits._
val source = spark
.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "datanode01:9092,datanode01:9092,datanode01:9092")
.option("subscribe", "streaming-bank")
.option("startingOffsets", "earliest")
.load()
.selectExpr("CAST(value AS STRING)")
.as[String]
val result = source.map {
item =>
val arr = item.replace("\"", "").split(";")
(arr(0).toInt, arr(1).toInt, arr(5).toInt)
}
.as[(Int, Int, Int)]
.toDF("age", "job", "balance")
class MySQLSink(options: Map[String, String], outputMode: OutputMode) extends Sink {
override def addBatch(batchId: Long, data: DataFrame): Unit = {
val userName = options.get("userName").orNull
val password = options.get("password").orNull
val table = options.get("table").orNull
val jdbcUrl = options.get("jdbcUrl").orNull
val properties = new Properties
properties.setProperty("user", userName)
properties.setProperty("password", password)
data.write.mode(outputMode.toString).jdbc(jdbcUrl, table, properties)
}
}
class MySQLStreamSinkProvider extends StreamSinkProvider with DataSourceRegister {
override def createSink(sqlContext: SQLContext,
parameters: Map[String, String],
partitionColumns: Seq[String],
outputMode: OutputMode): Sink = {
new MySQLSink(parameters, outputMode)
}
override def shortName(): String = "mysql"
}
result.writeStream
.format("mysql")
.option("username", "root")
.option("password", "123456")
.option("table", "streaming-bank-result")
.option("jdbcUrl", "jdbc:mysql://node01:3306/test")
.start()
.awaitTermination()
5.5. Trigger
实现步骤
- 微批次处理
- 连续流处理
微批次处理
什么是微批次 :并不是真正的流, 而是缓存一个批次周期的数据, 后处理这一批次的数据
连续流处理
- 微批次会将收到的数据按照批次划分为不同的
DataFrame
, 后执行DataFrame
, 所以其数据的处理延迟取决于每个DataFrame
的处理速度, 最快也只能在一个DataFrame
结束后立刻执行下一个, 最快可以达到100ms
左右的端到端延迟 - 而连续流处理可以做到大约
1ms
的端到端数据处理延迟 - 连续流处理可以达到
at-least-once
的容错语义 - 从
Spark 2.3
版本开始支持连续流处理, 我们所采用的2.2
版本还没有这个特性, 并且这个特性截止到2.4
依然是实验性质, 不建议在生产环境中使用
例子:
result.writeStream
.outputMode(OutputMode.Complete())
.format("console")
.trigger(Trigger.Continuous("1 second"))
.start()
.awaitTermination()
注意:有限制
- 只支持
Map
类的有类型操作 - 只支持普通的的
SQL
类操作, 不支持聚合 Source
只支持Kafka
Sink
只支持Kafka
,Console
,Memory
代码
// 创建数据源
val spark = SparkSession.builder()
.appName("triggers")
.master("local[6]")
.getOrCreate()
spark.sparkContext.setLogLevel("WARN")
// timestamp, value
val source = spark.readStream
.format("rate")
.load()
// 简单处理
//
val result = source
// 落地
source.writeStream
.format("console")
.outputMode(OutputMode.Append())
// 不写trigger的话 以最快的速度输出,不等待
.trigger(Trigger.Once()) // 只处理一次
.trigger(Trigger.ProcessingTime("20 seconds")) // 20秒一次
.start()
.awaitTermination()
5.6. 错误恢复和容错语义
三种容错语义
at-most-once
在数据从 Source 到 Sink 的过程中, 出错了, Sink 可能没收到数据, 但是不会收到两次, 叫做 at-most-once
一般错误恢复的时候, 不重复计算, 则是 at-most-once
at-least-once
在数据从 Source 到 Sink 的过程中, 出错了, Sink 一定会收到数据, 但是可能收到两次, 叫做 at-least-once
一般错误恢复的时候, 重复计算可能完成也可能未完成的计算, 则是 at-least-once
exactly-once
在数据从 Source 到 Sink 的过程中, 虽然出错了, Sink 一定恰好收到应该收到的数据, 一条不重复也一条都不少, 即是 exactly-once
想做到 exactly-once 是非常困难的
5.7 Sink 的容错
-
读取
WAL offsetlog
恢复出最新的offsets
当
StreamExecution
找到Source
获取数据的时候, 会将数据的起始放在WAL offsetlog
中, 当出错要恢复的时候, 就可以从中获取当前处理批次的数据起始, 例如Kafka
的Offset
-
读取
batchCommitLog
决定是否需要重做最近一个批次当
Sink
处理完批次的数据写入时, 会将当前的批次ID
存入batchCommitLog
, 当出错的时候就可以从中取出进行到哪一个批次了, 和WAL
对比即可得知当前批次是否处理完 -
如果有必要的话, 当前批次数据重做
- 如果上次执行在
(5)
结束前即失效, 那么本次执行里Sink
应该完整写出计算结果 - 如果上次执行在
(5)
结束后才失效, 那么本次执行里Sink
可以重新写出计算结果 (覆盖上次结果), 也可以跳过写出计算结果(因为上次执行已经完整写出过计算结果了)
- 如果上次执行在
-
这样即可保证每次执行的计算结果, 在 Sink 这个层面, 是 不重不丢 的, 即使中间发生过失效和恢复, 所以
Structured Streaming
可以做到exactly-once
容错所需要的存储
存储
offsetlog
和batchCommitLog
关乎于错误恢复offsetlog
和batchCommitLog
需要存储在可靠的空间里offsetlog
和batchCommitLog
存储在Checkpoint
中WAL
其实也存在于Checkpoint
中
指定 Checkpoint
只有指定了 Checkpoint
路径的时候, 对应的容错功能才可以开启
source.writeStream
.format("console")
.outputMode(OutputMode.Append())
.option("checkpointLocation", "path/to/HDFS/dir")// 指定存储
.format("memory")
需要的外部支持
如果要做到 exactly-once
, 只是 Structured Streaming
能做到还不行, 还需要 Source
和 Sink
系统的支持
-
Source
需要支持数据重放当有必要的时候,
Structured Streaming
需要根据start
和end offset
从Source
系统中再次获取数据, 这叫做重放 -
Sink
需要支持幂等写入如果需要重做整个批次的时候,
Sink
要支持给定的ID
写入数据, 这叫幂等写入, 一个ID
对应一条数据进行写入, 如果前面已经写入, 则替换或者丢弃, 不能重复
所以 Structured Streaming
想要做到 exactly-once
, 则也需要外部系统的支持, 如下
Source
Sink