Kafka 消息是怎么存储和消费的?
Kafka 中,一条消息从哪里写入、存在哪、由谁消费,以及消费到哪里了,分别由不同的结构负责。这里把这些概念整理在一起,并用一个简单的生产与消费示例展示它们的关系。
本文按指定日期归档;文中的示例与截图为后续补充,不代表截图拍摄于 2022 年。具体配置和行为以实际 Kafka 版本及客户端为准。
整体结构
生产者 Producer |
| 概念 | 作用 |
|---|---|
| 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 和 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 + 分区号 |

上图是某次测试环境的快照:__consumer_offsets 有 50 个分区、副本因子为 1,清理策略显示为 COMPACT。这些数字描述的是这套环境,不是所有 Kafka 集群都固定如此;单副本也意味着该内部主题没有副本冗余。
表格中的 First Offset 和 Next Offset 描述的是 __consumer_offsets 自己各个分区的日志位置,不是业务 Topic 的消费进度。界面的 Message Count 也不能直接理解成消费者组数量;对于 compact 主题,位置跨度与实际保留记录条数亦不一定相等。
从消费者组界面看状态

截图中多个组的成员数为 0、状态为 EMPTY:表示截图时没有活跃成员,并不等于它们之前的提交位点已被删除。Consumer Lag 通常表示业务分区日志末尾位置与已提交位置之间的差值,列表可能是分区汇总;它不是精确的处理耗时,压缩、事务和工具计算口径也可能影响解释。N/A 表示该界面当时无法给出数值,不能简单当成 0。
生产和消费示例
示例使用 Go 的 kafka-go,生产端写入 Key 和 Value:
writer.WriteMessages(ctx, kafka.Message{ |
消费端获取消息,处理成功后提交位点:
message, err := reader.FetchMessage(ctx) |
本地测试代码中,同一次循环实际调用了两次 WriteMessages;配置为 RequiredAcks: kafka.RequireNone,并未设置 Hash 分区器,与部分注释不一致。以上只是结构展示,生产环境还需按可靠性要求配置确认、副本、错误处理及幂等。




