把 Kafka 画成一个标着“Kafka”、有箭头进出的方框很容易,但这个方框隐藏了人们所依赖的每一个特性:什么是有序的、什么不是,数据有几份副本,Broker 宕机时会发生什么,以及多个服务如何在互不干扰的情况下读取同一个流。
本文画出一个包含关键部分的 Kafka 集群:Broker、主题和分区、Leader 与 Follower 副本、带键的生产者、消费者组和偏移量,以及管理集群的 KRaft 控制器。
主题、分区与 Broker
主题 是一个有名字的记录流,比如 orders。每个主题被划分为若干 分区,每个分区都是一个只追加的日志,其中每条记录都有一个递增的 偏移量。
分区是一切的单位:
- 顺序: 记录在分区内有序,跨分区则从不保证顺序。
- 并行度: 在一个消费者组内,能同时消费一个主题的消费者数量最多等于它的分区数。
- 分布: 分区分布在各个 Broker 上,也就是集群中的服务器。
根据每个主题所需的吞吐量和想要并行运行的消费者数量来选择分区数,并留出余量。之后可以增加分区,但这会改变键被映射到哪个分区。
复制
每个分区都有一个复制因子,通常为 3。其中一个副本是 Leader,负责处理读写;其他副本是复制 Leader 日志的 Follower。保持同步的 Follower 组成同步副本集合(ISR)。如果 Leader 所在的 Broker 发生故障,一个已同步的 Follower 会成为新的 Leader。
决定持久性的配置:
| 配置 | 位置 | 典型值 | 作用 |
|---|---|---|---|
replication.factor |
主题 | 3 | 每个分区的副本数 |
min.insync.replicas |
主题或 Broker | 2 | 一次写入生效前必须拥有该写入的副本数 |
acks |
生产者 | all |
等所有同步副本确认后才算成功 |
在复制因子为 3、min.insync.replicas=2 且 acks=all 的情况下,一次已确认的写入至少存在于两个 Broker 上,并且在一个 Broker 宕机时集群仍能继续接受写入。Kafka 文档 介绍了这些配置以及它们之间的相互作用。
生产者与键
生产者为每条记录选择一个分区。有键时,默认分区器会对键做哈希,所以同一个键的所有记录都会落到同一个分区并保持顺序。没有键时,记录会被分散以求均衡,彼此之间也就不再有任何顺序。
根据“哪个实体的事件必须保持顺序”来选择键:订单 ID、账户 ID。在图上把键写在生产者旁边;这是生产者最有影响的一项配置。避免使用只有少数几个巨大取值的键,因为无论有多少个分区,一个热点键都会压垮一个分区。
重试可能会产生重复记录。较新的 Kafka 版本默认启用幂等生产者,可以消除同一分区内由生产者重试造成的重复;但由你自己的应用重试造成的重复仍然可能出现,所以消费者也应该是幂等的。
消费者组与偏移量
消费者组 是为同一个目的分担工作的一组消费者。Kafka 会把每个分区分配给组内恰好一个消费者,所以三个分区最多只能让三个消费者忙起来,第四个只能闲着。当有消费者加入或离开时,组会进行再平衡,分区随之迁移。
每个组都会跟踪自己的位置,也就是每个分区已提交的 偏移量,Kafka 把它保存在一个内部主题里。多个服务正是这样独立读取同一个流的:fulfillment 和 analytics 各有自己的组和偏移量,互不影响。
在处理之后而不是之前提交偏移量,就能得到至少一次(at-least-once)的处理:崩溃之后,消费者会重新处理自上次提交以来的记录。让处理具备幂等性,重复处理就不会造成伤害。
保留与压缩
Kafka 会在记录被读取之后继续保存它们。基于时间或大小的保留策略会删除旧的日志段,比如 7 天之后。日志压缩则会永久保留每个键的最新一条记录,从而把主题变成当前状态的变更日志(changelog)。按主题来选择:事件通常使用保留策略,而“每个客户的最新地址”这类状态使用压缩。
一个容量估算示例
假设结账服务在峰值时每秒产生 3,000 个订单事件,每个约 1 KB,也就是 3 MB/s。fulfillment 服务中的一个消费者每秒能处理 400 个事件。峰值时至少需要 8 个并行的消费者,所以至少需要 8 个分区;选 12 个,为增长和故障消费者的分区留出余地。在复制因子为 3 的情况下,集群在所有 Broker 上合计每秒向磁盘写入 9 MB,按这个速率保留 7 天,最多会存储约 5.4 TB 的复制后数据。平均流量低于峰值,所以把这个数字当作磁盘规划的上限。这类数字应该写在架构图旁边,因为决定 Broker 数量和磁盘的是它们,而不是那个标着 Kafka 的方框。
KRaft:不再需要 ZooKeeper
旧的集群把元数据存放在 ZooKeeper 中。现在 Kafka 通过一个基于 Raft 的控制器仲裁(quorum)自行管理元数据,这就是 KRaft。2025 年 3 月发布的 Kafka 4.0 是第一个完全不依赖 ZooKeeper 运行的主要版本。在图上,即使小集群在同一批机器上同时运行这两种角色,也要把控制器仲裁和 Broker 分开画。
架构图
图表内容
- Checkout service
- Payments service
- Broker 1: P0 leader, P1 P2 followers
- Broker 2: P1 leader, P0 P2 followers
- Broker 3: P2 leader, P0 P1 followers
- KRaft controllers
- Consumer group: fulfillment
- Consumer 1
- Consumer 2
- Consumer 3
- Analytics consumer
- order 8812 to P0
- order 9001 to P2
- replicate
- same events, separate group
- metadata
这张图的读法如下:
- 左侧: 两个生产者以订单 ID 为键发送事件。订单 8812 被哈希到分区 0,订单 9001 被哈希到分区 2。
- 中间: 三个 Broker,每个都是一个分区的 Leader,同时是另外两个分区的 Follower。虚线表示复制;下方的 KRaft 控制器负责管理元数据和 Leader 选举。
- 右侧: fulfillment 组有三个消费者,每个分区一个。analytics 消费者属于另一个组,所以它用自己的偏移量读取每一个事件。
- Markdown 块 列出了这张图所依赖的主题配置。
Kafka 架构图上应该画什么
- 每个主题的分区数和键, 因为它们决定了顺序和并行度。
- 复制因子和
min.insync.replicas, 因为它们决定了持久性。 - 每个消费者组的名字, 以及组内的消费者。
- 每个主题的保留或压缩策略。
- 谁生产、谁消费, 要跨服务标明,让归属一目了然。
关于构建在 Kafka 之上的模式,请参阅 事件驱动架构图、从数据库可靠发布消息的 事务性发件箱模式,以及 Kafka 为分析系统供数的 数据管道架构图。
在 Archboard 中构建
- 使用 Google 或 GitHub 登录,并新建一张图。
- 添加 C4 模型节点包中的 容器 节点表示生产者,并在描述中写明它们的键。
- 添加数据节点包中的三个 Kafka 节点表示 Broker,在标签中注明每个 Broker 担任 Leader 和 Follower 的分区,再添加一个 容器 表示 KRaft 控制器。
- 添加基础节点包中的一个 框架 表示消费者组,每个消费者用一个 容器 表示,再为第二个组单独添加一个消费者。
- 用虚线画出复制关系,并添加一个基础节点包中的 Markdown 块,写明主题配置。
这个示例共用 12 个图表节点,在 Free 套餐的限制之内。
常见问题
Kafka 能保证消息顺序吗? 在分区内可以,跨分区不行。使用键,让必须保持顺序的记录落在同一个分区。
一个主题应该有多少个分区? 足以满足所需的吞吐量和想要并行的消费者数量,并留有增长空间。之后再改分区数,会改变键到分区的映射。
Broker 宕机时会发生什么?
对于它担任 Leader 的每个分区,另一个 Broker 上已同步的 Follower 会成为 Leader。在复制因子为 3、min.insync.replicas=2 的情况下,写入可以继续。
什么是消费者组? 一组在彼此之间分配主题分区的消费者。不同的组各自用自己的偏移量,独立读取同一个主题。
Kafka 还需要 ZooKeeper 吗? 不需要。Kafka 4.0 及之后的版本只运行在 KRaft 模式下,由控制器仲裁管理集群元数据。