5. 从 Kafka 中读取数据

目标

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

步骤

  1. Kafka 回顾
  2. Structured Streaming 整合 Kafka
  3. 读取 JSON 格式的内容
  4. 读取多个 Topic 的数据

5.1 Kafka 的场景和结构

目标

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

步骤

  1. Kafka 的应用场景
  2. Kafka 的特点
  3. Topic 和 Partitions

5.1.1 Kafka 是一个 Pub / Sub 系统

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

从kafka中拉取数据到es logstash_JSON

(2)发布订阅系统可以有多个 Publisher 对应一个 Subscriber, 例如多个系统都会产生日志, 通过这样的方式, 一个日志处理器可以简单的获取所有系统产生的日志

从kafka中拉取数据到es logstash_JSON_02

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

从kafka中拉取数据到es logstash_Streaming_03

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

从kafka中拉取数据到es logstash_Streaming_04

5.1.2 Kafka 的特点

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

  • 高吞吐量
  • 高可靠性

5.1.3 Topic 和 Partitions

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

从kafka中拉取数据到es logstash_数据_05

(2)Kafka 中使用 Topic 来组织不同类型的消息

从kafka中拉取数据到es logstash_数据_06

(3)Kafka 中的 Topic 要承受非常大的吞吐量, 所以 Topic 应该是可以分片的, 应该是分布式的

从kafka中拉取数据到es logstash_Streaming_07

5.1.4 总结

  • Kafka 的应用场景
  • 一般的系统中, 业务系统会不止一个, 数据系统也会比较复杂
  • 为了减少业务系统和数据系统之间的耦合, 要将其分开, 使用一个中间件来流转数据
  • Kafka 因为其吞吐量超高, 所以适用于这种场景
  • Kafka 如何保证高吞吐量
  • 因为消息会有很多种类, Kafka 中可以创建多个队列, 每一个队列就是一个 Topic, 可以理解为是一个主题, 存放相关的消息
  • 因为 Topic 直接存放消息, 所以 Topic 必须要能够承受非常大的通量, 所以 Topic 是分布式的, 是可以分片的, 使用分布式的并行处理能力来解决高通量的问题

5.2 Kafka 和 Structured Streaming 整合的结构

目标

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

步骤

  1. Topic 的 Offset
  2. Kafka 和 Structured Streaming 的整合结构
  3. Structured Streaming 读取 Kafka 消息的三种方式

5.2.1 Topic 的 Offset

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

从kafka中拉取数据到es logstash_JSON_08

(2)每个分区都对应一系列的 Log 文件, 消息存在于 Log 中, 消息的 ID 就是这条消息在本分区的 Offset 偏移量

从kafka中拉取数据到es logstash_数据_09

其它说明:

Offset 又称作为偏移量, 其实就是一个东西距离另外一个东西的距离

从kafka中拉取数据到es logstash_数据_10

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

5.2.2 Kafka 和 Structured Streaming 整合的结构

从kafka中拉取数据到es logstash_JSON_11

分析

  • Structured Streaming 中使用 Source 对接外部系统, 对接 Kafka 的 Source 叫做 KafkaSource
  • KafkaSource 中会使用 KafkaSourceRDD 来映射外部 Kafka 的 Topic, 两者的 Partition 一一对应

结论

  Structured Streaming 会并行的从 Kafka 中获取数据

5.2.3 Structured Streaming 读取 Kafka 消息的三种方式

从kafka中拉取数据到es logstash_数据_12

  • Earliest 从每个 Kafka 分区最开始处开始获取
  • Assign 手动指定每个 Kafka 分区中的 Offset
  • Latest 不再处理之前的消息, 只获取流计算启动后新产生的数据

5.2.4 总结

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

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

目标

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

步骤

  1. 创建 Topic 并输入数据到 Topic
  2. Spark 整合 kafka
  3. 读取到的 DataFrame 的数据结构

5.3.1 创建 Topic 并输入数据到 Topic

(1)使用命令创建 Topic

bin/kafka-topics.sh --create --topic streaming-test --replication-factor 1 --partitions 3 --zookeeper node01:2181

(2)开启 Producer

bin/kafka-console-producer.sh --broker-list node01:9092,node02:9092,node03:9092 --topic streaming-test

(3)把 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"}}}}

5.3.2 使用 Spark 读取 Kafka 的 Topic

(1)编写 Spark 代码读取 Kafka Topic

val source = spark.readStream
  .format("kafka")
  .option("kafka.bootstrap.servers", "node01:9092,node01:9092,node03:9092")
  .option("subscribe", "streaming_test")
  .option("startingOffsets", "earliest")
  .load()
  • 三个参数
  • kafka.bootstrap.servers : 指定 Kafka 的 Server 地址
  • subscribe : 要监听的 Topic, 可以传入多个, 传入多个 Topic 则监听多个 Topic, 也可以使用 topic-* 这样的通配符写法
  • startingOffsets : 从什么位置开始获取数据, 可选值有 earliestassignlatest
  • format 设置为 Kafka 指定使用 KafkaSource 读取数据

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

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

        

从kafka中拉取数据到es logstash_JSON_13

          前端大多数情况下使用 JSON 做数据交互

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

        

从kafka中拉取数据到es logstash_JSON_14

        可以主动或者被动的把数据交给 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, 必须要有 TopicPartition之类的信息

(3)从 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

类型

解释

key

binary

Kafka 消息的 Key

value

binary

Kafka 消息的 Value

topic

string

本条消息所在的 Topic, 因为整合的时候一个 Dataset 可以对接多个 Topic, 所以有这样一个信息

partition

integer

消息的分区号

offset

long

消息在其分区的偏移量

timestamp

timestamp

消息进入 Kafka 的时间戳

timestampType

integer

时间戳类型

5.3.3 总结

  1. 一定要把 JSON 转为一行, 再使用 Producer 发送, 不然会出现获取多行的情况
  2. 使用 Structured Streaming 连接 Kafka 的时候, 需要配置如下三个参数
  • kafka.bootstrap.servers : 指定 Kafka 的 Server 地址
  • subscribe : 要监听的 Topic, 可以传入多个, 传入多个 Topic 则监听多个 Topic, 也可以使用 topic-* 这样的通配符写法
  • startingOffsets : 从什么位置开始获取数据, 可选值有 earliestassignlatest
  1. 从 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)

5.4  JSON 解析和数据统计

目标

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

步骤

  1. JSON 解析
  2. 数据处理
  3. 运行测试

5.4.1 JSON 解析

(1)准备好 JSON 所在的列

问题:

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

解决方式:

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

(2)编写 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)

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

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

(4)使用 from_json 这个 UDF 格式化 JSON

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

(5)选择格式化过后的 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")

5.4.2 数据处理

(1)统计各个时段有人的数据

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

(2)将数据落地到控制台

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

5.4.3 全部代码

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", "node01:9092,node02:9092,node03: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()

5.4.4 运行测试

(1)进入服务器中, 启动 Kafka

(2)启动 Kafka 的 Producer

bin/kafka-console-producer.sh --broker-list node01:9092,node02:9092,node03:9092 --topic streaming-test

(3)启动 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