Skip to content
Charles Shao
Go back

Kafka 性能内核:磁盘系统凭什么跑出内存级吞吐

views

系列前四篇讲了 Kafka 是什么、怎么写、怎么读、怎么不丢:核心原理ProducerConsumer可靠性与 EOS。最后一块拼图是性能

Kafka 用的是最普通的磁盘,为什么能跑出百万级吞吐、甚至比很多”内存”系统还快?

答案不是什么黑魔法,而是四个朴素到极致的选择:顺序写、页缓存、零拷贝、日志分段,再叠加批量与压缩。这篇把它们逐个拆开——理解了它们,你也就理解了核心原理篇 §11 那句”把日志顺序化、把状态客户端化、把并行分区化”里”顺序化”到底快在哪。

TL;DR

Table of contents

Open Table of contents

1. 一个反直觉的事实:磁盘不一定慢

工程师的直觉是”磁盘慢、内存快,所以高性能系统要把数据放内存”。Kafka 偏偏反着来——数据全落磁盘,却比很多内存系统还快。破解这个悖论的钥匙是一句被误解的话:

“磁盘慢”慢的是随机访问,不是顺序访问。 在顺序读写下,磁盘的带宽可以逼近内存,还便宜好几个数量级。

Kafka 的全部性能设计,都是围绕”把一切访问都变成顺序的、把一切拷贝都省掉”。下面四节就是这四板斧。

2. 第一板斧:顺序写

核心原理篇 §1 说 Kafka 的内核是一条 append-only 日志——这不只是数据模型上的优雅,更是性能上的第一杀招。

顺序写 vs 随机写对比图,分上下两块。上块『随机写(传统 MQ / 数据库常见)· 慢』:写请求进来后磁头/寻址不断跳来跳去,每次写要 seek 到不同位置,寻道加旋转延迟成为瓶颈,HDD 上仅约百 KB/s 到几 MB/s。下块『顺序写(Kafka:append-only 日志)· 快』:写请求永远追加到当前段文件末尾,磁头几乎不用移动,接近磁盘线速,HDD 也能上百 MB/s,加上 page cache 攒批刷盘可逼近内存写。底部注解:同一块磁盘,顺序与随机可差几个数量级;Kafka 的第一板斧就是把所有写都变成顺序追加,这是日志内核的直接红利。

传统数据库/MQ 常要更新任意位置的记录(随机写),磁头/寻址不停跳动,寻道与旋转延迟成为瓶颈。Kafka 因为只追加不修改

这就是为什么核心原理篇 §11 把”只能追加、不支持随机改”列为一项取舍:Kafka 主动放弃了随机修改能力,换来了顺序写的极致吞吐。数据模型的约束,直接变成了性能红利。

3. 第二板斧:页缓存(把内存交给 OS)

拿到顺序写还不够。Kafka 的第二个聪明之处是:它几乎不自己管缓存,而是把内存全交给操作系统的 page cache。

页缓存读写路径图。Producer 写入先落到操作系统内核的 Page Cache(OS 内存),再按策略异步 flush(顺序写)到磁盘上的段文件 .log。Consumer 读取时:读命中的情况下,热数据直接从 page cache 返回(追尾消费几乎全命中);读未命中时,冷数据(回溯历史)才真正读盘、加载进 page cache 再返回。底部注解说明 Kafka 不自建应用层缓存、把内存交给 OS page cache 的四点好处:① 避免 JVM 堆内缓存的 GC 压力与对象开销;② 避免应用缓存加 page cache 的双重缓存浪费内存;③ 进程重启缓存仍在(page cache 属于 OS,不随进程消失);④ 生产者刚写的数据往往就是消费者马上要读的,命中率高。

写入路径:消息先写进 page cache(OS 内存里的文件缓存),由 OS 按策略异步刷盘——所以”写”几乎是内存速度,落盘在后台顺序进行。读取路径:消费者读数据时,热数据直接命中 page cache,根本不碰磁盘。

为什么不用 JVM 堆内缓存?因为 page cache 有四个堆内缓存给不了的好处:

  1. 躲开 GC:几十上百 GB 的数据若放 JVM 堆,GC 会被拖垮;page cache 在 OS 管理的堆外内存,与 GC 无关。
  2. 不双重缓存:数据本来就要经过 page cache 落盘,若应用再缓存一份,就是”page cache + 堆内”两份,白白浪费内存。
  3. 重启不失效:page cache 属于 OS,Kafka 进程重启,缓存仍在——重启后消费不会全部冷读。
  4. 天然高命中:Kafka 的访问模式是”刚写的数据马上被读”(追尾消费),这正是 page cache 最擅长的局部性。

一个推论:别给 Kafka 的 JVM 堆配太大(通常 6GB 左右足矣),把物理内存尽量留给 page cache。这和调优其他 Java 服务”堆越大越好”的直觉恰好相反——Kafka 的性能主要靠 OS 内存,不靠 JVM 堆。

4. 第三板斧:零拷贝(sendfile)

消费者拉数据时,broker 要把磁盘/page cache 里的段文件发到网络。传统做法要在用户态和内核态之间来回拷贝好几趟,纯属浪费。

零拷贝对比图,分上下两块。上块『传统方式:4 次拷贝 + 4 次上下文切换』:① 磁盘到内核 page cache(DMA 拷贝)→ ② page cache 到用户态缓冲区(CPU 拷贝)→ ③ 用户态缓冲区到 socket 缓冲区(CPU 拷贝)→ ④ socket 缓冲区到网卡 NIC(DMA 拷贝),注解指出数据在用户态过了一趟手又原样送回内核,纯搬运浪费 CPU 与内存带宽。下块『零拷贝 sendfile:数据不进用户态』:① 磁盘到内核 page cache(DMA)→ ② page cache 直接到网卡 NIC(sendfile/DMA,不经用户态),注解指出消费者拉数据时 Kafka 用 sendfile 把段文件从 page cache 直送网卡,拷贝次数与上下文切换大幅减少。底部警告:开启 SSL/TLS 加密时 broker 需在用户态加密数据,会破坏零拷贝、回落到需要用户态拷贝,导致吞吐下降、CPU 上升。

Kafka 用操作系统的 sendfile 系统调用(Java 里是 FileChannel.transferTo):把段文件的数据从 page cache 直接送到网卡,中间不经过用户态缓冲区。数据”过一次内核”就走了,CPU 拷贝和上下文切换大幅减少。

这也是”read 不删除、按 offset 顺序读”和零拷贝的绝配:因为消费者读的就是段文件里连续的一段字节,Kafka 可以原样 sendfile 出去,不需要在用户态反序列化/重组。 数据模型(连续日志)再一次直接喂给了性能优化。

一个重要的实战注意

开启 SSL/TLS 会破坏零拷贝。 因为加密必须在用户态进行,数据不得不被拷回用户态加密再发出——零拷贝失效,吞吐下降、CPU 上升。所以”要不要给 broker 间/客户端加 TLS”是一个明确的性能取舍,不是免费的。

5. 第四板斧:日志分段与稀疏索引

一个分区的日志不是一个巨型文件,而是切成许多段文件(log segment)。这既是核心原理篇 §8 存储机制的物理基础,也是查找与保留高效的关键。

日志分段与索引结构图,分上下两块。上块『一个 Partition 目录 = 多个 Segment(段)』:Segment 0(00000000000.log 及其 .index/.timeindex)→ Segment 1(00000123456.log 及索引)→ Active Segment 正在写(00000456789.log 及索引)。下块『按 offset 查找一条消息(两级二分)』:① 用文件名(基准 offset)二分定位所在段 → ② 在该段的 .index 里二分(稀疏索引:offset 映射到文件物理位置)→ ③ 从最近的物理位置顺序扫到目标记录。底部注解说明段的意义:① 保留/删除以整段为单位,到期删老段而不改动正在写的段;② 索引是稀疏的(每隔几 KB 记一条)加上通过 mmap 内存映射,查找快且占用小;③ 只有 active segment 在追加写、其余只读,因此顺序写与零拷贝读都成立。

每个段由三个文件组成:

文件内容
.log实际消息数据(一批批顺序追加的 record batch)
.index稀疏的 offset 索引:相对 offset → 文件内物理位置(每隔几 KB 记一条,不是每条都记)
.timeindex时间戳索引:timestamp → offset(支持按时间定位,如 offsetsForTimes

按 offset 定位一条消息的过程是两级二分 + 顺序扫

  1. 段文件名就是该段的基准 offset,用它二分定位目标 offset 落在哪个段;
  2. 在该段的 .index 里二分,找到不超过目标 offset 的最近索引项(物理位置);
  3. 从那个物理位置开始顺序扫到目标记录。

为什么索引是稀疏的?因为”每条都建索引”太占空间,而”稀疏索引 + 短距离顺序扫”在有 page cache 的前提下几乎一样快。索引文件还通过 mmap 内存映射访问——又省内存又快。分段 + 稀疏索引,是”既要能按 offset 随机定位、又要保持顺序 IO 优势”的精妙折中。

段还让保留策略变得廉价:删除过期数据(retention)或压缩(compact)都以整段为单位操作,从不去改动正在写的 active segment——顺序写和只读段的零拷贝因此都始终成立。

6. 加成项:批量与压缩

前面四板斧是”IO 路径”上的优化,还有一层是”传输的数据量”上的优化。

两个协同效应值得记住:①批越大,压缩比通常越高(更多相似数据可压),所以 linger.ms / batch.size 调大既增吞吐又省带宽;②压缩省的是网络和磁盘带宽,代价是两端 CPU——zstd/lz4 在压缩比与 CPU 间平衡最好,是当下主流。broker 原样存压缩批,也正是零拷贝(§4)能成立的前提之一。

7. 把”快”串起来:一条消息的高速公路

单独看四板斧还不够直观,把它们串成一条消息的全生命周期就清楚了:

  1. Producer:多条消息攒成 batch、压缩 → 一次网络请求发出(批量)。
  2. Broker 写入:batch 原样顺序追加到 active segment,先落 page cache(近内存速度),异步刷盘。
  3. 复制:follower 从 leader 顺序拉取并同样顺序写——复制路径也是顺序 IO。
  4. Consumer 读取:命中 page cache 的热数据,通过 sendfile 零拷贝从 page cache 直送网卡,原样(仍压缩)发给消费者,消费者端解压。

全程的共同点:没有随机 IO,没有多余的用户态拷贝,没有 broker 端的反复解压/压缩。 这就是 Kafka 能用普通磁盘跑出百万级吞吐的全部秘密——不是某一个黑科技,而是数据模型(顺序日志)与系统调用(page cache + sendfile)严丝合缝地咬在一起

8. 什么时候 Kafka 会变慢

理解了快的原理,就能预判慢的场景——它们几乎都是”破坏了上面某一条”:

变慢场景破坏了什么对策
消费者回溯历史 / 冷读page cache 命中率暴跌,被迫磁盘随机读老段错峰重放、独立集群/分层存储、限速
物理内存不足 / JVM 堆过大page cache 被挤占,热读也要读盘减小 JVM 堆、给 page cache 留足内存
开启 SSL/TLS零拷贝失效(用户态加密)权衡安全与吞吐;必要时加机器补偿
分区/段文件过多打散顺序性、更多文件句柄与元数据、刷盘变碎按吞吐反推分区数(核心原理篇 §7),别无脑堆
频繁强制 flush(flush.ms 太小)破坏”异步顺序刷盘”,退化成频繁小 IO依赖副本保证持久性,别靠频繁 fsync
消息体过大 / 不压缩网络与磁盘带宽打满lz4/zstd、控制单条大小

记住这条主线:Kafka 的”快”建立在”追尾消费 + page cache 高命中 + 顺序 IO + 零拷贝”上。任何让消费者去读冷数据、让内存不够缓存热数据、让零拷贝失效的操作,都会把它打回”普通磁盘系统”的原形。

9. 性能相关配置速查

参数位置作用
batch.size / linger.msProducer攒批大小/等待,越大吞吐与压缩比越高(延迟换吞吐)
compression.typeProducerlz4 / zstd 主流,省带宽换 CPU
log.segment.bytesBroker/Topic单个段文件大小,影响保留粒度与文件数
log.index.interval.bytesBroker/Topic稀疏索引的间隔(每多少字节记一条索引)
log.flush.interval.messages/msBroker/Topic强制刷盘阈值;通常不设,靠副本保证持久
num.replica.fetchersBrokerfollower 复制并行度,影响复制吞吐
JVM 堆大小(KAFKA_HEAP_OPTSBroker通常 ~6GB,把内存留给 page cache
socket.request.max.bytes / fetch.max.bytesBroker/Consumer单请求/单次拉取的字节上限

10. 落到 AdTech:为什么”Netty + Kafka”能扛事件洪峰

核心原理篇 §13 提过高并发广告服务的经典栈 Netty + Kafka + Redis + Flink。现在能说清 Kafka 在其中”抗洪峰”的底气到底在哪:

一句话:广告事件洪峰之所以能被 Kafka 稳稳接住,靠的正是这篇讲的四板斧——顺序写吸洪流、page cache 供热读、零拷贝喂多方、分段索引管海量留存。

11. 生产反模式与踩坑

12. 常见误解 ↔ 正解

常见误解正解
Kafka 快是因为把数据放内存数据落磁盘;快来自顺序写 + page cache + 零拷贝,不是”全内存”
磁盘一定比内存慢很多慢的是随机访问;顺序 IO 下磁盘带宽逼近内存
broker JVM 堆越大越快恰恰相反,堆太大挤占 page cache;通常 ~6GB,内存留给 OS
Kafka 自己实现了高效缓存刻意不自建缓存,把缓存交给 OS page cache
加了 SSL 不影响性能SSL 破坏零拷贝,吞吐下降、CPU 上升,是明确取舍
索引是每条消息一条稀疏索引(每隔几 KB 一条)+ 短距离顺序扫
broker 会解压再压缩消息broker 原样存压缩批;全链路只压一次解一次
分区/段越多性能越好过多会打散顺序性、涨元数据与句柄、碎化刷盘
频繁 fsync 才安全持久性靠副本;频繁 fsync 反而破坏顺序刷盘、拖慢吞吐

13. 速查表

至此,Kafka 深挖系列凑齐了完整闭环:是什么(日志)→ 怎么写(Producer)→ 怎么读(Consumer)→ 怎么不丢不重(可靠性/EOS)→ 凭什么这么快(性能内核)。把这五篇的主线连起来——顺序日志 + 分区并行 + 客户端管状态 + 副本保可靠 + 页缓存零拷贝保性能——你就握住了推导 Kafka 几乎所有行为与调优方向的完整地图。


延伸阅读

本系列内部串读:

一手资料与优质教程:


views
Share this post on:

Previous Post
Kafka 可靠性与 Exactly-Once 深挖:acks、ISR、min.insync.replicas 与事务,把『不丢不重』讲到底
Next Post
Kafka Consumer 深挖:poll 循环、offset 提交与 rebalance,怎么做到不丢不重不卡顿