5. 从 Kafka 中读取数据
目标
通过本章节的学习, 便可以理解流式系统和队列间的关系, 同时能够编写代码从
Kafka
以流的方式读取数据步骤
Kafka
回顾Structured Streaming
整合Kafka
- 读取
JSON
格式的内容- 读取多个
Topic
的数据
5.1 Kafka 的场景和结构
目标
通过这一个小节的学习, 大家可以理解
Kfaka
在整个系统中的作用, 日后工作的话, 也必须要先站在更高层去理解系统的组成, 才能完成功能和代码步骤
Kafka
的应用场景Kafka
的特点Topic
和Partitions
5.1.1 Kafka 是一个 Pub / Sub 系统
(1)Pub / Sub
是 Publisher / Subscriber
的简写, 中文称作为发布订阅系统
(2)发布订阅系统可以有多个 Publisher
对应一个 Subscriber
, 例如多个系统都会产生日志, 通过这样的方式, 一个日志处理器可以简单的获取所有系统产生的日志
(3)发布订阅系统也可以一个 Publisher
对应多个 Subscriber
, 这样就类似于广播了, 例如通过这样的方式可以非常轻易的将一个订单的请求分发给所有感兴趣的系统, 减少耦合性
(4)当然, 在大数据系统中, 这样的消息系统往往可以作为整个数据平台的入口, 左边对接业务系统各个模块, 右边对接数据系统各个计算工具
5.1.2 Kafka 的特点
Kafka
有一个非常重要的应用场景就是对接业务系统和数据系统, 作为一个数据管道, 其需要流通的数据量惊人, 所以 Kafka
如果要满足这种场景的话, 就一定具有以下两个特点
- 高吞吐量
- 高可靠性
5.1.3 Topic 和 Partitions
(1)消息和事件经常是不同类型的, 例如用户注册是一种消息, 订单创建也是一种消息
(2)Kafka
中使用 Topic
来组织不同类型的消息
(3)Kafka
中的 Topic
要承受非常大的吞吐量, 所以 Topic
应该是可以分片的, 应该是分布式的
5.1.4 总结
Kafka
的应用场景
- 一般的系统中, 业务系统会不止一个, 数据系统也会比较复杂
- 为了减少业务系统和数据系统之间的耦合, 要将其分开, 使用一个中间件来流转数据
- Kafka 因为其吞吐量超高, 所以适用于这种场景
Kafka
如何保证高吞吐量
- 因为消息会有很多种类,
Kafka
中可以创建多个队列, 每一个队列就是一个Topic
, 可以理解为是一个主题, 存放相关的消息 - 因为
Topic
直接存放消息, 所以Topic
必须要能够承受非常大的通量, 所以Topic
是分布式的, 是可以分片的, 使用分布式的并行处理能力来解决高通量的问题
5.2 Kafka 和 Structured Streaming 整合的结构
目标
通过本小节可以理解
Kafka
和Structured Streaming
整合的结构原理, 同时还能理解Spark
连接Kafka
的时候一个非常重要的参数步骤
Topic
的Offset
Kafka
和Structured Streaming
的整合结构Structured Streaming
读取Kafka
消息的三种方式
5.2.1 Topic 的 Offset
(1)Topic
是分区的, 每一个 Topic
的分区分布在不同的 Broker
上
(2)每个分区都对应一系列的 Log
文件, 消息存在于 Log
中, 消息的 ID
就是这条消息在本分区的 Offset
偏移量
其它说明:
Offset
又称作为偏移量, 其实就是一个东西距离另外一个东西的距离
Kafka
中使用Offset
命名消息, 而不是指定ID
的原因是想表示永远自增,ID
是可以指定的, 但是Offset
只能是一个距离值, 它只会越来越大, 所以, 叫做Offset
而不叫ID
也是这个考虑, 消息只能追加到Log
末尾, 只能增长不能减少
5.2.2 Kafka 和 Structured Streaming 整合的结构
分析
Structured Streaming
中使用Source
对接外部系统, 对接Kafka
的Source
叫做KafkaSource
KafkaSource
中会使用KafkaSourceRDD
来映射外部Kafka
的Topic
, 两者的Partition
一一对应
结论
Structured Streaming
会并行的从 Kafka
中获取数据
5.2.3 Structured Streaming 读取 Kafka 消息的三种方式
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
, 从其中获取数据步骤
- 创建
Topic
并输入数据到Topic
Spark
整合kafka
- 读取到的
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
: 从什么位置开始获取数据, 可选值有earliest
,assign
,latest
format
设置为Kafka
指定使用KafkaSource
读取数据
(2)
思考: 从 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
之类的信息
(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 | 类型 | 解释 |
|
|
|
|
|
|
|
| 本条消息所在的 |
|
| 消息的分区号 |
|
| 消息在其分区的偏移量 |
|
| 消息进入 |
|
| 时间戳类型 |
5.3.3 总结
- 一定要把
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)
5.4 JSON 解析和数据统计
目标
通过本章的学习, 便能够解析
Kafka
中的JSON
数据, 这是一个重点中的重点步骤
JSON
解析- 数据处理
- 运行测试
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