导读:为什么 Kafka 的核心是一根日志
Kafka 不是一个消息队列。更准确的说法是:Kafka 是一个分布式追加日志(append-only log)系统,消息队列的语义只是日志操作的一种投影。
本文只抓一个问题:追加日志这个数据结构,如何成为 Kafka 全部机制的起点。
追加日志:一种最朴素的数据结构
追加日志的定义可以压成三条规则:
- 写入只能追加到尾部(append)。
- 每条记录获得一个单调递增的序号(offset)。
- 已写入的记录不可修改(immutable)。
用 ASCII 表示一根日志的状态:
1 | |
这个结构有三个直接推论:
- 写入是顺序 I/O。磁盘的顺序写入速度接近内存随机写入速度,这是 Kafka 高吞吐的物理基础。
- 读取是按 offset 寻址。给定一个 offset,定位到对应记录的时间复杂度是 O(log n)(通过稀疏索引二分查找)。
- 记录不可变意味着不需要加锁。多个读者可以各自维护自己的 offset,互不干扰。
消息队列语义是日志操作的投影
传统消息队列有两种基本模型:点对点(point-to-point)和发布-订阅(pub/sub)。这两种模型在日志上的映射关系如下:
1 | |
点对点模型要求一条消息只被一个消费者处理。在日志上,这等价于一组消费者共享一个 offset 游标——任意时刻只有一个消费者持有某个 offset 的读取权。Kafka 用 consumer group 内的 partition 分配实现这个语义。
发布-订阅模型要求一条消息被所有订阅者处理。在日志上,这等价于每个订阅者维护自己独立的 offset 游标——同一条记录被多个游标扫过。Kafka 用不同 consumer group 实现这个语义。
关键区别在于:传统消息队列在 acknowledge 之后通常删除消息,而 Kafka 的日志保留消息,删除由独立的保留策略(retention policy)控制。消息不因被消费而消失,这使得重放(replay)成为一等操作。
"日志"不是 Kafka 的发明
追加日志作为系统设计的核心抽象,远早于 Kafka 出现。Jay Kreps 在 2013 年的文章 The Log: What every software engineer should know about real-time data’s unifying abstraction 中系统阐述了这个观点:日志是分布式系统中数据一致性的基础构件。
以下工程系统都以追加日志为核心数据结构:
1 | |
这些系统的共同模式是:状态可以从日志中重建。数据库的当前数据页状态等于 checkpoint 加上 checkpoint 之后所有 WAL 记录的回放;Kafka consumer 的当前消费位置等于初始 offset 加上所有 commit 操作的累积。
实验:用 offset 观察日志的可重复读取
以下实验用 kafka-console-producer 和 kafka-console-consumer 演示日志最核心的性质——同一个 offset 的数据可以反复读取。
启动一个单节点 Kafka(使用 KRaft 模式,无需 ZooKeeper):
1 | |
创建 topic 并写入数据:
1 | |
第一次从头读取:
1 | |
输出:
1 | |
第二次从头读取——数据完全相同:
1 | |
输出不变:
1 | |
这个实验验证了两个事实:
- 消费不删除数据。两次
--from-beginning读到的内容一致。 - offset 是稳定的。alpha 永远在 offset 0,epsilon 永远在 offset 4。
在传统消息队列中,第一次 receive + acknowledge 之后,第二次 receive 将拿不到相同的消息。Kafka 的日志语义从根本上改变了这个行为。
模式提炼
追加日志模式(Append-Only Log Pattern)的核心特征:
- 写入路径只有 append,没有 update 和 delete。这使得写入路径极度简单,不需要处理并发修改冲突。
- 每条记录有全局唯一的位置标识(offset、LSN、binlog position)。位置标识是单调递增的,因此可以用来定义 happens-before 关系。
- 当前状态可以从日志中重建。这意味着日志是 source of truth,而当前状态是日志的派生视图。
这个模式的工程价值在于把"写"和"读"解耦:写入者只关心追加,读取者各自维护自己的游标。系统的复杂度从"如何协调多个读写者的并发访问"降低为"如何管理多个游标"。
工程迁移表
| 维度 | Kafka commit log | 数据库 WAL | RocketMQ CommitLog |
|---|---|---|---|
| 写入方式 | 追加到 partition 尾部 | 追加到 WAL 文件尾部 | 追加到全局 CommitLog 尾部 |
| 位置标识 | offset(64-bit 整数) | LSN(Log Sequence Number) | commitlog offset(物理偏移量) |
| 是否可变 | 不可变 | 不可变 | 不可变 |
| 删除策略 | 基于时间或大小的 retention | checkpoint 之后截断 | 基于时间的定期删除 |
| 多读者支持 | 每个 consumer group 独立 offset | 从库各自维护回放位置 | 每个 consumer group 独立 offset |
| 读取方式 | 按 offset 寻址 | 按 LSN 顺序回放 | 通过 ConsumeQueue 索引间接寻址 |
常见误解
误解一:“Kafka 就是一个消息队列。”
Kafka 的 API 和生态确实支持消息队列的使用模式,但其内部数据结构是追加日志而非队列。队列的语义是 FIFO + dequeue(取出后删除),日志的语义是 append + seek(按位置读取,不删除)。这个区别决定了 Kafka 支持消息重放、多 consumer group 独立消费、以及基于 offset 的精确定位——这些在传统队列模型中要么不支持,要么需要额外机制。
误解二:“消费过的消息会被删除。”
Kafka 的消息删除与消费行为无关。消息的生命周期由 retention policy 控制:log.retention.hours 控制基于时间的保留,log.retention.bytes 控制基于大小的保留。一条消息即使被所有 consumer group 消费过,只要没有超过保留期限,仍然存在于日志中,可以被新的 consumer group 从头读取。
误解三:“Kafka 的高吞吐来自内存缓存。”
Kafka broker 不维护应用层的消息缓存。高吞吐的来源是三个因素的叠加:顺序磁盘 I/O(追加写入)、操作系统 page cache(由 OS 管理,对 Kafka 透明)、以及零拷贝传输(sendfile 系统调用,避免数据在内核态和用户态之间复制)。这些机制将在第 02 篇详细展开。
练习
-
使用 kafka-console-consumer 的
--from-beginning和--offset参数,分别从 offset 0 和 offset 3 开始读取上面实验中的log-demotopic。观察两次读取的输出差异,理解 offset 寻址的含义。 -
写入更多消息到
log-demo,然后观察 Kafka 数据目录下log-demo-0/中的文件。找到.log文件和.index文件,思考它们与追加日志模型的对应关系(详细解读在第 02 篇)。 -
查阅 MySQL binlog 的文档,找到 binlog 中与 Kafka offset 对应的位置标识(提示:binlog position 或 GTID)。思考两者在主从复制 / 消费位点管理中的相似角色。
系列导航
参考资料
- Jay Kreps. The Log: What every software engineer should know about real-time data’s unifying abstraction. LinkedIn Engineering, 2013. https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying
- Apache Kafka Documentation — Design: Persistence. https://kafka.apache.org/documentation/#design_persistence
- Jay Kreps. I Heart Logs: Event Data, Stream Processing, and Data Integration. O’Reilly, 2014.
- Apache Kafka Documentation — Quick Start. https://kafka.apache.org/documentation/#quickstart
- Martin Kleppmann. Designing Data-Intensive Applications. O’Reilly, 2017. Chapter 11: Stream Processing.
