1. 概述
本文将带你深入探讨如何在事件驱动架构中使用 Apache Kafka 进行数据建模。
2. 环境搭建
一个 Kafka 集群由多个 Kafka broker 组成,它们通过 Zookeeper 集群进行协调管理。为了简化操作,我们直接使用 Confluent 提供的现成 Docker 镜像和 docker-compose 配置文件来启动集群,配置来源是 Confluent 官方文档。
首先下载 3 节点 Kafka 集群的 docker-compose.yml
文件:
$ BASE_URL="https://raw.githubusercontent.com/confluentinc/cp-docker-images/5.3.3-post/examples/kafka-cluster"
$ curl -Os "$BASE_URL"/docker-compose.yml
接着启动 Zookeeper 和 Kafka broker 节点:
$ docker-compose up -d
最后确认所有 Kafka broker 是否正常启动:
$ docker-compose logs kafka-1 kafka-2 kafka-3 | grep started
kafka-1_1 | [2020-12-27 10:15:03,783] INFO [KafkaServer id=1] started (kafka.server.KafkaServer)
kafka-2_1 | [2020-12-27 10:15:04,134] INFO [KafkaServer id=2] started (kafka.server.KafkaServer)
kafka-3_1 | [2020-12-27 10:15:03,853] INFO [KafkaServer id=3] started (kafka.server.KafkaServer)
✅ 至此,Kafka 集群已就绪。
3. 事件基础概念
在进行事件驱动系统的数据建模前,我们需要先了解几个核心概念:事件、事件流、生产者消费者、主题等。
3.1. 事件(Event)
在 Kafka 的世界里,事件是对领域中发生某件事的信息记录。它以键值对的形式保存,并附带时间戳、元信息、头部等属性。
假设我们在建模一场国际象棋游戏,那么“走子”就是一个典型的事件:
可以看到,事件包含了执行者(actor)、动作(action)以及发生时间等关键信息。在这个例子中,Player1
是执行者,动作为“车从 a1 移动到 a5”,时间为 12/2020/25 00:08:30
。
3.2. 消息流(Message Stream)
Apache Kafka 是一个流处理系统,能够捕获并处理事件流。在我们的象棋游戏中,可以把玩家的每一步都看作一个事件流中的消息。
每当发生一个事件,棋盘的状态就会发生变化。传统数据库通常用表结构来存储对象的最新静态状态。
而事件流则可以帮助我们捕捉两个连续状态之间的动态变化。如果我们重放这些不可变的事件序列,就可以从一个状态过渡到另一个状态。这就是所谓的 流表二象性(Stream Table Duality)。
来看两个连续事件组成的事件流示意图:
4. 主题(Topics)
在这一节中,我们将学习如何对 Kafka 中的消息进行分类。
4.1. 消息分类机制
在 Kafka 这样的消息系统中,产生事件的一方被称为生产者(Producer),读取消费消息的一方则是消费者(Consumer)。
现实中,每个生产者可能生成多种类型的事件,如果让每个消费者都去过滤自己关心的消息,效率显然不高。
为了解决这个问题,Kafka 引入了 Topic(主题)机制,用于将具有相似语义的消息归类在一起。这样消费者就可以高效地订阅和处理相关消息。
比如在象棋游戏中,我们可以创建一个名为 chess-moves
的 Topic 来统一收集所有走子事件:
$ docker run \
--net=host --rm confluentinc/cp-kafka:5.0.0 \
kafka-topics --create --topic chess-moves \
--if-not-exists \
--partitions 1 --replication-factor 1 \
--zookeeper localhost:32181
Created topic "chess-moves".
4.2. 生产者与消费者交互
下面我们看看生产者和消费者是如何通过 Topic 来传递消息的。这里使用 Kafka 自带的命令行工具 kafka-console-producer
和 kafka-console-consumer
来演示。
首先启动一个名为 kafka-producer
的容器,进入交互模式并调用生产者工具:
$ docker run \
--net=host \
--name=kafka-producer \
-it --rm \
confluentinc/cp-kafka:5.0.0 /bin/bash
# kafka-console-producer --broker-list localhost:19092,localhost:29092,localhost:39092 \
--topic chess-moves \
--property parse.key=true --property key.separator=:
同时启动另一个名为 kafka-consumer
的容器,运行消费者工具:
$ docker run \
--net=host \
--name=kafka-consumer \
-it --rm \
confluentinc/cp-kafka:5.0.0 /bin/bash
# kafka-console-consumer --bootstrap-server localhost:19092,localhost:29092,localhost:39092 \
--topic chess-moves --from-beginning \
--property print.key=true --property print.value=true --property key.separator=:
现在通过生产者发送一条走子记录:
>{Player1 : Rook, a1->a5}
消费者会实时收到这条消息,输出如下:
{Player1 : Rook, a1->a5}
5. 分区(Partitions)
接下来我们看看如何通过分区实现更细粒度的消息分类,并提升整个系统的并发性能。
5.1. 并发支持
我们可以将一个 Topic 划分为多个分区(Partition),并启动多个消费者分别消费不同分区的消息。这种方式可以显著提高系统的吞吐量。
默认情况下,在创建 Topic 时如果不显式指定分区数,Kafka 会自动创建一个单分区的 Topic。不过对于已存在的 Topic,我们也可以后续增加分区数。例如为 chess-moves
增加至 3 个分区:
$ docker run \
--net=host \
--rm confluentinc/cp-kafka:5.0.0 \
bash -c "kafka-topics --alter --zookeeper localhost:32181 --topic chess-moves --partitions 3"
WARNING: If partitions are increased for a topic that has a key, the partition logic or ordering of the messages will be affected
Adding partitions succeeded!
⚠️ 注意:增加分区数会影响基于 key 的分区逻辑或消息顺序。
5.2. 分区键(Partition Key)
在同一个 Topic 内部,Kafka 使用分区键来决定消息应该被路由到哪个分区。生产者通过 key 计算出目标分区,消费者则从特定分区读取消息。
默认策略是对 key 做哈希后取模分区数,然后将消息投递到对应分区。
我们再次使用 kafka-console-producer
发送两条来自不同玩家的走子记录:
# kafka-console-producer --broker-list localhost:19092,localhost:29092,localhost:39092 \
--topic chess-moves \
--property parse.key=true --property key.separator=:
>{Player1: Rook, a1 -> a5}
>{Player2: Bishop, g3 -> h4}
>{Player1: Rook, a5 -> e5}
>{Player2: Bishop, h4 -> g3}
现在启动两个消费者,分别监听 partition-1 和 partition-2:
# kafka-console-consumer --bootstrap-server localhost:19092,localhost:29092,localhost:39092 \
--topic chess-moves --from-beginning \
--property print.key=true --property print.value=true \
--property key.separator=: \
--partition 1
{Player2: Bishop, g3 -> h4}
{Player2: Bishop, h4 -> g3}
可以看到,所有 Player2 的走子都被分配到了 partition-1;同理,Player1 的走子则进入了 partition-0。
6. 扩展性考量
合理设计 Topic 和 Partition 的数量,对 Kafka 集群的水平扩展至关重要。
- Topic 更像是预定义的数据分类方式
- Partition 则是运行时动态划分的数据分片
此外,还需要注意实际环境中分区数量的限制。因为 每个分区在 broker 上都会映射为一个目录,增加分区数也会增加操作系统打开的文件句柄数。
根据 Confluent 的建议,每个 broker 的分区数应控制在 100 × b × r
以内,其中:
b
表示 Kafka 集群中的 broker 数量r
表示副本因子(replication factor)
7. 小结
本文借助 Docker 环境介绍了 Kafka 数据建模的基础知识,包括事件、主题、分区等核心概念。掌握了这些基本要素之后,我们就能更好地理解和应用事件流架构,构建可扩展、高性能的事件驱动系统。