Kafka — 把消息系统降维成只追加的日志文件
是什么
Kafka 是一个分布式消息系统:发消息的人(producer)只管追加,存消息的服务器(broker)只管把字节按顺序写进文件,收消息的人(consumer)自己记”上次读到哪”。日常类比:传统消息队列像图书馆——管理员要记每本书谁借了、谁还了、谁催过;Kafka 像一份不断追加的报纸——它只往后印,订阅者自己记上次读到第几版。
写一段最小的发消息代码:
echo "user-1::login" | kafka-console-producer --topic events --bootstrap-server localhost:9092这条消息被追加到 events topic 的某个 partition 末尾。下游想消费时随时来 fetch(topic, partition, offset),broker 不知道也不关心你是谁、读了几次。
这套”broker 当文件、consumer 持游标”的设计,就是今天每条 CDC pipeline、每个 event sourcing 应用、每次 click 流分发到 100 个下游的共同骨架。
为什么重要
不理解 Kafka,下面这些事都没法解释:
- 为什么 LinkedIn 一天数百 GB activity log 不会把 broker 堆爆——传统 ActiveMQ 单机几万 msg/s 就到顶
- 为什么 event sourcing / CDC / stream-table join 在 2014 后突然流行——Kafka 让”日志即真值”成本降到可负担
- 为什么 Pulsar / Redpanda / WarpStream 都自称”Kafka 协议兼容”——协议成了事实标准
- 为什么面试问”Kafka 怎么保证不丢”答案不在 NetDB 2011 论文,而在 KIP-1 / KIP-101——论文 §6 写着 “replication is future work”
核心要点
Kafka 的反直觉决定可以拆成 3 件事:
-
broker 重写成只追加的日志文件:传统 broker 把每条消息当可单独 ack/delete/TTL 的实体,要维护 per-message 状态。Kafka 反过来——broker 上的 partition 就是一组顺序 segment 文件(如
00000000000000000000.log),写入是 O(1) 的 append,永不修改、永不单删。类比:传统 broker 是有 1 万张索引卡的档案柜(每张卡记一条消息状态);Kafka 是一卷只能往末尾贴纸条的电报纸带。 -
offset 是 consumer 自己的状态:传统 broker 必须记”谁读到哪、谁 ack 了什么”,这是 broker 复杂度的根源。Kafka 让 consumer 自己持有 offset,broker 只暴露
fetch(topic, partition, offset, max_bytes)这一个接口。broker 因此对”谁读了什么”完全无知。 -
page cache + sendfile 做零拷贝扇出:Kafka 不维护应用层 buffer 缓存,把”缓存什么”完全交给 OS。读取时 broker 通过
sendfile(2)系统调用把内核 page cache 的字节直接 DMA 到 socket——不进用户态、不复制、不序列化。多个 consumer 订阅同一 topic 时只需要一份内存。
把 broker 从”有状态、复杂、慢”降级成”无状态、简单、快”——这是 Kafka 单 broker 比 ActiveMQ 高一个数量级吞吐的根本原因。
实践案例
案例 1:用户行为日志 pipeline
LinkedIn 的原始动机:每天数百 GB 的 page view / 搜索 / 广告点击事件,既要喂离线 Hadoop(推荐 / 反作弊),也要喂在线服务(实时报表 / feature store)。
# 生产端:埋点服务发消息producer.send("page_view", key=user_id, value={ "url": "/feed", "ts": 1716868800})
# 消费端 1:实时报表(5 秒读一次)consumer.subscribe(["page_view"], group_id="dashboard")
# 消费端 2:离线 ETL(每天凌晨从 offset=0 重读一周)consumer.subscribe(["page_view"], group_id="etl_hadoop")两个 consumer group 各自持有独立 offset,互不影响——broker 只存一份数据,被读 N 次。
案例 2:CDC(数据库变更同步)
把 MySQL/Postgres 的 binlog 写进 Kafka,下游想消费就消费:
# Debezium connector 把 MySQL binlog 转成 Kafka 事件source: mysqltopic: orders.public.userskey: user_id这是当前几乎所有”数据库 → 数仓 / Elasticsearch / Redis”同步链路的标配——一份 binlog,N 个下游各自重放。Kafka 的 retention(默认 7 天)让你”重放历史”成为日常操作而不是灾难恢复。
案例 3:扇出广播
一条订单事件被三个下游各自消费:
order_created (offset=42) ├─→ consumer group "alert" (实时告警,offset 跟尾部) ├─→ consumer group "etl" (每小时批量入仓,offset=30 落后) └─→ consumer group "audit" (审计回放,offset=0 从头读)broker 只存一份 partition 文件,三个 group 各自读。这是 Kafka 与传统 fan-out 的根本差别——传统 broker 给每个订阅者维护一个 cursor,扇出成本随 consumer 数线性增长;Kafka 的扇出成本恒定(只多了几次 sendfile)。
踩过的坑
-
不要把 Kafka 当 RPC:Kafka 是单向的 produce → fetch 模型。强行做
reply_to模式可以但延迟高、运维丑——请求-响应该用 gRPC / HTTP,不要硬塞进 Kafka。 -
不要把 Kafka 当数据库:
compacted topic(保留每个 key 最新值)看着像 KV store,但 read-by-key 是 O(n) 顺序扫描,替代不了 Redis / RocksDB。compaction 只为”留最新快照”,不为查询。 -
不要让 broker 跑业务逻辑:Kafka Streams / ksqlDB 在客户端 JVM 跑,broker 永远只做存储 + 路由。Pulsar 的 Function 反过来在 broker 上跑——这是哲学差异,跨派别套用会遇到性能黑洞。
-
不要按 timestamp 做精确查询:
offsetsForTimes(ts)是稀疏索引近似,且 producer 的时间戳由客户端写入,broker 不校验。多 producer 时钟漂移会让结果失真——KIP-32 没强约束。时间精度要求高 → 业务 ID 或 Spanner / TrueTime 派系统。
适用 vs 不适用场景
适用:
- 大批量事件流 / event sourcing / CDC pipeline(生态最厚,Connect / Streams / Schema Registry 完整)
- 需要多消费者各自独立游标 + 可重放(log 派结构性优势)
- 吞吐 > 10 MB/s 的场景(page cache + sendfile + batch 三件套真正发力)
- 跨数据中心异步复制(MirrorMaker / Confluent Replicator)
不适用:
- 请求-响应 RPC(用 gRPC / HTTP)
- 高频小消息低延迟(< 1ms P99——用 Redis Streams / NATS)
- 需要 per-message TTL / 严格 routing key / job queue 语义(用 RabbitMQ / SQS——这是 queue 派的护城河)
- 单机简单 pub-sub(杀鸡用牛刀,用 Redis pub-sub 即可)
历史小故事(可跳过)
- 2010 年:LinkedIn 内部 activity log 每天数百 GB,ActiveMQ 扛不住吞吐,Scribe 不能 replay 也无法实时——既存方案两条路都堵死。
- 2011 年:Jay Kreps、Neha Narkhede、Jun Rao 三人在 NetDB workshop 发表 6 页论文,把 broker 写成 file。论文 §6 老老实实写”replication is future work”。
- 2013 年:Kafka 0.8 实现 ISR + high watermark——这才是今天 90% 文档讨论的核心,但论文里没有。
- 2014 年:三人离开 LinkedIn 创立 Confluent,把 Kafka 推成商业基础设施。
- 2019 年:KIP-500 启动用 KRaft(内嵌 Raft)取代 ZooKeeper;2024 年 Kafka 4.0 默认 KRaft,ZK 模式 deprecated。
学到什么
- 把”复杂的中间件”重写成”笨的文件追加器”是降维打击——所有 per-message 状态消失后,吞吐自然涨一个数量级
- 协议不变 + 实现迭代——论文 6 页定哲学,KIP 系列(1 / 32 / 98 / 101 / 405 / 500)补 90% 实现,但 produce/fetch 接口 12 年没变
- 生态护城河 > 协议优势——Pulsar / Redpanda 在协议或性能上都更优,但 Kafka 的 Connect / Debezium / Flink-Kafka 生态厚到无法替代
- 诚实地承认 at-least-once——Kafka 自身只保证 at-least-once,exactly-once 必须靠下游幂等。不夸大反而比某些后续宣传更可信
延伸阅读
- 工程长文:The Log: What every software engineer should know — Jay Kreps 2013(NetDB 2011 论文哲学的展开版,必读)
- 论文 PDF:Kafka NetDB 2011 — 6 页
- KIP 必读集:KIP-1(replication)/ KIP-32(timestamp)/ KIP-98(exactly-once)/ KIP-101(leader epoch)/ KIP-500(KRaft)/ KIP-405(tiered storage)
- 视频:Confluent — Apache Kafka 101(10 集,从 producer 到 streams)
- pulsar —— 计算 / 存储分离的”次世代 Kafka”
- bigtable —— 同期 Google 论文,另一种 append-only 哲学(SSTable)
关联
- bigtable —— SSTable 的 append-only + compaction 思想与 Kafka segment 一脉相承
- gfs —— Kafka 的 page cache + sendfile 借用 GFS 同款”信任 OS、信任顺序写”假设
- chubby —— Kafka 早期用 ZooKeeper 做协调,ZK 是 Chubby 的开源版
- raft —— KRaft(KIP-500)用 Raft 取代 ZooKeeper,把控制面合并进 broker
- paxos —— Raft 的前辈;理解 Paxos 才能看懂为什么 Kafka 选了 Raft 而非 Paxos
- spanner —— Spanner 的 TrueTime 哲学与 Kafka “假装时钟问题不存在”恰好相反
- aurora —— Aurora 也是”日志即数据库”派,把 redo log 当真值——与 Kafka “日志即数据骨干”同源