Structured Streaming 快速入门系列(二)Structured Streaming 实战之 Souce

Source

目标和过程

目标

流式计算一般就是通过数据源读取数据, 经过一系列处理再落地到某个地方, 所以这一小节先了解一下如何读取数据, 可以整合哪些数据源

过程

  • 从 HDFS 中读取数据
  • 从 Kafka 中读取数据

从 HDFS 中读取数据

目标和过程

目标

在数据处理的时候, 经常会遇到这样的场景

在这里插入图片描述
有时候也会遇到这样的场景

在这里插入图片描述
以上两种场景有两个共同的特点

  • 会产生大量小文件在 HDFS 上
  • 数据需要处理

通过本章节的学习, 便能够更深刻的理解这种结构, 具有使用 Structured Streaming 整合 HDFS, 从其中读取数据的能力

步骤

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

案例结构

目标和步骤

目标

通过本章节可以了解案例的过程和步骤, 以及案例的核心意图

步骤

  • 案例结构
  • 实现步骤
  • 难点和易错点

案例流程

在这里插入图片描述
编写 Python 小程序, 在某个目录生成大量小文件

  • Python 是解释型语言, 其程序可以直接使用命令运行无需编译, 所以适合编写快速使用的程序, 很多时候也使用 Python 代替 Shell
  • 使用 Python 程序创建新的文件, 并且固定的生成一段 JSON 文本写入文件
  • 在真实的环境中, 数据也是一样的不断产生并且被放入 HDFS 中, 但是在真实场景下, 可能是 Flume 把小文件不断上传到 HDFS 中, 也可能是 Sqoop 增量更新不断在某个目录中上传小文件

使用 Structured Streaming 汇总数据

  • HDFS 中的数据是不断的产生的, 所以也是流式的数据
  • 数据集是 JSON 格式, 要有解析 JSON 的能力
  • 因为数据是重复的, 要对全局的流数据进行汇总和去重, 其实真实场景下的数据清洗大部分情况下也是要去重的

使用控制台展示数据

  • 最终的数据结果以表的形式呈现
  • 使用控制台展示数据意味着不需要在修改展示数据的代码, 将 Sink 部分的内容放在下一个大章节去说明
  • 真实的工作中, 可能数据是要落地到 MySQL, HBase, HDFS 这样的存储系统中

实现步骤

Step 1: 编写 Python 脚本不断的产生数据

  • 使用 Python 创建字符串保存文件中要保存的数据
  • 创建文件并写入文件内容
  • 使用 Python 调用系统 HDFS 命令上传文件

Step 2: 编写 Structured Streaming 程序处理数据

  • 创建 SparkSession
  • 使用 SparkSession 的 readStream 读取数据源
  • 使用 Dataset 操作数据, 只需要去重
  • 使用 Dataset 的 writeStream 设置 Sink 将数据展示在控制台中

Step 3: 部署程序, 验证结果

  • 上传脚本到服务器中, 使用 python 命令运行脚本
  • 开启流计算应用, 读取 HDFS 中对应目录的数据
  • 查看运行结果

难点和易错点

在读取 HDFS 的文件时, Source 不仅对接数据源, 也负责反序列化数据源中传过来的数据

  • Source 可以从不同的数据源中读取数据, 如 Kafka, HDFS
  • 数据源可能会传过来不同的数据格式, 如 JSON, Parquet

读取 HDFS 文件的这个 Source 叫做 FileStreamSource

  • 从命名就可以看出来这个 Source 不仅支持 HDFS, 还支持本地文件读取, 亚马逊云, 阿里云 等文件系统的读取, 例如: file://, s3://, oss://

基于流的 Dataset 操作和基于静态数据集的 Dataset 操作是一致的

总结

整个案例运行的逻辑是

  • Python 程序产生数据到 HDFS 中
  • Structured Streaming 从 HDFS 中获取数据
  • Structured Streaming 处理数据
  • 将数据展示在控制台

整个案例的编写步骤

  • Python 程序
  • Structured Streaming 程序
  • 运行

产生小文件并推送到 HDFS

目标和步骤

目标

通过本章节看到 Python 的大致语法, 并了解 Python 如何编写脚本完成文件的操作, 其实不同的语言使用起来并没有那么难, 完成一些简单的任务还是很简单的

步骤

  • 创建 Python 代码文件
  • 编写代码
  • 本地测试, 但是因为本地环境搭建比较浪费大家时间, 所以暂时不再本地测试

代码编写

随便在任一目录中创建文件 gen_files.py, 编写以下内容

import os

for index in range(100):
    content = """
    {"name":"Michael"}
    {"name":"Andy", "age":30}
    {"name":"Justin", "age":19}
    """

    file_name = "/opt/data/text{0}.json".format(index)

    with open(file_name, "w") as file:  
        file.write(content)

    os.system("/opt/module/hadoop-2.7.2/bin/hdfs dfs -mkdir -p /input/data/")
    os.system("/opt/module/hadoop-2.7.2/bin/hdfs dfs -put {0} /input/data/".format(file_name))
  • 创建文件, 使用这样的写法是因为 with 是一种 Python 的特殊语法, 如果使用 with 去创建文件的话, 使用结束后会自动关闭流

总结

  • Python 的语法灵活而干净, 比较易于编写
  • 对于其它的语言可以玩乐性质的去使用, 其实并没有很难

流式计算统计 HDFS 上的小文件

目标和步骤

目标

通过本章节的学习, 大家可以了解到如何使用 Structured Streaming 读取 HDFS 中的文件, 并以 JSON 的形式解析

步骤

  • 创建文件
  • 编写代码

代码

object HDFSSource {
  def main(args: Array[String]): Unit = {
    Logger.getLogger("org").setLevel(Level.ERROR)
    //1.创建SparkSession
    val spark = SparkSession.builder()
      .appName(this.getClass.getSimpleName)
      .master("local[6]")
      .getOrCreate()


    //2.数据读取 , 目录只能是文件夹,不能是某一个文件

    val schema = new StructType()
      .add("name", "String")
      .add("age", "integer")


    val source = spark.readStream.schema(schema).json("hdfs://Bigdata01:9000/input/data")

    //3.输出结果
    source.writeStream
      .outputMode(OutputMode.Append())
      .format("console") //打印到控制台上
      .start()
      .awaitTermination()
  }
}

总结

在这里插入图片描述

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

从 Kafka 中读取数据

目标和步骤

目标

通过本章节的学习, 便可以理解流式系统和队列间的关系, 同时能够编写代码从 Kafka 以流的方式读取数据

步骤

  • Kafka 回顾
  • Structured Streaming 整合 Kafka
  • 读取 JSON 格式的内容
  • 读取多个 Topic 的数据

Kafka 的场景和结构

目标和步骤

目标

通过这一个小节的学习, 大家可以理解 Kfaka 在整个系统中的作用, 日后工作的话, 也必须要先站在更高层去理解系统的组成, 才能完成功能和代码

步骤

  • Kafka 的应用场景
  • Kafka 的特点
  • Topic 和 Partitions

Kafka 是一个 Pub / Sub 系统

Pub / Sub 是 Publisher / Subscriber 的简写, 中文称作为发布订阅系统

在这里插入图片描述
发布订阅系统可以有多个 Publisher 对应一个 Subscriber, 例如多个系统都会产生日志, 通过这样的方式, 一个日志处理器可以简单的获取所有系统产生的日志

在这里插入图片描述
发布订阅系统也可以一个 Publisher 对应多个 Subscriber, 这样就类似于广播了, 例如通过这样的方式可以非常轻易的将一个订单的请求分发给所有感兴趣的系统, 减少耦合性

在这里插入图片描述
当然, 在大数据系统中, 这样的消息系统往往可以作为整个数据平台的入口, 左边对接业务系统各个模块, 右边对接数据系统各个计算工具

在这里插入图片描述
Kafka 的特点

Kafka 有一个非常重要的应用场景就是对接业务系统和数据系统, 作为一个数据管道, 其需要流通的数据量惊人, 所以 Kafka 如果要满足这种场景的话, 就一定具有以下两个特点

  • 高吞吐量
  • 高可靠性

Topic 和 Partitions

消息和事件经常是不同类型的, 例如用户注册是一种消息, 订单创建也是一种消息

在这里插入图片描述
Kafka 中使用 Topic 来组织不同类型的消息

在这里插入图片描述
Kafka 中的 Topic 要承受非常大的吞吐量, 所以 Topic 应该是可以分片的, 应该是分布式的

在这里插入图片描述
总结

Kafka 的应用场景

  • 一般的系统中, 业务系统会不止一个, 数据系统也会比较复杂
  • 为了减少业务系统和数据系统之间的耦合, 要将其分开, 使用一个中间件来流转数据
  • Kafka 因为其吞吐量超高, 所以适用于这种场景

Kafka 如何保证高吞吐量

  • 因为消息会有很多种类, Kafka 中可以创建多个队列, 每一个队列就是一个 Topic, 可以理解为是一个主题, 存放相关的消息
  • 因为 Topic 直接存放消息, 所以 Topic 必须要能够承受非常大的通量, 所以 Topic 是分布式的, 是可以分片的, 使用分布式的并行处理能力来解决高通量的问题

Kafka 和 Structured Streaming 整合的结构

目标和步骤

目标

通过本小节可以理解 Kafka 和 Structured Streaming 整合的结构原理, 同时还能理解 Spark 连接 Kafka 的时候一个非常重要的参数

步骤

  • Topic 的 Offset
  • Kafka 和 Structured Streaming 的整合结构
  • Structured Streaming 读取 Kafka 消息的三种方式

Topic 的 Offset

Topic 是分区的, 每一个 Topic 的分区分布在不同的 Broker 上

在这里插入图片描述
每个分区都对应一系列的 Log 文件, 消息存在于 Log 中, 消息的 ID 就是这条消息在本分区的 Offset 偏移量

在这里插入图片描述
Offset 又称作为偏移量, 其实就是一个东西距离另外一个东西的距离

在这里插入图片描述
Kafka 中使用 Offset 命名消息, 而不是指定 ID 的原因是想表示永远自增, ID 是可以指定的, 但是 Offset 只能是一个距离值, 它只会越来越大, 所以, 叫做 Offset 而不叫 ID 也是这个考虑, 消息只能追加到 Log 末尾, 只能增长不能减少

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 不再处理之前的消息, 只获取流计算启动后新产生的数据

总结

  • Kafka 中的消息存放在某个 Topic 的某个 Partition 中, 消息是不可变的, 只会在消息过期的时候从最早的消息开始删除, 消息的 ID 也叫做 Offset, 并且只能正增长
  • Structured Streaming 整合 Kafka 的时候, 会并行的通过 Offset 从所有 Topic 的 Partition 中获取数据
  • Structured Streaming 在从 Kafka 读取数据的时候, 可以选择从最早的地方开始读取, 也可以选择从任意位置读取, 也可以选择只读取最新的

需求介绍

目标和步骤

目标

通过本章节的学习, 可以掌握一个常见的需求, 并且了解后面案例的编写步骤

步骤

  • 需求
  • 数据

需求

模拟一个智能物联网系统的数据统计

在这里插入图片描述

  • 有一个智能家居品牌叫做 Nest, 他们主要有两款产品, 一个是恒温器, 一个是摄像头
  • 恒温器的主要作用是通过感应器识别家里什么时候有人, 摄像头主要作用是通过学习算法来识别出现在摄像头中的人是否是家里人, 如果不是则报警
  • 所以这两个设备都需要统计一个指标, 就是家里什么时候有人, 此需求就是针对这个设备的一部分数据, 来统计家里什么时候有人

使用生产者在 Kafka 的 Topic : streaming-test 中输入 JSON 数据

{
  "devices": {
    "cameras": {
      "device_id": "awJo6rH",
      "last_event": {
        "has_sound": true,
        "has_motion": true,
        "has_person": true,
        "start_time": "2020-09-19T00:00:00.000Z",
        "end_time": "2020-09-19T18:42:00.000Z"
      }
    }
  }
}

使用 Structured Streaming 来过滤出来家里有人的数据

把数据转换为 时间 → 是否有人 这样类似的形式

数据转换

追踪 JSON 数据的格式

可以在一个在线的工具 https://jsonformatter.org/ 中格式化 JSON

反序列化

JSON 数据本质上就是字符串, 只不过这个字符串是有结构的, 虽然有结构, 但是很难直接从字符串中取出某个值

而反序列化, 就是指把 JSON 数据转为对象, 或者转为 DataFrame, 可以直接使用某一个列或者某一个字段获取数据, 更加方便

而想要做到这件事, 必须要先根据数据格式, 编写 Schema 对象, 从而通过一些方式转为 DataFrame

val eventType = new StructType()
  .add("has_sound", BooleanType, nullable = true)
  .add("has_motion", BooleanType, nullable = true)
  .add("has_person", BooleanType, nullable = true)
  .add("start_time", DateType, nullable = true)
  .add("end_time", DateType, nullable = true)

val camerasType = new StructType()
  .add("device_id", StringType, nullable = true)
  .add("last_event", eventType, nullable = true)

val devicesType = new StructType()
  .add("cameras", camerasType, nullable = true)

val schema = new StructType()
  .add("devices", devicesType, nullable = true)

总结
业务简单来说, 就是收集智能家居设备的数据, 通过流计算的方式计算其特征规律

Kafka 常见的业务场景就是对接业务系统和数据系统

  • 业务系统经常会使用 JSON 作为数据传输格式
  • 所以使用 Structured Streaming 来对接 Kafka 并反序列化 Kafka 中的 JSON 格式的消息, 是一个非常重要的技能

无论使用什么方式, 如果想反序列化 JSON 数据, 就必须要先追踪 JSON 数据的结构

使用 Spark 流计算连接 Kafka 数据源

目标和步骤

目标

通过本章节的数据, 能够掌握如何使用 Structured Streaming 对接 Kafka, 从其中获取数据

步骤

  • 创建 Topic 并输入数据到 Topic
  • Spark 整合 kafka
  • 读取到的 DataFrame 的数据结构

创建 Topic 并输入数据到 Topic

使用命令创建 Topic

bin/kafka-topics.sh --create --topic streaming-test --replication-factor 1 --partitions 3 --zookeeper Bigdata01:2181,Bigdata02:2181,Bigdata03:2181

开启 Producer

bin/kafka-console-producer.sh --broker-list Bigdata01:9092,Bigdata02:9092,Bigdata03:9092 --topic streaming-test

把 JSON 转为单行输入

{"devices":{"cameras":{"device_id":"awJo6rH","last_event":{"has_sound":true,"has_motion":true,"has_person":true,"start_time":"2020-09-19T00:00:00.000Z","end_time":"2020-09-19T18:42:00.000Z"}}}}

使用 Spark 读取 Kafka 的 Topic

编写 Spark 代码读取 Kafka Topic

val source = spark.readStream
  .format("kafka")
  .option("kafka.bootstrap.servers", "Bigdata01:9092,Bigdata02:9092,Bigdata03:9092")
  .option("subscribe", "streaming_test")
  .option("startingOffsets", "earliest")
  .load()

三个参数

  • kafka.bootstrap.servers : 指定 Kafka 的 Server 地址
  • subscribe : 要监听的 Topic, 可以传入多个, 传入多个 Topic 则监听多个 Topic, 也可以使用 topic-* 这样的通配符写法
  • startingOffsets : 从什么位置开始获取数据, 可选值有 earliest, assign, latest

format 设置为 Kafka 指定使用 KafkaSource 读取数据

思考: 从 Kafka 中应该获取到什么?

业务系统有很多种类型, 有可能是 Web 程序, 有可能是物联网

在这里插入图片描述
前端大多数情况下使用 JSON 做数据交互

问题1: 业务系统如何把数据给 Kafka ?

在这里插入图片描述
可以主动或者被动的把数据交给 Kafka, 但是无论使用什么方式, 都在使用 Kafka 的 Client 类库来完成这件事, Kafka 的类库调用方式如下

Producer<String, String> producer = new KafkaProducer<String, String>(properties);
producer.send(new ProducerRecord<String, String>("HelloWorld", msg));

其中发给 Kafka 的消息是 KV 类型的

问题2: 使用 Structured Streaming 访问 Kafka 获取数据的时候, 需要什么东西呢?

  • 需求1: 存储当前处理过的 Kafka 的 Offset
  • 需求2: 对接多个 Kafka Topic 的时候, 要知道这条数据属于哪个 Topic

结论

  • Kafka 中收到的消息是 KV 类型的, 有 Key, 有 Value
  • Structured Streaming 对接 Kafka 的时候, 每一条 Kafka 消息不能只是 KV, 必须要有 Topic, Partition 之类的信息

从 Kafka 获取的 DataFrame 格式

source.printSchema()

结果如下

root
 |-- key: binary (nullable = true)
 |-- value: binary (nullable = true)
 |-- topic: string (nullable = true)
 |-- partition: integer (nullable = true)
 |-- offset: long (nullable = true)
 |-- timestamp: timestamp (nullable = true)
 |-- timestampType: integer (nullable = true)

从 Kafka 中读取到的并不是直接是数据, 而是一个包含各种信息的表格, 其中每个字段的含义如下

Key类型解释
keybinaryKafka 消息的 Key
valuebinaryKafka 消息的 Value
topicstring本条消息所在的 Topic, 因为整合的时候一个 Dataset 可以对接多个 Topic, 所以有这样一个信息
partitioninteger消息的分区号
offsetlong消息在其分区的偏移量
timestamptimestamp消息进入 Kafka 的时间戳
timestampTypeinteger时间戳类型

总结

一定要把 JSON 转为一行, 再使用 Producer 发送, 不然会出现获取多行的情况

使用 Structured Streaming 连接 Kafka 的时候, 需要配置如下三个参数

  • kafka.bootstrap.servers : 指定 Kafka 的 Server 地址
  • subscribe : 要监听的 Topic, 可以传入多个, 传入多个 Topic 则监听多个 Topic, 也可以使用 topic-* 这样的通配符写法
  • startingOffsets : 从什么位置开始获取数据, 可选值有 earliest, assign, latest

从 Kafka 获取到的 DataFrame 的 Schema 如下

root
 |-- key: binary (nullable = true)
 |-- value: binary (nullable = true)
 |-- topic: string (nullable = true)
 |-- partition: integer (nullable = true)
 |-- offset: long (nullable = true)
 |-- timestamp: timestamp (nullable = true)
 |-- timestampType: integer (nullable = true)

JSON 解析和数据统计

目标和步骤

目标

通过本章的学习, 便能够解析 Kafka 中的 JSON 数据, 这是一个重点中的重点

步骤

  • JSON 解析
  • 数据处理
  • 运行测试

JSON 解析

准备好 JSON 所在的列

问题

由 Dataset 的结构可以知道 key 和 value 列的类型都是 binary 二进制, 所以要将其转为字符串, 才可进行 JSON 解析

解决方式

source.selectExpr("CAST(key AS STRING) as key", "CAST(value AS STRING) as value")

编写 Schema 对照 JSON 的格式

Key 要对应 JSON 中的 Key

Value 的类型也要对应 JSON 中的 Value 类型

val eventType = new StructType()
  .add("has_sound", BooleanType, nullable = true)
  .add("has_motion", BooleanType, nullable = true)
  .add("has_person", BooleanType, nullable = true)
  .add("start_time", DateType, nullable = true)
  .add("end_time", DateType, nullable = true)

val camerasType = new StructType()
  .add("device_id", StringType, nullable = true)
  .add("last_event", eventType, nullable = true)

val devicesType = new StructType()
  .add("cameras", camerasType, nullable = true)

val schema = new StructType()
  .add("devices", devicesType, nullable = true)

因为 JSON 中包含 Date 类型的数据, 所以要指定时间格式化方式

val jsonOptions = Map("timestampFormat" -> "yyyy-MM-dd'T'HH:mm:ss.sss'Z'")

使用 from_json 这个 UDF 格式化 JSON

.select(from_json('value, schema, jsonOptions).alias("parsed_value"))

选择格式化过后的 JSON 中的字段

因为 JSON 被格式化过后, 已经变为了 StructType, 所以可以直接获取其中某些字段的值

.selectExpr("parsed_value.devices.cameras.last_event.has_person as has_person",
          "parsed_value.devices.cameras.last_event.start_time as start_time")

数据处理

统计各个时段有人的数据

.filter('has_person === true)
.groupBy('has_person, 'start_time)
.count()

将数据落地到控制台

result.writeStream
  .outputMode(OutputMode.Complete())
  .format("console")
  .start()
  .awaitTermination()

全部代码

import org.apache.spark.sql.SparkSession

val spark = SparkSession.builder()
  .master("local[6]")
  .appName("kafka integration")
  .getOrCreate()

import org.apache.spark.sql.streaming.OutputMode
import org.apache.spark.sql.types._

val source = spark
  .readStream
  .format("kafka")
  .option("kafka.bootstrap.servers", "Bigdata01:9092,Bigdata02:9092,Bigdata03:9092")
  .option("subscribe", "streaming-test")
  .option("startingOffsets", "earliest")
  .load()

val eventType = new StructType()
  .add("has_sound", BooleanType, nullable = true)
  .add("has_motion", BooleanType, nullable = true)
  .add("has_person", BooleanType, nullable = true)
  .add("start_time", DateType, nullable = true)
  .add("end_time", DateType, nullable = true)

val camerasType = new StructType()
  .add("device_id", StringType, nullable = true)
  .add("last_event", eventType, nullable = true)

val devicesType = new StructType()
  .add("cameras", camerasType, nullable = true)

val schema = new StructType()
  .add("devices", devicesType, nullable = true)

val jsonOptions = Map("timestampFormat" -> "yyyy-MM-dd'T'HH:mm:ss.sss'Z'")

import org.apache.spark.sql.functions._
import spark.implicits._

val result = source.selectExpr("CAST(key AS STRING) as key", "CAST(value AS STRING) as value")
    .select(from_json('value, schema, jsonOptions).alias("parsed_value"))
    .selectExpr("parsed_value.devices.cameras.last_event.has_person as has_person",
      "parsed_value.devices.cameras.last_event.start_time as start_time")
    .filter('has_person === true)
    .groupBy('has_person, 'start_time)
    .count()

result.writeStream
  .outputMode(OutputMode.Complete())
  .format("console")
  .start()
  .awaitTermination()

运行测试

进入服务器中, 启动 Kafka

启动 Kafka 的 Producer

bin/kafka-console-producer.sh --broker-list Bigdata01:9092,Bigdata02:9092,Bigdata03:9092 --topic streaming-test

启动 Spark shell 并拷贝代码进行测试

./bin/spark-shell --packages org.apache.spark:spark-sql-kafka-0-10_2.11:2.2.0

因为需要和 Kafka 整合, 所以在启动的时候需要加载和 Kafka 整合的包 spark-sql-kafka-0-10

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值