Kafka 敢把消息写进磁盘,是因为它避开了磁盘最慢的用法
Kafka 的高吞吐不是某个神奇组件撑起来的。它真正做对的,是把随机、小块、反复搬运的数据路径,改成 append、batch、page cache 和 sendfile 能吃下去的形状。

日志量一上来,消息队列先扛不住。
很多团队的第一反应很自然:换 SSD,加 broker,调 batch size,甚至开始怀疑是不是 Kafka 参数没配对。
但吞吐上不去,有时候不是机器不够好。是数据一路都在走慢路:小块写、随机查、反复序列化,从应用层读出来,再拷进 socket,CPU 和 GC 跟着一起烧。
Kafka 最容易被误解的地方就在这里。
它不是让磁盘突然变快了,也不是靠 zero-copy 一个神技把所有问题抹掉。Kafka 真正做对的,是从一开始就把数据路径设计成硬件和操作系统擅长的形状。
顺序,大块,少拷贝。
这才是 Kafka 高吞吐的底层味道。
先把“快”说清楚:Kafka 快,主要快在吞吐
讨论 Kafka 为什么快,先别急着背 partition、ISR、replica、controller。
更应该先问一句:这里的快,到底是哪种快?
很多人把快默认理解成低延迟:一条消息发出去,下一毫秒就要被消费。Kafka 当然也在意延迟,但它最出名的能力不是“单条消息永远最低延迟”,而是持续搬运大量 records 的能力。
也就是 high throughput。
这两个东西不一样。
低延迟像外卖骑手送一单,追的是这一单多久到。高吞吐像港口卸货,追的是一小时能吞掉多少集装箱。你不能拿港口的设计去要求它像骑手一样灵活,也不能拿骑手的速度去推导一个港口的吞吐。
Kafka 更像后者。
它关心的是:数据能不能稳定地、大块地、连续地从 producer 进来,写进 log,再被 consumer 拉走。只要这条路径足够顺,单条消息上的一点点额外开销,被摊到一大批数据里,就不再那么显眼。
所以这篇文章不想把 Kafka 写成“低延迟神器”。它真正值得学的,是高吞吐系统怎么避开慢路径。
“磁盘慢”这句话,太粗了
很多人第一次理解 Kafka 时,会卡在一个矛盾上:
消息不是落磁盘吗?磁盘不是慢吗?那 Kafka 为什么还快?
这个问题的坑在于,“磁盘慢”只说对了一半。
磁盘确实怕慢,但它怕的不是“被使用”,而是被随机、小块、来回折腾地使用。
Kafka 官方设计文档里有一组经典数字:在 6 块 7200rpm SATA 磁盘的配置下,线性写大约可以到 600MB/s,而随机写大约只有 100kB/s,差距超过 6000 倍。
这组数字不要拿去当今天的硬件采购 benchmark。现代 SSD、NVMe、文件系统、队列深度都会改变具体结果。
但它说明的方向一直没变:访问模式会决定硬件表现。
顺序写像沿着一本账本继续往下记。笔不用停,页不用翻来翻去。
随机写像每写一行都去不同房间找一本书,写完再去下一个房间。真正花时间的,不是那几个字,而是来回找位置。
Kafka 的 partition log 就是在利用这个差异。
Producer 写入消息时,本质上是往某个 partition 的 log 末尾 append。Consumer 读取时,也通常按 offset 顺序往后 fetch。数据不是到处插入、更新、删除,而是不断向后追加。
这件事非常朴素,但很关键。
Kafka 没有把磁盘当成一个随便查、随便改的数据库用。它更像一本只往后写的流水账。
磁盘最怕随机访问,Kafka 就尽量不给它随机访问。
这不是玄学优化,是工程礼貌:让硬件做它擅长的事。
batch 不是参数细节,而是把小动作变成大流量
只说 append-only log 还不够。
如果每条消息都单独来一次系统调用、单独刷一次网络包、单独触发一次磁盘写,哪怕方向是顺序的,也会被碎片化开销拖死。
高吞吐系统怕的不是单次动作慢一点,而是“小动作”乘上百万次。
Kafka 里 batch 的价值就在这里。
Producer 可以把多条消息攒成一批发送。Broker 以 message set / batch 的形式接收并追加。Consumer 也不是每次只拿一条,而是 fetch 一大块连续数据。
这会同时改变几件事:
网络包变大,协议开销被摊薄;磁盘写更连续,OS 更容易做 write-behind;读路径也更像线性扫描,而不是被无数小请求打断。
所以 batch 不只是一个“调大吞吐”的参数。它是 Kafka 快路径的一部分。
append 解决的是“往哪写”。
batch 解决的是“不要一粒一粒搬”。
这两个东西合在一起,数据才开始像流水线,而不是像一堆快递员各跑各的。
Kafka 没有和操作系统抢缓存
第二个容易误解的地方是内存。
很多人会下意识觉得:要快,就应该把数据放在应用内存里。磁盘慢,那就尽量别碰磁盘。
Kafka 走了另一条路。
它没有在 JVM 堆里维护一份巨大的消息缓存,再等内存不够了手忙脚乱 flush。Kafka 把数据写进 filesystem 上的 persistent log,实际效果是让数据先进入操作系统的 page cache,再由 OS 按自己的策略写回磁盘。
这听起来像把控制权交出去了,但恰恰是聪明的地方。
现代操作系统本来就会把空闲内存拿来做磁盘缓存。你从文件读写的数据,天然会经过这层 cache。Kafka 如果在 JVM 堆里再做一份缓存,很可能变成两份:应用里一份,OS page cache 里一份。
这不只是浪费内存。
在 JVM 里,数据不是一块干净的 byte array 就结束了。对象有额外开销,堆越大,GC 越难控。你以为自己是在加缓存,最后可能是在给 GC 加压力。
Kafka 官方文档里也强调了这个取舍:依赖 filesystem 和 page cache,可以自动利用空闲内存,避免应用内缓存和 OS cache 重复,还能让服务重启后保留一部分 warm cache。
这点很容易被低估。
进程内缓存重启就没了。page cache 属于操作系统,只要机器没重启,Kafka 进程重启后,热数据仍可能还在。
所以 Kafka 不是“不用内存”。
它是把缓存放到了更合适的位置。
应用层不适合做的事情,就别硬抢。尤其是这种已经被 OS 打磨了几十年的事。
zero-copy 不是没有 copy,是少让应用层当搬运工
再看消费路径。
Producer 写进来的数据,最后要被 consumer 拉走。这个过程如果走普通 read/write,大概会变成这样:
- OS 从磁盘把数据读到 page cache。
- Kafka application 从 kernel space 读到 user-space buffer。
- Kafka 再把 user-space buffer 写回 kernel space 的 socket buffer。
- OS 把数据送到 NIC buffer,经网络发出去。
你会发现,Kafka application 在中间干了一件很尴尬的事:它不一定真的需要理解这些 bytes,却要先把它们接到手里,再递出去。
像一个仓库主管,本来只需要安排货从 A 口到 B 口,结果每个箱子都要先搬进自己办公室,再搬出去。
数据量小的时候,这点动作不明显。吞吐一上来,所有多余 copy 都会变成 CPU、内存带宽、上下文切换和 GC 压力。
sendfile() 的价值,就是把这个办公室绕过去。
Linux man page 对 sendfile() 的说法很直接:它在两个 file descriptor 之间传输数据;因为复制发生在 kernel 内部,所以比 read() + write() 更高效,后者需要把数据传入和传出 user space。
Kafka 能利用这个能力,是因为它的数据格式和存储方式刚好配合:消息以相对稳定的二进制格式写在 log 里,消费时可以把 page cache 里的数据沿着 socket 路径送出去。

这里一定要压住一个误解:zero-copy 不是“物理上完全没有拷贝”。
名字很容易误导人。更准确的理解是:少经过用户态中转,少做重复搬运,少让 CPU 做没必要的 byte copy。
Kafka 官方文档也没有把它讲成魔法。优化路径里仍然会有到网络设备相关 buffer 的数据传递。关键不是“零”,而是应用层不再夹在中间做纯搬运。
这就是为什么 zero-copy 对 Kafka 这种场景特别有用。
Kafka 不需要在消费路径上重新解释每条消息。它更需要的是,把已经写好的 log chunk 高效送给 consumer。
只要数据路径能保持这个形状,OS 就能接手很多脏活。
真正的链路是 append → batch → page cache → sendfile
把前面几件事串起来,Kafka 的高吞吐就没那么神秘了。
它不是一个点快。
它是一整条路不别扭。
Producer 端通过 batch 把很多小消息合成更大的请求。
Broker 端把数据 append 到 partition log,尽量保持顺序写。
Filesystem 和 OS page cache 接住这些数据,负责缓存、预读、写回。
Consumer 拉取时,如果数据还在 page cache,甚至可能没有明显的物理磁盘读;发送时再通过 sendfile() 这类路径减少用户态中转。
这几步单独看,都不是特别神秘。
但它们咬在一起,就形成了一条非常适合高吞吐的路线:
| |
这也是为什么“Kafka 为什么快”不能回答成“因为 zero-copy”。
zero-copy 只是其中一段。
如果前面不是 append-only log,不是统一的消息格式,不是大块连续数据,不是 page cache 能接住的文件路径,sendfile() 也吃不到多少红利。
很多技术文章喜欢找一个主角:某个算法、某个系统调用、某个参数。
但工程系统真正厉害的地方,常常不是某个点,而是几个普通设计互相配合,最后让一条路径变短。
Kafka 就是这样。
这对业务系统有什么用?
你不一定要写一个 Kafka。
但你很可能会写一个日志系统、埋点管道、审计队列、批量导入服务,或者某个“看起来只是把数据从 A 搬到 B”的后台任务。
这类系统一慢,团队很容易先找硬件和框架背锅。
先别急。
画一遍数据路径,通常更有用。
可以按这五个问题查:
- 写入是不是 append-only?还是频繁随机更新、随机查找?
- 请求是不是 batch?还是每条记录都触发一次小 I/O?
- 数据从磁盘到网络时,有没有经过应用层无意义中转?
- 应用堆里是不是做了一份和 OS page cache 重复的缓存?
- 你要的是高吞吐,还是单条最低延迟?这两个目标有没有混在一起调?
这套检查不只适用于 Kafka。
如果一个系统的核心任务是搬大量数据,那最先看的就不该是“用了什么高级组件”,而是数据有没有在不该停留的地方停留。
反过来,如果你的系统需要大量随机查询、复杂过滤、强事务更新,Kafka 这套 log 型思路也不该被硬套。
顺序追加很强,但它不是万能设计。
它牺牲的是随机修改的自由,换来线性吞吐、可回放和更简单的数据流。
这就是取舍。
结尾:别问磁盘快不快,先问路径对不对
Kafka 的快,不是靠违反“磁盘慢”的常识。
它靠的是更细的常识:磁盘怕随机,不怕顺序;系统怕小碎步,不怕大块流动;应用层最不该做的事,就是在高吞吐链路里当纯搬运工。
所以下次有人问“Kafka 为什么明明写磁盘还这么快”,不要急着回答 zero-copy。
先画路径。
一条消息从 producer 到 broker,再从 broker 到 consumer,中间有没有随机、小块、重复拷贝、用户态来回折返?
如果这些慢路都被避开了,磁盘就不再是那个最直觉的瓶颈。
Kafka 真正给后端开发者的启发也在这里:
高吞吐系统不是把硬件喊快,而是让数据少走冤枉路。