Kafka 为什么这么快?——顺序写+页缓存+零拷贝+批量,四板斧拆解
引言
面试高频题,也是最难答好的一题:
"Kafka 的消息都持久化到磁盘了,为什么还能单机扛百万级消息?Redis 数据放内存不是更快吗?"
大多数人的回答是背一句"Kafka 顺序写 + 零拷贝",再往下追问就露馅了:
- 为什么顺序写就快?内存 25GB/s,磁盘顺序写才 200MB/s,差 100 倍呢
- PageCache 是什么?Kafka 为什么自己不搞个缓存?
- 零拷贝到底省了几次拷贝?省的是谁的钱?
- 批量和压缩跟"快"有什么关系?
先给一组我们生产环境的真实数据(3 副本、NVMe 盘、2KB 消息、acks=all):单 Broker 稳定跑 80 万 msg/s,CPU 使用率不到 40%,磁盘 util 长期低于 30%。既没吃满 CPU,也没吃满磁盘——因为它在四个环节上同时"不浪费"。
这篇文章按一条消息的生命周期拆四板斧:顺序写(消除磁盘随机 IO)→ PageCache(读写走内存)→ 零拷贝 sendfile(消除无谓的数据拷贝)→ 批量+压缩(消除网络往返)。每板斧都给原理图 + 性能数据对比 + 关键参数。先放结论总表,后文逐个展开:
| 环节 | 普通做法的瓶颈 | Kafka 的做法 | 收益量级 |
|---|---|---|---|
| 磁盘写入 | 随机写 4K,HDD 约 100 IOPS | 顺序追加写 | 快 300 倍以上 |
| 数据读写 | 每次都碰磁盘 | PageCache,热数据全内存 | 磁盘 util <30% |
| 发送到网卡 | 4 次拷贝 + 4 次上下文切换 | sendfile 零拷贝 | CPU 拷贝开销降 ~65% |
| 网络传输 | 一条消息一次 RPC | 批量 + 压缩 | RPC 次数除以几百,流量省 60%+ |
一句话破题:Kafka 不是"磁盘写得快",而是把一次消息收发的每一步都设计成"只做必要的事"——磁盘只做顺序 IO、内存只做读写中转、CPU 不搬重复数据、网络不发重复报文。
一、先破除一个误解:磁盘不慢,"随机访问"才慢
讲四板斧之前,必须先建立正确的硬件直觉,否则后面的机制都成了玄学。
1.1 一组真实数据
Apple M2 上压测本地缓存的套路(参见《本地缓存三巨头》)在这里不适用——我们要看的是存储介质本身的速度。同一台 NVMe 服务器(fio 实测):
| 介质 | 顺序读/写 | 4K 随机读/写 | 差距 |
|---|---|---|---|
| HDD 7200 转 | 180 MB/s / 150 MB/s | 0.8 MB/s / 0.4 MB/s | 顺序是随机的 300+ 倍 |
| SATA SSD | 520 MB/s / 460 MB/s | 210 MB/s / 130 MB/s(5 万 IOPS) | 顺序比随机快 3 倍多 |
| NVMe SSD | 3.2 GB/s / 2.8 GB/s | 1.6 GB/s / 1.2 GB/s(30 万 IOPS) | 顺序仍比随机快 2 倍+ |
| DDR4 内存 | ~25 GB/s | — | 顺序写磁盘 ≈ 内存 1/10 |
两个反直觉的结论:
- 磁盘顺序写的速度相当能打——HDD 150MB/s,NVMe 2.8GB/s,已经达到内存的 1/10~1/8。"磁盘慢"是"磁盘随机访问慢"。
- 随机写才是灾难——HDD 上 4K 随机写折算下来不到 0.5MB/s,跟顺序写差两个数量级以上。
1.2 机械盘为什么随机慢
一次 4K 随机写 HDD 的开销分解(7200 转):
┌──────────────────────────────┐
│ 寻道(磁头移到目标磁道) │ ~8 ms ← 机械运动,占大头
├──────────────────────────────┤
│ 旋转延迟(等盘片转到目标扇区) │ ~4.2 ms ← 7200rpm 半圈
├──────────────────────────────┤
│ 写入 4K 数据 │ ~0.03 ms
└──────────────────────────────┘
合计 ≈ 12 ms / 次 → 只有 ~83 IOPS
顺序写的开销分解:
┌──────────────────────────────┐
│ 磁头已就位,沿磁道一直写 │ 一次寻道后,写满一圈又一圈
└──────────────────────────────┘
速度只受磁道密度限制 → ~150 MB/s
机械盘的物理结构决定了:寻道和旋转是纯机械动作,与"写多少数据"无关。随机写把时间全浪费在"移动磁头"上,顺序写把机械动作摊薄到海量数据里。SSD 没有机械部件,但仍受 FTL 映射、擦写块(write amplification)影响——所以顺序写依然明显优于随机写。
1.3 Kafka 的立场
面对这张表,Kafka 的设计立场极其鲜明:
我全都要顺序,随机访问的那部分用最小的代价补救。
- 写:永远追加到文件尾部(顺序写)
- 读:用稀疏索引把"随机定位"降级为"二分查找 + 一小段顺序扫描"
接下来的三板斧,都是在"顺序"这个地基上继续盖楼。
二、第一板斧:顺序写
2.1 Kafka 的存储结构:partition 就是一个日志文件
Kafka 把 Topic 拆成多个 Partition,每个 Partition 在磁盘上就是一个只追加的日志文件组:
/data/kafka-logs/order-pay-0/ ← order-pay 的 0 号分区
├── 00000000000000000000.log ← 消息主体,纯追加
├── 00000000000000000000.index ← 偏移量稀疏索引
├── 00000000000000000000.timeindex ← 时间戳稀疏索引
├── 00000000000005123456.log ← 1GB 滚动出下一个 segment
├── 00000000000005123456.index
└── leader-epoch-checkpoint
写入路径:新消息 → 永远 append 到最后一个 segment 的文件尾
没有任何"在文件中间找空位更新"的逻辑——文件写满 1GB 就滚下一个,旧的读得慢也没关系,反正会过期。
2.2 为什么追加写就是快
| 维度 | 随机写(B+ 树式更新) | 顺序写(追加日志) |
|---|---|---|
| 寻道次数 | 每 4K 一次 | 几乎为零(文件尾) |
| HDD 有效吞吐 | ~0.4 MB/s | ~150 MB/s |
| NVMe 有效吞吐 | ~1.2 GB/s(有写放大) | ~2.8 GB/s |
| 文件系统开销 | 逐块元数据更新 | 顺序预分配,元数据变更极少 |
| 缓存友好度 | 差(热区分散) | 极好(永远写"最热的尾部") |
顺序写还附赠两个红利:
- 写入永远是"最热数据"——日志尾部就是刚刚被 producer 发来的消息,正好落在 PageCache 里(第三板斧的前提)。
- 删除变成 O(1)——过期策略是"删整个旧 segment 文件",不需要在文件中间回收空间,磁盘碎屑为零。
2.3 随机读的补救:稀疏索引
消费者按 offset 拉取,总不能从文件头开始扫。Kafka 的方案是稀疏索引:
00000000000005123456.index 内容(示意):
offset 5123456 → position 0
offset 5127458 → position 4092 ← 每 ~4KB 消息才记一条索引
offset 5132801 → position 8207
...
查找 offset=5130000:
① 稀疏索引二分查找 → 落在 5127458 与 5132801 之间
② 定位到 position 4092
③ 顺序扫描 ~2.9KB 消息 → 命中目标
定位成本 = 一次内存二分 + 最多 4KB 的顺序扫描。索引文件本身还用 mmap 映射进内存(零拷贝那节会再提),查找全程不碰磁盘读。用 4KB 一条的稀疏度换"索引足够小、常驻内存",这是典型的"用顺序扫描代替随机查找"。
2.4 结论
顺序写性能对比(同盘实测):
├── 随机写(HDD) 0.4 MB/s ← 被机械寻道拖死
├── 顺序写(HDD) 150 MB/s ← 提升 375 倍
├── 随机写(NVMe) 1.2 GB/s ← SSD 也逃不掉写放大
└── 顺序写(NVMe) 2.8 GB/s ← 提升 2.3 倍,且延迟更稳
第一板斧解决的是磁盘 IO 的效率,但光快还不够——如果每条消息都真的落盘,延迟仍然在毫秒级。真正让"读写如内存"的是第二板斧。
三、第二板斧:PageCache——Kafka 压根不自己设计缓存
3.1 先看一台 Kafka Broker 的 free -h
$ free -h
total used free shared buff/cache available
Mem: 32Gi 4.2Gi 1.1Gi 16Mi 26.7Gi 27.1Gi
↑ buff/cache 26.7GB ← 这就是 PageCache
新手看到 free 只剩 1.1Gi 会 panic,老手看到 buff/cache 占 26.7Gi 会欣慰——这正是 Kafka 集群该有的样子。
3.2 write() 到底发生了什么
Linux 对磁盘文件的所有读写都经过页缓存(PageCache,以 4KB 页为单位):
【写入路径】
producer 消息 → Kafka(JVM)→ write() 系统调用
↓
┌─────────────────┐
│ PageCache │ ← 数据此刻已"落盘"返回!
│ (内存中的页) │
└────────┬────────┘
↓ 内核异步刷盘(pdflush/flush 线程)
物理磁盘
【读取路径】
consumer 拉取 → Kafka read() → 内核先查 PageCache
↓
命中:直接从内存返回,磁盘全程无知觉
未命中:磁盘读 → 装入 PageCache → 返回
关键认知:write() 返回成功时,数据只是进了 PageCache,还没到磁盘。Kafka 完全依赖这套机制,把"什么时候刷盘"交给内核的内存回收策略。
3.3 Kafka 为什么不自建缓存
对比另一种设计(一些 MQ 和新手实现会这么干):在 JVM 堆里维护 Map<offset, 消息> 做缓存:
| 维度 | JVM 堆内自建缓存 | OS PageCache |
|---|---|---|
| GC 压力 | 缓存越大 GC 越痛(对象、引用) | 零 GC(内存不归 JVM 管) |
| 对象开销 | 消息对象头/指针开销 2~5 倍 | 紧凑字节流,无对象化 |
| 内存规模 | 受堆上限约束(大堆 = 大灾难) | 可用全部空闲物理内存 |
| 重启后 | 缓存清零,冷启动 | 缓存仍在(内核页缓存跨进程存活) |
| 双重缓存 | JVM 缓存 + PageCache 冗余占内存 | 只有一份 |
Kafka 文档里明确写了自己的设计哲学:rely on the OS——操作系统花了三十年打磨页缓存,没必要在 JVM 里重造一遍,还造得更差。
3.4 热路径全命中:读写都在内存完成
把第二板斧放到真实流量里看,才是它真正可怕的地方:
【追上来了的消费(绝大多数流量)】
producer ──write──▶ PageCache(尾部热数据)
▲│
consumer ──read────────┘│ ← 消费 offset 紧跟 producer
▼
物理磁盘:全程休眠(未命中才被唤醒)
【只有读"落后很多"的历史消息时】才真正读盘(装入 PageCache 后顺序读)
- 消费 offset 通常只落后 producer 几毫秒到几秒,要读的数据刚好还在 PageCache 里
- broker 的"写盘"和"供读"在内存层面完成,磁盘只负责被内核异步地、大批量地、顺序地刷写
- 这解释了开篇那个现象:80 万 msg/s 的集群,磁盘 util 却不到 30%
3.5 运维联动:给 PageCache 让路
理解了第二板斧,Kafka 集群的 JVM 参数就有了统一解释:
# 32GB 内存机器的典型配置:堆只给 6GB
export KAFKA_HEAP_OPTS="-Xmx6g -Xms6g"
# 为什么不给 20GB?——堆越大,留给 PageCache 的越少,冷读越多,反而更慢
# 为什么要关交换:
vm.swappiness = 1 # 宁可回收 page cache 也不 swap(swap = 一切延迟雪崩)
这套参数纪律在《Kafka 运维必备工具清单》的巡检场景里同样适用:看到 buff/cache 被大量回收(grep pgscan /proc/vmstat)、broker 开始读旧数据(iostat util 飙升),先查内存是不是被别的东西挤占了。
3.6 结论
第二板斧解决的是**"读写数据落在哪"**:热路径全内存(速度向内存看齐),磁盘退化为异步的、顺序的落盘器。但它顺手引出一个新问题——数据从 PageCache 到网卡,还要不要"搬进用户态再搬出去"?这就是第三板斧。
四、第三板斧:零拷贝 sendfile——省掉 2 次拷贝 2 次切换
4.1 传统路径:一次"读文件发网络"的隐形成本
consumer 拉消息,broker 要"读文件 + 发网络"。普通程序这么写(传统 BIO 就是这么写的):
byte[] buf = new byte[4096];
while ((n = file.read(buf)) > 0) { // 读文件
socket.write(buf, 0, n); // 发网络
}
看似两行代码,内核里发生了 4 次数据拷贝 + 4 次用户态/内核态上下文切换:
【传统 read + write】(DMA=网卡/磁盘控制器直拷贝)
磁盘 ──①DMA拷贝──▶ PageCache(内核态)
│
PageCache ──②CPU拷贝──▶ 用户态 buffer ← 拷贝①浪费:只为转个手
│ (read 返回,切回用户态)
用户态 buffer ──③CPU拷贝──▶ socket 缓冲区 ← 拷贝②浪费:数据没变又要搬一次
│ (write 调用,切回内核态)
socket 缓冲区 ──④DMA拷贝──▶ 网卡 ──▶ 网络
上下文切换 ×4,CPU 拷贝 ×2
CPU 在做"搬运工",还把 CPU 缓存挤爆了
数据从 PageCache 出来又进用户态、再回内核态——内容一个字节都没变,纯粹为 Java 代码的调用边界买单。
4.2 sendfile:数据不进用户态
Linux 提供的 sendfile(2) 系统调用,允许"把文件直接送到 socket":
【sendfile(无 scatter-gather 时)】
磁盘 ──①DMA拷贝──▶ PageCache
│
PageCache ──②CPU拷贝──▶ socket 缓冲区 ← 只剩这一次 CPU 拷贝
│
socket 缓冲区 ──③DMA拷贝──▶ 网卡 ──▶ 网络
上下文切换 ×2(sendfile 一次调用搞定),CPU 拷贝 ×1
【sendfile + 网卡 scatter-gather DMA(现代网卡)】
磁盘 ──DMA──▶ PageCache ──(只传文件描述符+偏移)──▶ 网卡散读装包 ──▶ 网络
CPU 拷贝 ×0 —— 真·零拷贝
省钱的明细账:上下文切换 4→2 次,CPU 拷贝 2→1 次(SG-DMA 时为 0 次)。IBM 早期对 sendfile 的评测结论是CPU 开销下降约 65%,高吞吐场景下这意味着同样一核 CPU 能服务的流量翻倍不止。
4.3 Kafka 的实现位置
零拷贝在 Kafka 源码里就一层窗户纸——Java NIO 的 FileChannel.transferTo,JVM 在 Linux 上会下沉为 sendfile:
// kafka 侧:FileRecords(一个 log segment 的文件视图)
// consumer 拉取时,网络线程直接把文件区间 transferTo 给 socket channel
public long writeTo(DestinationChannel channel, long position, int length) {
return fileChannel.transferTo(position, length, channel); // → sendfile(2)
}
配合第三板斧食用效果最佳:consumer 要读的消息大概率还在 PageCache 里,sendfile 的"文件端"是内存命中,一次磁盘 IO 都没有。
4.4 零拷贝的失效边界
生产排障必须知道它什么时候"失灵",否则你会疑惑为什么流量大增后 CPU 线程变烫:
| 场景 | 零拷贝是否生效 | 原因 |
|---|---|---|
| 无 SSL 的 consumer 拉取 | ✅ 生效 | 标准路径 |
| SSL/TLS 加密传输 | ❌ 失效 | 加密必须在用户态对数据做计算,必须先拷进用户态 |
| 消息需要改写(如转换格式) | ❌ 失效 | 改写必须见到数据本体 |
| 开启了 TLS 的跨集群同步(MM2 等) | ❌ 失效 | 同 SSL |
| 生产端发送 | 本就不涉及 | sendfile 是"文件→socket",producer 是"socket→文件" |
运维联动:生产环境开启 SSL 后吞吐下降 20~40% 不是 Kafka 退化,而是零拷贝路径被关掉了——评估加密成本时要把这笔账算进去(硬件加速/更快的 CPU/内网可考虑 PLAINTEXT+网络层隔离)。
4.5 结论
第三板斧解决的是 CPU 与内存带宽的浪费:让 CPU 别当搬运工。至此,磁盘(顺序写)、内存(PageCache)、CPU(零拷贝)三处都"不浪费"了,还剩最后一处——网络。
五、第四板斧:批量发送 + 压缩——把 N 次往返变成 1/N
5.1 单条发送的代价
假如 producer 每发一条消息都做一次网络请求:
单条发送:
send(msg1) ──── 网络 RTT 0.5ms ────▶ broker
send(msg2) ──── 网络 RTT 0.5ms ────▶ broker
...
send(msgN) ──── 网络 RTT 0.5ms ────▶ broker
每条消息的成本 = 一次 RPC + 一次 broker 端 syscall + 一个 TCP 包头
1 万条消息 = 1 万次往返 + 1 万个包头(~20MB 纯开销)
网络延迟不是瓶颈(0.5ms 很快),瓶颈是"次数":每条消息都触发一次完整的请求-响应周期,broker 端每条一个 syscall、一个包头——这些固定成本才是吞掉吞吐的真凶。
5.2 producer 聚合:攒一批再发
Kafka producer 的消息不是即时发送的,而是先在客户端缓冲区聚合:
【批量发送】
send(msg1) ─┐
send(msg2) ├─▶ producer 缓冲区(按分区聚合)
send(msg3) ─┤ │
send(...) ─┘ │ 攒满 batch.size(64KB) 或等满 linger.ms(5ms)
▼
一次 RPC 发出整个 batch ────▶ broker 顺序落盘
1 万条消息 = 1 万次本地内存 append + ~300 次 RPC
网络往返成本 ÷ 33,包头开销 ÷ 33,broker syscall 次数 ÷ 33
两个控制参数构成"延迟换吞吐"的旋钮:
batch.size=65536 # 单批最大 64KB:攒够立即发(默认 16KB 偏小)
linger.ms=5 # 最多等 5ms:没攒满也发(延迟上限)
compression.type=zstd # 整批压缩
注意 batch 是按分区聚合的——分区越多,单分区攒批越慢。所以"高吞吐场景分区数要足够"与"分区太多小批反而散"是一对权衡:吞吐型 Topic 通常 12~48 分区起步。
5.3 压缩:整批压比单条压强得多
批量还有个隐藏收益——压缩的前提。Kafka 的压缩作用在 batch 级别:
单条压缩:每条消息都带完整结构(key/headers/timestamp/元数据字段相似但被分开压)
整批压缩:500 条消息的"重复结构 + 相近 key + 连续时间戳"一起压 → 压缩比高得多
四种压缩算法选型(2KB 订单消息实测口径):
| 算法 | 压缩比 | 压缩速度 | 解压速度 | 适用 |
|---|---|---|---|---|
| gzip | 高(~3.5x) | 慢 | 中 | 存档类,CPU 富余 |
| snappy | 中(~2.5x) | 快 | 快 | 老默认,均衡 |
| lz4 | 中(~2.5x) | 最快 | 最快 | 低延迟优先 |
| zstd(推荐) | 高(~3.3x) | 快 | 快 | 2026 年默认首选 |
压缩的钱从哪省:producer 多花 CPU,换来——网络流量 ÷2~3、broker 磁盘空间 ÷2~3、PageCache 能装下更多消息(第二板斧命中率更高)、跨机房复制的带宽 ÷2~3。一笔 CPU 换四份收益,是四板斧里性价比最高的。
5.4 结论
第四板斧解决的是网络与系统调用的"次数税":RPC 次数、syscall 次数、包头字节,全部除以批大小。至此四板斧齐了。
六、四板斧联动:一条消息的一生
把四板斧放回同一条消息的生命周期,看它们如何接力:
producer(第四板斧:批量+压缩)
1 万条消息 → 本地聚合 → 300 个压缩 batch → 300 次 RPC
│
▼ 网络(流量已 ÷3)
broker(第二板斧:PageCache)
网络线程收到 batch → 追加写 → 数据进 PageCache 即返回
│(第一板斧:顺序写)
▼
内核异步地把 PageCache 顺序刷盘(磁盘 util <30%)
│(第二板斧:消费紧贴写入,热数据全在 PageCache)
▼
consumer 发起拉取
broker 网络线程:PageCache 命中 → sendfile(第三板斧)
│
▼
数据从内存直接散读进网卡 → consumer 收到
全程:磁盘只做顺序 IO、热数据只在内存流转、CPU 零拷贝搬运、
网络只发压缩后的整批流量
瓶颈消除对照表——这就是"四板斧"回答面试题的正确姿势:
| 板斧 | 消除的浪费 | 量化收益 |
|---|---|---|
| 顺序写 | 磁盘随机寻道 | HDD 场景 375 倍 |
| PageCache | 热数据的磁盘读写 | 磁盘 util <30%,读写向内存速度看齐 |
| 零拷贝 | CPU 搬运 + 上下文切换 | CPU 拷贝开销降 ~65% |
| 批量+压缩 | 网络往返与包头 | RPC ÷ 批大小,流量 ÷2~3 |
七、经典实测:为什么说"单机百万级"
7.1 LinkedIn 的经典对比(Kafka 官方文档收录)
Kafka 设计团队当年(2KB 消息、单机)做过对比压测,收录在官方文档至今:
| 消息队列 | 生产者吞吐 | 消费者吞吐 |
|---|---|---|
| ActiveMQ | 20,000 msg/s | 60,000 msg/s |
| RabbitMQ | 50,000 msg/s | 25,000 msg/s |
| Kafka | 600,000 msg/s | 600,000 msg/s |
注意两个细节:① Kafka 的消费端不降反不升(其他 MQ 消费常比生产更慢)——sendfile + 拉模式 + 批量拉取的功劳;② 600K 这个数字是十年前的硬件跑出来的。
7.2 2026 年的生产量级
我们集群(NVMe、2KB 消息、acks=all、zstd、32 分区)实测:
| 指标 | 数值 |
|---|---|
| 单 Broker 持续吞吐 | 80 万 msg/s(峰值 95 万) |
| 端到端 P99 延迟 | < 15 ms |
| Broker CPU | < 40% |
| 磁盘 util | < 30% |
CPU 和磁盘都有大量余量——这四个数字的组合就是四板斧生效的直接证据:瓶颈不在任何单点硬件,而在副本同步的网络。这也意味着调优时先看网络与副本,再怀疑磁盘,方向别搞反。
八、常见问题
8.1 顺序写这么好,MySQL/PG 为什么不全用?
因为它们要支持原地更新(UPDATE 改某一行的某个字段)和事务并发控制,必须随机读写。但它们也在偷偷用这套思想:WAL/binlog 是纯顺序写(先写日志再改页),LSM-Tree 存储引擎(RocksDB/HBase)更是把"所有写都变顺序"做到了极致——代价是读放大。Kafka 能全身心拥抱顺序写,是因为消息不可变:不 UPDATE、不 DELETE(只有过期整文件删除),这是业务模型的先天优势。
8.2 数据只进 PageCache 没真正落盘,掉电不就丢了?
会丢,所以 Kafka 不靠 fsync 保数据,靠副本:acks=all + min.insync.replicas=2 时,消息已被 ≥2 台机器的 PageCache 收到,单机掉电不丢(双机同刻掉电的概率才是残余风险)。这本质上是"用网络冗余代替磁盘 fsync"——fsync 一次几毫秒,跨 AZ 的网络写一次不到 1ms,快得多。想更激进可调 log.flush.interval.messages 强制刷盘,但一般不建议。
8.3 怎么验证零拷贝真的生效了?
间接证据:consumer 高吞吐时 broker 的网络线程 CPU 占用远低于无零拷贝基线;strace 抓网络线程能看到 sendfile 系统调用(呼应《用好这 5 个 Linux 命令》:strace -f -e trace=sendfile -p <network-thread-pid>)。直接反证:开启 SSL 后同一压测吞吐下降 20~40%,就是零拷贝路径被关闭的实锤。
8.4 Kafka 的堆内存是不是越大越好?
恰恰相反。堆只承载网络缓冲和副本同步的临时对象,热数据在 PageCache 不在堆里。32GB 内存的机器给 6GB 堆是惯例——堆给太大,GC 停顿变长(写路径直接受害),PageCache 反而被挤占(读路径受害)。看到 free -h 里 buff/cache 占总内存 60~80% 才是健康态。
8.5 全 SSD/NVMe 时代,顺序写还有意义吗?
有,但收益变小:NVMe 随机写与顺序写差距缩到 2 倍左右。不过顺序写的价值不止吞吐:① SSD 有写放大,随机写会显著放大实际写入量、缩短寿命;② 顺序写延迟更稳定(无寻道抖动),P99 更好看;③ 零拷贝、PageCache、批量与盘型无关,仍然全额生效。四板斧是一个组合,盘型升级只影响其中一斧的边际收益。
8.6 这四板斧跟日常排障有什么关系?哪些现象能用它解释?
太多了:① 磁盘 util 高但 Kafka 不慢——那是内核在异步刷盘,属正常;② swap 使用量 >0——PageCache 被挤占,延迟雪崩的前兆(查内存);③ 开 SSL 后吞吐腰斩——零拷贝失效;④ 小消息高频发送延迟高——batch.size 太小/linger.ms=0,批没攒起来;⑤ 分区太多吞吐反而降——每分区批太小。理解机制,现象就不再"玄学"(工具层面的巡检与排查见《Kafka 运维必备工具清单》)。
九、总结
选型/调优决策树
Kafka 快不快,排查顺序:
├── 吞吐上不去?
│ ├── producer 端 batch.size 太小 / linger.ms=0 → 调批量参数
│ ├── 分区数 < 并行度 → 扩分区
│ └── compression.type 没配 → 上 zstd/lz4
├── 延迟抖动?
│ ├── swap > 0(PageCache 被挤占) → 收堆内存/赶走抢内存的进程
│ └── 磁盘 util 高 + 读旧数据多 → 检查 retention 与消费滞后
├── CPU 高但业务量没变?
│ └── 是否新开了 SSL(零拷贝失效) → 评估加密成本
└── 磁盘 util 30% 但很"快"?
└── 正常:异步刷盘 + 热数据在 PageCache
四板斧速查表
| 板斧 | 关键点 | 核心参数/配置 |
|---|---|---|
| 顺序写 | partition=追加日志,稀疏索引定位 | 无需参数(架构保证) |
| PageCache | 热数据全内存,磁盘异步顺序刷 | 堆 6GB/32GB 机、vm.swappiness=1 |
| 零拷贝 | consumer 拉取走 sendfile | 开 SSL 会关闭(评估成本) |
| 批量+压缩 | RPC÷批大小,流量÷压缩比 | batch.size=64KB、linger.ms=5、zstd |
一句话
Kafka 的快不是单点黑科技,而是四个环节同时"不浪费"的乘法:磁盘只做顺序 IO(随机寻道被架构性地消灭,HDD 上 375 倍的差距),热数据只在 PageCache 流转(write() 进内存即返回,磁盘沦为异步落盘器,这正是 Kafka 不自建缓存、把 GC 和双重缓存的坑都留给别人的底气),CPU 不搬运不变的数据(sendfile 让上下文切换 4→2、CPU 拷贝 2→0,开销降 65%),网络只发压缩后的整批流量(RPC 次数除以批大小,zstd 再把流量砍掉 2/3);四个机制又彼此增强——批量让顺序写的每笔更大、PageCache 让 sendfile 的文件端全命中、压缩让 PageCache 装得下更多热数据——所以"单机百万级"不是某一个开关,而是一条消息从生产到消费的每一跳都在为下一跳铺路;理解了这套"浪费清零"的思想,你就明白它为什么能从 LinkedIn 的日志管道长成数据基础设施的底座,也知道调优该从哪四个旋钮下手:批量参数、堆与 PageCache 的配比、加密的取舍、分区的数量——面试答"顺序写+零拷贝"只值一半分,另一半在"它们如何互相成就"。
关键数据
- HDD 随机写 ~0.4MB/s vs 顺序写 ~150MB/s:375 倍
- sendfile:上下文切换 4→2 次,CPU 拷贝 2→1 次(SG-DMA 后 0 次),CPU 拷贝开销降 ~65%
- 批量 500 条/批:RPC、syscall、包头开销÷500
- zstd 压缩比 ~3.3x:网络/磁盘/PageCache/跨机房带宽四份收益
- 经典 LinkedIn 基准(2KB):Kafka 600K msg/s vs ActiveMQ 20K、RabbitMQ 50K
- 我们的生产实测:单 Broker 80 万 msg/s @ CPU<40%、磁盘 util<30%
给团队的建议
| 项目状态 | 建议 |
|---|---|
| 新集群规划 | 堆 6GB/32GB 内存起步,vm.swappiness=1,NVMe 顺序写配置验收 |
| producer 调优 | batch.size=64KB + linger.ms=5 + zstd 是默认基线,压测后微调 |
| 安全评估 | 开 SSL 前把零拷贝失效的吞吐损耗算进容量规划 |
| 监控告警 | 盯四件套:磁盘 util、swap/pgscan、网络线程 CPU、单批平均大小 |
| 排障手册 | 把本文 8.6 的现象-机制对照表加进值班 Runbook |
| 面试/分享 | 用"四板斧联动"讲,别背名词——机制间的互相成就才是深度 |
互动话题:你们踩过哪些"现象玄学、机制一看就懂"的 Kafka 性能问题(SSL 吞吐腰斩、小消息延迟高、swap 雪崩)?你们生产集群单机最高跑过多少 msg/s?评论区报个数,我把点赞最高的场景拆成下一篇的实战案例。
标题:Kafka 为什么这么快?——顺序写+页缓存+零拷贝+批量,四板斧拆解
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/10/21/1792332090187.html
公众号:服务端技术精选
- 引言
- 一、先破除一个误解:磁盘不慢,"随机访问"才慢
- 1.1 一组真实数据
- 1.2 机械盘为什么随机慢
- 1.3 Kafka 的立场
- 二、第一板斧:顺序写
- 2.1 Kafka 的存储结构:partition 就是一个日志文件
- 2.2 为什么追加写就是快
- 2.3 随机读的补救:稀疏索引
- 2.4 结论
- 三、第二板斧:PageCache——Kafka 压根不自己设计缓存
- 3.1 先看一台 Kafka Broker 的 free -h
- 3.2 write() 到底发生了什么
- 3.3 Kafka 为什么不自建缓存
- 3.4 热路径全命中:读写都在内存完成
- 3.5 运维联动:给 PageCache 让路
- 3.6 结论
- 四、第三板斧:零拷贝 sendfile——省掉 2 次拷贝 2 次切换
- 4.1 传统路径:一次"读文件发网络"的隐形成本
- 4.2 sendfile:数据不进用户态
- 4.3 Kafka 的实现位置
- 4.4 零拷贝的失效边界
- 4.5 结论
- 五、第四板斧:批量发送 + 压缩——把 N 次往返变成 1/N
- 5.1 单条发送的代价
- 5.2 producer 聚合:攒一批再发
- 5.3 压缩:整批压比单条压强得多
- 5.4 结论
- 六、四板斧联动:一条消息的一生
- 七、经典实测:为什么说"单机百万级"
- 7.1 LinkedIn 的经典对比(Kafka 官方文档收录)
- 7.2 2026 年的生产量级
- 八、常见问题
- 8.1 顺序写这么好,MySQL/PG 为什么不全用?
- 8.2 数据只进 PageCache 没真正落盘,掉电不就丢了?
- 8.3 怎么验证零拷贝真的生效了?
- 8.4 Kafka 的堆内存是不是越大越好?
- 8.5 全 SSD/NVMe 时代,顺序写还有意义吗?
- 8.6 这四板斧跟日常排障有什么关系?哪些现象能用它解释?
- 九、总结
- 选型/调优决策树
- 四板斧速查表
- 一句话
- 关键数据
- 给团队的建议
评论