Kafka 为什么快:顺序写、零拷贝、批量拼车——Java 工兵连第 18 篇
「Java 工兵连」第 18 篇。问「Kafka 为什么快」,多数答案止步于「分布式、内存快」——都不在点上。Kafka 的快是把磁盘和网卡的数据通路重新定制了一遍:它大量用磁盘、却比许多纯内存系统还快。这篇拆三板斧,再补上比“快”更值钱的三条纪律。
一、先建直觉:中央收发室的传送带
- Topic 是一条传送带,太长搬不动,切成几段(Partition)各自独立运转——这就是水平扩展的单位;
- 顺序写 = 图书馆还书:按归还顺序把书码在推车末尾,而不是每本插回书架正确位置——码推车比插书架快百倍;
- 页缓存 = 最近还的书堆在台面上,借阅热门恰好是最近还的——直接从台面拿,不用进书库;
- 零拷贝 = 快递中转站不过分拣台:包裹从进货卡车直接滑上传出卡车,不拆箱上架再取下;
- 批量 = 拼车:一条消息发一次太浪费,攒一车再走。
二、三板斧逐一拆
① 顺序追加写:磁盘「变快」的根基
每个 partition 在磁盘上就是一组只追加的日志段文件(segment):新消息永远写到最后一个文件的末尾,「删除」旧数据是按时间/大小整个丢弃旧段——不更新、不插入、不随机寻址。机械盘顺序写对随机写有数量级的差距(SSD 也差数倍),这一招直接把磁盘变成「大容量廉价内存」。眼熟吗?MySQL 的 redo、Redis 的 AOF 全是同一心法(第 12/15 篇的暗线第三次报到):「不覆盖历史,只追加新版本」——工兵连第一季 MVCC 篇说的那个思想,在存储世界处处开花。
② 页缓存代劳:Kafka 聪明的「不作为」
Kafka 不自己在 JVM 里缓存消息——写进 OS 页缓存就应答,刷盘交给操作系统;消费者又大多「追尾读」(读最近写入的),恰好全在页缓存里。三个白捡的好处:不吃 GC(大堆缓存消息是 GC 灾难)、进程重启缓存还在(OS 没重启)、机器断电前数据已由 OS 兜底。运维推论:Kafka 的堆不用大,内存尽量留给页缓存——堆给 6GB、机器 64GB 的配置很正常。
③ 零拷贝 + 批量:通路砍短,货车装满
传统「读文件发网络」要四次拷贝:磁盘→内核缓冲→用户空间→Socket 缓冲→网卡。Kafka 用 sendfile(Java 层 FileChannel.transferTo):磁盘→页缓存→直接 DMA 到网卡,用户态全程不参与——拷贝次数减半、上下文切换消失。再叠加攒批发送(linger.ms 等几毫秒凑一车)和整链路一次压缩(生产者压 lz4/zstd,broker 原样存,消费者解压),网络利用率直接翻倍。注意 linger.ms 是经典的吞吐-延迟权衡:等 5ms 凑批 vs 到一条发一条——没有对错,看业务。
三、30 秒架构速览(三板斧的舞台)
- Producer 按 key 哈希定分区——同一个 key 永远进同一分区,这是「有序」的前提;
- Consumer Group 分区分摊——一组消费者瓜分所有分区,并行度上限 = 分区数(分区 3 个、消费者 5 个,就有 2 个闲着);
- 有序只在分区内——跨分区、跨 topic 无顺序保证。要全局有序?单分区(吞吐换顺序)或按业务把「需要有序的」用同 key 圈进同分区。
四、快之外的三条纪律(老兵最看重)
纪律一:消息不丢三板斧。 三段链路各堵一处:Producer 端 acks=all + 重试(等所有 ISR 副本确认);Broker 端 replication.factor≥3 且 min.insync.replicas≥2(至少两份同步才算落笔);Consumer 端先处理、后手动提交 offset(自动提交 = 处理一半就提交 = 崩溃丢消息)。三段全设,才有资格谈「不丢」。
纪律二:顺序的边界要认账。 就算同 key 同分区,Producer 重试(max.in.flight>1 时先发的后成功)也可能乱序。要严格顺序:开幂等生产者(enable.idempotence=true,broker 按 序列号去重并保序)——顺带把「重试导致的重复」也消了。
纪律三:消费幂等是必修课。 Kafka 的投递语义默认 at-least-once:rebalance 时没提交的 offset 会重新投递——重复消费不是故障,是架构属性(跟第 15 篇「主从异步丢尾巴」一个道理:先认清语义,再谈对策)。消费端的幂等怎么做?答案你在第 7 篇学过了:唯一约束、状态机、去重表——锁和幂等兜底的思路,跨中间件通用。
老兵点评
- Kafka 的快给了一个反直觉的启示:优化 IO 路径,比升级硬件和堆缓存有效。顺序写把机械盘用出 SSD 的感觉、页缓存让「不用缓存」成为最好的缓存、零拷贝把 CPU 从搬运工岗位上撤下来——三板斧没有一斧是「加内存」。设计系统时先画数据通路,再谈性能参数。
- 选型时先想清「我要什么语义」:丢一条能不能忍(日志收集可以,交易不行)?重复能不能忍(统计可以,扣款不行——除非幂等)?顺序要不要(行为流要,埋点不用)?语义定了,Kafka 十几个参数里九成自动确定。这又是那把尺子:「丢了多少能接受」。
- 面试段位差:初级背「Kafka 快因为顺序写和零拷贝」;中级讲页缓存与 sendfile 的数据通路;高级讲 ISR 与 acks 的落盘语义、幂等生产者的序号去重、以及 at-least-once 下消费幂等的设计——聊到这层是在聊消息语义,不是聊品牌。
带走三句话
- 三板斧口诀:顺序写、靠缓存、零拷贝、拼个批——全打在数据通路(IO)上,不在 CPU。
- 顺序边界:有序只在分区里——同 key 同分区 + 幂等生产者,才有「严格有序不重复」。
- 投递语义:at-least-once 是常态,消费幂等是必修;不丢三板斧 = acks=all + 副本≥3/ISR≥2 + 先处理后提交。
本篇是「Java 工兵连」第 18 篇。→ 上一篇:第 17 篇 · 缓存一致性:写库删缓存,删优于更 | 继续阅读:第 19 篇 · 分库分表与分布式 ID | 浏览整个专栏 →。
❦ 终 ❦