Apache Kafka 官方 Logo

Kafka 中,一条消息从哪里写入、存在哪、由谁消费,以及消费到哪里了,分别由不同的结构负责。这里把这些概念整理在一起,并用一个简单的生产与消费示例展示它们的关系。

本文按指定日期归档;文中的示例与截图为后续补充,不代表截图拍摄于 2022 年。具体配置和行为以实际 Kafka 版本及客户端为准。

整体结构

生产者 Producer
│ 写入消息
▼
主题 Topic:orders
├── 分区 Partition 0: [消息 0] [消息 1] [消息 2] ...
└── 分区 Partition 1: [消息 0] [消息 1] [消息 2] ...
│
▼
消费者组 Consumer Group
├── Consumer A
└── Consumer B
│ 提交消费进度
▼
内部主题 __consumer_offsets
概念 作用
Broker 一台 Kafka 服务节点;多个 Broker 可以组成集群。
Topic 消息的逻辑分类,例如订单事件主题。
Partition Topic 内的有序日志;一个 Topic 可以有多个分区,每个分区独立编号。
Record 写入分区的一条消息,包含 Key、Value 等字段。
Offset 消息在某个分区中的位置;不同分区的 offset 相互独立。
Consumer Group 一组共同消费的消费者,组内按照分区分配工作。
Replica 分区的副本,用于提高可用性;生产和消费通常与该分区的 leader 交互。

Topic、Partition 与消息

Topic 是逻辑上的分类,消息实际保存在各个分区中。Kafka 保证单个分区内的顺序,不保证跨分区的全局顺序。 一个分区的 offset 是它自己的位置编号,不能与另一个分区的 offset 直接比较。

一条消息可以理解为下面的结构:

Topic: orders       Partition: 0       Offset: 42
Key: order-1001
Value: {"status":"created"}
Timestamp: 消息时间戳
Headers: 可选的附加信息

Key 和 Value 都是字节数据,Kafka 不要求业务内容必须是 JSON;具体编码和反序列化由生产者与消费者约定。是否按 Key 选择分区取决于客户端的分区策略,不能只因为消息带有 Key 就断言一定进入固定分区。

每个分区的日志又会拆成多个 segment(日志段),便于管理、清理和查找。消息保留时间和空间由主题或 Broker 配置决定,消费完成并不意味着消息立即被删除。压缩(compact)主题会按 Key 清理较旧记录,但清理不是实时发生的,也可能留有 offset 空洞。

消费者组与消费进度

同一个消费者组中,一个分区在同一时刻通常只分配给组内一个消费者;不同组可以各自消费同一个 Topic,并分别记录进度。消费者加入或离开时,分区可能重新分配,这个过程叫 rebalance。

消息 offset 和已提交的消费 offset 不是一回事。 前者是消息的位置;后者是消费者组保存的进度,通常指下一条准备读取的位置。例如处理完 offset 42 后,提交的位置是 43。读取消息后还未提交时,重启或重新分配可能再次读取该消息,因此业务处理应考虑重复消费。

__consumer_offsets 是什么?

它是 Kafka 的内部 Topic,主要用于持久化消费者组提交的位点和消费组相关元数据,不是存放订单等业务消息的地方。消费者通过组协调器提交位点,协调器把记录写入该内部 Topic;协调器也会维护缓存以便查询。这个 Topic 使用日志压缩,保留每个键所需的最新状态;过期、删除和清理仍受相关规则及配置影响。

可以用下面的逻辑关系理解位点记录(这是示意,不是 Kafka 实际二进制编码):

键:消费者组 ID + 业务 Topic + 分区号
值:已提交的下一条 offset + 相关元数据

例如:group-a + orders + partition-0 → 43

Kafka UI 中的 __consumer_offsets 内部主题

上图是某次测试环境的快照:__consumer_offsets 有 50 个分区、副本因子为 1,清理策略显示为 COMPACT。这些数字描述的是这套环境,不是所有 Kafka 集群都固定如此;单副本也意味着该内部主题没有副本冗余。

表格中的 First Offset 和 Next Offset 描述的是 __consumer_offsets 自己各个分区的日志位置,不是业务 Topic 的消费进度。界面的 Message Count 也不能直接理解成消费者组数量;对于 compact 主题,位置跨度与实际保留记录条数亦不一定相等。

从消费者组界面看状态

Kafka UI 中的消费者组列表

截图中多个组的成员数为 0、状态为 EMPTY:表示截图时没有活跃成员,并不等于它们之前的提交位点已被删除。Consumer Lag 通常表示业务分区日志末尾位置与已提交位置之间的差值,列表可能是分区汇总;它不是精确的处理耗时,压缩、事务和工具计算口径也可能影响解释。N/A 表示该界面当时无法给出数值,不能简单当成 0。

生产和消费示例

示例使用 Go 的 kafka-go,生产端写入 Key 和 Value:

writer.WriteMessages(ctx, kafka.Message{
Key: []byte(key),
Value: []byte(value),
})

消费端获取消息,处理成功后提交位点:

message, err := reader.FetchMessage(ctx)
if err != nil { return }
// 处理 message.Key 和 message.Value
if err := reader.CommitMessages(ctx, message); err != nil { return }

本地测试代码中,同一次循环实际调用了两次 WriteMessages;配置为 RequiredAcks: kafka.RequireNone,并未设置 Hash 分区器,与部分注释不一致。以上只是结构展示,生产环境还需按可靠性要求配置确认、副本、错误处理及幂等。

参考资料