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/s0.8 MB/s / 0.4 MB/s顺序是随机的 300+ 倍
SATA SSD520 MB/s / 460 MB/s210 MB/s / 130 MB/s(5 万 IOPS)顺序比随机快 3 倍多
NVMe SSD3.2 GB/s / 2.8 GB/s1.6 GB/s / 1.2 GB/s(30 万 IOPS)顺序仍比随机快 2 倍+
DDR4 内存~25 GB/s—顺序写磁盘 ≈ 内存 1/10

两个反直觉的结论:

  1. 磁盘顺序写的速度相当能打——HDD 150MB/s,NVMe 2.8GB/s,已经达到内存的 1/10~1/8。"磁盘慢"是"磁盘随机访问慢"。
  2. 随机写才是灾难——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
文件系统开销逐块元数据更新顺序预分配,元数据变更极少
缓存友好度差(热区分散)极好(永远写"最热的尾部")

顺序写还附赠两个红利:

  1. 写入永远是"最热数据"——日志尾部就是刚刚被 producer 发来的消息,正好落在 PageCache 里(第三板斧的前提)。
  2. 删除变成 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 消息、单机)做过对比压测,收录在官方文档至今:

消息队列生产者吞吐消费者吞吐
ActiveMQ20,000 msg/s60,000 msg/s
RabbitMQ50,000 msg/s25,000 msg/s
Kafka600,000 msg/s600,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
公众号:服务端技术精选
    评论
    0 评论
avatar

取消