Kafka 凭什么吞吐百万级?—— 架构与存储
属于 S3 Kafka 深入 · 第一篇 下一篇:生产消费语义
消息队列那么多,Kafka 最响亮的标签是"吞吐百万级 TPS"。这不是营销话术,而是两个核心设计的结果:分区并行和磁盘顺序读写。这一篇把 Kafka 的架构和存储机制拆开,用尽量形象的比喻把抽象概念讲透。
架构:一条高速公路上怎么分工
先把 Kafka 的骨架搭起来。想象 Kafka 是一条高速公路:
- Broker 是服务区/收费站,一个 Kafka 集群有多个 Broker,数据就存在它们上面。
- Topic 是"路线主题",比如"订单流"、"日志流"——业务上的分类。
- Partition(分区) 是一条 Topic 拆出来的多条车道。这是关键:一个分区只能被一个消费者实例消费,所以分区数就是并行度的上限。想要更快,就多开几条车道。
- Replica(副本) 是每条车道的"备份车道",一主多从,防止某条车道塌了没路走。
为什么要拆分区?两个原因:一是并行(多车道同时跑车),二是扩展(数据分散到多台 Broker,突破单机吞吐和容量上限)。
分区里谁是头儿:Leader 和 Follower
每个分区有一个 Leader 副本和若干 Follower 副本。Leader 负责读写,Follower 不停从 Leader 同步数据——用高速公路比喻,Leader 是"正在通行的主车道",Follower 是"跟着复制的备份车道",平时不接客,主车道塌了才顶上。
这里有两个必须理解的概念,面试常被追问,也是"不丢数据"的基石:
- LEO(Log End Offset):每个副本日志的"末尾位置"——这条副本已经写到了哪里。
- HW(High Watermark,高水位线):想象一个水库的水位线,水位线以下的数据才对消费者可见。HW 的取值是"所有 ISR 副本中 LEO 最小的那个"——只有所有同步副本都写到的位置,才算"安全",消费者才能读到。
这个设计回答了一个问题:为什么消费者读不到刚刚写入、但还没同步到从副本的数据? 因为 HW 卡在"最慢的那个同步副本"的 LEO 上,保证消费者读到的数据一定是"多数副本都确认过"的,不会因为主副本挂了而读到"后来丢失"的数据。
ISR:跟得上节奏的副本才算数
ISR(In-Sync Replicas)是"同步中的副本集合"。用一个教室比喻:老师(Leader)在讲台上讲课,跟得上进度的学生(Follower)组成 ISR;某个学生(Follower)开小差掉队了(同步延迟超过 replica.lag.time.max.ms,默认 10 秒),就被请出 ISR,等它赶上来再回来。
ISR 的意义在于 Leader 选举:Leader 挂了,只从 ISR 里选新 Leader——因为 ISR 里的副本数据是跟得上的,选它当新 Leader 才不会丢数据。如果所有副本都掉队、ISR 空了怎么办?这取决于 unclean.leader.election.enable 配置:默认 false,宁可不选、也不选一个落后的副本当 Leader(因为选了必然丢数据);设为 true 则优先可用性,允许选落后副本。
Controller:集群的"总指挥"
集群里还有一个特殊角色 Controller——它不是独立进程,而是某一个 Broker 兼任。Controller 负责全局协调:分区 Leader 选举、副本分配、Broker 上下线管理。它通过 ZooKeeper(老版本)或 KRaft 共识协议(新版本,去 ZooKeeper)来维护集群元数据。你可以把它理解成"高速公路的调度中心",哪条车道塌了、派谁顶上,都由它拍板。
存储:为什么写磁盘还这么快
Kafka 最反直觉的地方是:它把数据写磁盘,却比很多写内存的中间件还快。秘诀有三根支柱。
第一根:顺序写(append-only)。 Kafka 的消息是追加写到分区日志末尾的,从不在中间插入或修改。这像写日记——永远从最后一页接着往下写。而磁盘的顺序写速度接近内存,远快于随机写(随机写要频繁寻道,像在书里乱翻页)。这就是"写磁盘还快"的根本原因。
第二根:零拷贝(zero-copy)。 这是 Kafka 读快的关键。先看传统方式发一个文件要几次拷贝:
传统(read + write):
磁盘 → 内核缓冲区 → 用户空间 → socket 缓冲区 → 网卡
(4 次拷贝,其中 2 次 CPU 拷贝 + 2 次 DMA 拷贝,4 次上下文切换)
Kafka(sendfile):
磁盘 → 内核缓冲区 → 网卡
(2 次拷贝,全程 DMA,2 次上下文切换)用快递比喻:传统方式是"快递先送到中转站(内核)→ 再送到你家(用户空间)→ 你再送去另一个中转站(socket)→ 最后才发出";零拷贝是"快递从仓库直达目的地,不经过你家"。Kafka 用 sendfile 系统调用,让数据从磁盘直接到网卡,绕过了用户空间,省掉一半拷贝和上下文切换。
第三根:页缓存(Page Cache)。 Kafka 读写都不直接落盘,而是走操作系统的页缓存——数据先写进内存,由操作系统异步刷盘,读的时候优先命中内存。相当于"先在脑子里记着,回头再誊到本子上"。
磁盘上,每个分区的日志切成多个 segment 文件,每个 segment 配一个稀疏索引——不是每条消息都建索引,而是每隔若干字节记一条"offset → 位置"的映射,查找时先二分定位到附近,再顺序扫。这样索引体积小、查找又快。
acks:可靠性和吞吐的旋钮
生产者发消息,有三个可靠性档位,由 acks 参数控制:
| acks | 含义 | 可靠性 | 吞吐 |
|---|---|---|---|
| 0 | 发出去就不管,不等确认 | 可能丢 | 最高 |
| 1 | Leader 写入即确认 | Leader 挂了可能丢 | 高 |
| -1(all) | 所有 ISR 副本都确认 | 最可靠 | 最低 |
注意一个经典陷阱:acks=-1 并不绝对不丢。如果 ISR 只剩 Leader 一个(其余副本都掉队被踢了),-1 就退化成 1 了——Leader 确认就返回,它一挂还是丢。所以真正要"不丢",得配合 min.insync.replicas >= 2,保证"至少两个副本同步才算写成功"。
串起来
Kafka 的高吞吐是一套组合拳:分区提供并行度;Leader/Follower + ISR 在副本一致性和可用性之间取平衡;HW 保证消费者只读到"多数副本确认过"的数据;顺序写 + 零拷贝 + 页缓存让磁盘读写接近内存速度;acks 让使用者在可靠性和吞吐之间自己旋钮。理解了这套"车道 + 备份 + 水位线"的机制,Kafka 的骨架就立起来了。
下一篇讲生产消费语义:消息发出去,怎么保证不丢、不重?消费组里的"重新分座位"(rebalance)又是怎么回事?