StructuredStreaming -- 01 【概述,编程模型,source,sink】

Structured Streaming

1、 回顾

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

  1. SparkAPI 进化过程
  2. Spark 的序列化进化过程
  3. Spark StreamingStructured Streaming

1.1、Spark 编程模型的进化过程

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

过程

  1. 编程模型 RDD 的优点和缺陷
  2. 编程模型 DataFrame 的优点和缺陷
  3. 编程模型 Dataset 的优点和缺陷

在这里插入图片描述

分析编程模型
RDDrdd.flatMap(_.split(" "))
.map((_, 1))
.reduceByKey(_ + _)
.collect
1. 针对自定义数据对象进行处理, 可以处理任意类型的对象, 比较符合面向对象
2. RDD 无法感知到数据的结构, 无法针对数据结构进行编程
DataFramespark.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. SparkDataFrame 设计了新的数据读写框架, 更加强大, 支持的数据源众多
DataSetspark.read
.csv(“…”)
.as[Person]
.where(_.age!= “”)
.groupByKey(_.age)
.count()
.show()
1. Dataset 结合了 RDDDataFrame 的特点, 从 API 上即可以处理结构化数据, 也可以处理非结构化数据
2. DatasetDataFrame 其实是一个东西, 所以 DataFrame 的性能优势, 在 Dataset 上也有

总结

  • RDD 的优点
    1. 面向对象的操作方式
    2. 可以处理任何类型的数据
  • RDD 的缺点
    1. 运行速度比较慢, 执行过程没有优化
    2. API 比较僵硬, 对结构化数据的访问和操作没有优化
  • DataFrame 的优点
    1. 针对结构化数据高度优化, 可以通过列名访问和转换数据
    2. 增加 Catalyst 优化器, 执行过程是优化的, 避免了因为开发者的原因影响效率
  • DataFrame 的缺点
    1. 只能操作结构化数据
    2. 只有无类型的 API, 也就是只能针对列和 SQL 操作数据, API 依然僵硬
  • Dataset 的优点
    1. 结合了 RDDDataFrameAPI, 既可以操作结构化数据, 也可以操作非结构化数据
    2. 既有有类型的 API 也有无类型的 API, 灵活选择

1.2、Spark 的 序列化 的进化过程

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

    1. 序列化和反序列化是什么 ?
    2. Spark 中什么地方用到序列化和反序列化 ?
    3. RDD 的序列化和反序列化如何实现 ?
    4. 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 过程是由 ReducerMapper 中拉取数据, 这里面涉及到两个需要序列化对象的原因

    • RDD 中的数据对象需要在 Mapper 端落盘缓存, 等待拉取
    • MapperReducer 要传输数据对象
  • Spark StreamingReceiver

在这里插入图片描述

  • 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 是什么

    KryoSpark 引入的一个外部的序列化工具, 可以增快 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 中无法感知数据的组成, 无法感知数据结构, 只能以对象的形式处理数据

DataFrameDataset 的特点

  • DataFrameDataset 是为结构化数据优化的

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

1.3、Spark Streaming 和 Structured Streaming

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

Spark Streaming 时代

在这里插入图片描述

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

Structured Streaming 时代

在这里插入图片描述

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

Spark StreamingStructured 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 时候是什么, 以及核心体系原理打下基础

步骤

  1. 需求梳理
  2. Structured Streaming 代码实现
  3. 运行
  4. 验证结果

2.1、需求梳理

在这里插入图片描述

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

整体结构

在这里插入图片描述

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

开发步骤及实施

Socket server 使用 Netcat nc 来实现

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

  1. 编写代码
  2. 启动 nc 发送 Socket 消息
  3. 运行代码接收 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 的体系结构和核心原理, 有两点好处, 一是需要了解原理才好进行性能调优, 二是了解原理后, 才能理解代码执行流程, 从而更好的记忆, 也做到知其然更知其所以然

步骤

  1. WordCount 的执行原理
  2. Structured Streaming 的体系结构

3.1. 无限扩展的表格

问题

  1. 了解 Dataset 这个计算模型和流式计算的关系
  2. 如何使用 Dataset 处理流式数据?
  3. 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
  • 数据需要处理

实现步骤

  1. 案例结构
  2. 产生小文件并推送到 HDFS
  3. 流式计算统计 HDFS 上的小文件
  4. 运行和总结

案例流程

在这里插入图片描述

  1. 编写 Python 小程序, 在某个目录生成大量小文件
    • Python 是解释型语言, 其程序可以直接使用命令运行无需编译, 所以适合编写快速使用的程序, 很多时候也使用 Python 代替 Shell
    • 使用 Python 程序创建新的文件, 并且固定的生成一段 JSON 文本写入文件
    • 在真实的环境中, 数据也是一样的不断产生并且被放入 HDFS 中, 但是在真实场景下, 可能是 Flume 把小文件不断上传到 HDFS 中, 也可能是 Sqoop 增量更新不断在某个目录中上传小文件
  2. 使用 Structured Streaming 汇总数据
    • HDFS 中的数据是不断的产生的, 所以也是流式的数据
    • 数据集是 JSON 格式, 要有解析 JSON 的能力
    • 因为数据是重复的, 要对全局的流数据进行汇总和去重, 其实真实场景下的数据清洗大部分情况下也是要去重的
  3. 使用控制台展示数据
    • 最终的数据结果以表的形式呈现
    • 使用控制台展示数据意味着不需要在修改展示数据的代码, 将 Sink 部分的内容放在下一个大章节去说明
    • 真实的工作中, 可能数据是要落地到 MySQL, HBase, HDFS 这样的存储系统中

总结

整个案例运行的逻辑是

  1. Python 程序产生数据到 HDFS
  2. Structured StreamingHDFS 中获取数据
  3. Structured Streaming 处理数据
  4. 将数据展示在控制台

整个案例的编写步骤

  1. Python 程序
  2. Structured Streaming 程序
  3. 运行

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. 运行和流程总结

步骤

  1. 运行 Python 程序
  2. 运行 Spark 程序
  3. 总结

运行 Python 程序

  1. 上传 Python 源码文件到服务器中

  2. 运行 Python 脚本

   # 进入 Python 文件被上传的位置
   cd ~
   
   # 创建放置生成文件的目录
   mkdir -p /export/dataset
   
   # 运行程序
   python gen_files.py

运行 Spark 程序

  1. 使用 Maven 打包

在这里插入图片描述

  1. 上传至服务器

  2. 运行 Spark 程序

  # 进入保存 Jar 包的文件夹
   cd ~
   
   # 运行流程序
   spark-submit --class cn.qf.structured.HDFSSource ./original-streaming-0.0.1.jar

总结

在这里插入图片描述

  1. Python 生成文件到 HDFS, 这一步在真实环境下, 可能是由 FlumeSqoop 收集并上传至 HDFS
  2. Structured StreamingHDFS 中读取数据并处理
  3. Structured Streaming 讲结果表展示在控制台

4.2. 从 Kafka 中读取数据

步骤

  1. Structured Streaming 整合 Kafka
  2. 读取 JSON 格式的内容
  3. 读取多个 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

  1. 使用命令创建 Topic
   bin/kafka-topics.sh --create --topic streaming-test --replication-factor 1 --partitions 3 --zookeeper qianfeng01:2181

  1. 开启 Producer
   bin/kafka-console-producer.sh --broker-list qianfeng01:9092,qianfeng02:9092,qianfeng03:9092 --topic streaming-test

  1. 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()
    }
}

测试

  1. 进入服务器中, 启动 Kafka

  2. 启动 KafkaProducer

   bin/kafka-console-producer.sh --broker-list qianfeng01:9092,qianfeng02:9092,qianfeng03:9092 --topic streaming-test
  1. 启动代码进行测试

5. Streaming -> Sink

  1. HDFS Sink
  2. Kafka Sink
  3. MySQL Sink
  4. 自定义 Sink
  5. Tiggers
  6. 错误恢复和容错语义

5.1. HDFS Sink

案例需求

Kafka 接收数据, 从给定的数据集中, 裁剪部分列, 落地于 HDFS

实现步骤

  1. Kafka 读取数据, 生成源数据集
    1. 连接 Kafka 生成 DataFrame
    2. DataFrame 中取出表示 Kafka 消息内容的 value 列并转为 String 类型
  2. 对源数据集选择列
    1. 解析 CSV 格式的数据
    2. 生成正确类型的结果集
  3. 落地 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
实现步骤

  1. Kafka 读取数据, 生成源数据集
    1. 连接 Kafka 生成 DataFrame
    2. DataFrame 中取出表示 Kafka 消息内容的 value 列并转为 String 类型
  2. 对源数据集选择列
    1. 解析 CSV 格式的数据
    2. 生成正确类型的结果集
  3. 再次落地 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

实现步骤

  1. 创建 DataFrame 表示 Kafka 数据源
  2. 在源 DataFrame 中选择三列数据
  3. 创建 ForeachWriter 接收每一个批次的数据落地 MySQL
  4. 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 加载 SourceSink

  • Spark 提供了 StreamSinkProvider 用以创建 Sink, 提供必要的依赖

  • 所以如果要创建自定义的 Sink, 需要做两件事

    1. 创建一个注册器, 继承 DataSourceRegister 提供注册功能, 继承 StreamSinkProvider 获取创建 Sink 的必备依赖
    2. 创建一个 Sink 子类

自定义 Sink 步骤

  1. 读取 Kafka 数据
  2. 简单处理数据
  3. 创建 Sink
  4. 创建 Sink 注册器
  5. 使用自定义 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

实现步骤

  1. 微批次处理
  2. 连续流处理

微批次处理

什么是微批次 :并不是真正的流, 而是缓存一个批次周期的数据, 后处理这一批次的数据

在这里插入图片描述

连续流处理

  • 微批次会将收到的数据按照批次划分为不同的 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 中, 当出错要恢复的时候, 就可以从中获取当前处理批次的数据起始, 例如 KafkaOffset

  • 读取 batchCommitLog 决定是否需要重做最近一个批次

    Sink 处理完批次的数据写入时, 会将当前的批次 ID 存入 batchCommitLog, 当出错的时候就可以从中取出进行到哪一个批次了, 和 WAL 对比即可得知当前批次是否处理完

  • 如果有必要的话, 当前批次数据重做

    • 如果上次执行在 (5) 结束前即失效, 那么本次执行里 Sink 应该完整写出计算结果
    • 如果上次执行在 (5) 结束后才失效, 那么本次执行里 Sink 可以重新写出计算结果 (覆盖上次结果), 也可以跳过写出计算结果(因为上次执行已经完整写出过计算结果了)
  • 这样即可保证每次执行的计算结果, 在 Sink 这个层面, 是 不重不丢 的, 即使中间发生过失效和恢复, 所以 Structured Streaming 可以做到 exactly-once

容错所需要的存储

存储

  • offsetlogbatchCommitLog 关乎于错误恢复
  • offsetlogbatchCommitLog 需要存储在可靠的空间里
  • offsetlogbatchCommitLog 存储在 Checkpoint
  • WAL 其实也存在于 Checkpoint

指定 Checkpoint

只有指定了 Checkpoint 路径的时候, 对应的容错功能才可以开启

source.writeStream
  .format("console")
  .outputMode(OutputMode.Append())
  .option("checkpointLocation", "path/to/HDFS/dir")// 指定存储
  .format("memory")

需要的外部支持

如果要做到 exactly-once, 只是 Structured Streaming 能做到还不行, 还需要 SourceSink 系统的支持

  • Source 需要支持数据重放

    当有必要的时候, Structured Streaming 需要根据 startend offsetSource 系统中再次获取数据, 这叫做重放

  • Sink 需要支持幂等写入

    如果需要重做整个批次的时候, Sink 要支持给定的 ID 写入数据, 这叫幂等写入, 一个 ID 对应一条数据进行写入, 如果前面已经写入, 则替换或者丢弃, 不能重复

所以 Structured Streaming 想要做到 exactly-once, 则也需要外部系统的支持, 如下

Source
在这里插入图片描述

Sink

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值