系列前四篇讲了 Kafka 是什么、怎么写、怎么读、怎么不丢:核心原理、Producer、Consumer、可靠性与 EOS。最后一块拼图是性能:
Kafka 用的是最普通的磁盘,为什么能跑出百万级吞吐、甚至比很多”内存”系统还快?
答案不是什么黑魔法,而是四个朴素到极致的选择:顺序写、页缓存、零拷贝、日志分段,再叠加批量与压缩。这篇把它们逐个拆开——理解了它们,你也就理解了核心原理篇 §11 那句”把日志顺序化、把状态客户端化、把并行分区化”里”顺序化”到底快在哪。
TL;DR
- 顺序写:Kafka 只做 append-only 顺序追加,把”随机 IO”变成”顺序 IO”——同一块磁盘,两者能差几个数量级,顺序写 HDD 也能上百 MB/s。
- 页缓存(page cache):Kafka 不自建应用层缓存,把内存交给 OS page cache。避免 JVM 堆内缓存的 GC 与对象开销、避免双重缓存、重启后缓存还在;且”刚写的数据正是马上要读的”→ 追尾消费几乎全命中内存。
- 零拷贝(zero-copy /
sendfile):消费者拉数据时,把段文件从 page cache 直送网卡,不经用户态——从”4 次拷贝 + 4 次上下文切换”降到极少几次。SSL 加密会破坏零拷贝。 - 日志分段(log segment)+ 稀疏索引:分区日志切成多个段文件(
.log/.index/.timeindex),保留/删除以整段为单位,查找靠”文件名二分 + 索引二分 + 顺序扫”;索引稀疏 +mmap,又快又省内存。 - 批量 + 压缩:Producer 攒批、Broker 原样存、Consumer 原样取;压缩(lz4/zstd/snappy/gzip)端到端只压/解一次,批越大压缩比越高。
- 快是”串起来”的:produce → 顺序写 page cache → 复制 → consume 零拷贝,全程避免随机 IO 与用户态拷贝。
- 会变慢的时候:page cache 被挤占、消费者读冷数据(回溯历史触发磁盘随机读)、段/分区过多、开 SSL、频繁强制 flush。
Table of contents
Open Table of contents
1. 一个反直觉的事实:磁盘不一定慢
工程师的直觉是”磁盘慢、内存快,所以高性能系统要把数据放内存”。Kafka 偏偏反着来——数据全落磁盘,却比很多内存系统还快。破解这个悖论的钥匙是一句被误解的话:
“磁盘慢”慢的是随机访问,不是顺序访问。 在顺序读写下,磁盘的带宽可以逼近内存,还便宜好几个数量级。
Kafka 的全部性能设计,都是围绕”把一切访问都变成顺序的、把一切拷贝都省掉”。下面四节就是这四板斧。
2. 第一板斧:顺序写
核心原理篇 §1 说 Kafka 的内核是一条 append-only 日志——这不只是数据模型上的优雅,更是性能上的第一杀招。
传统数据库/MQ 常要更新任意位置的记录(随机写),磁头/寻址不停跳动,寻道与旋转延迟成为瓶颈。Kafka 因为只追加不修改:
- 写永远落在当前段文件的末尾,磁头几乎不动 → 接近磁盘顺序带宽;
- 没有”就地更新""删除单条”这些需要随机 IO 的操作(删除以整段为单位,见 §5)。
这就是为什么核心原理篇 §11 把”只能追加、不支持随机改”列为一项取舍:Kafka 主动放弃了随机修改能力,换来了顺序写的极致吞吐。数据模型的约束,直接变成了性能红利。
3. 第二板斧:页缓存(把内存交给 OS)
拿到顺序写还不够。Kafka 的第二个聪明之处是:它几乎不自己管缓存,而是把内存全交给操作系统的 page cache。
写入路径:消息先写进 page cache(OS 内存里的文件缓存),由 OS 按策略异步刷盘——所以”写”几乎是内存速度,落盘在后台顺序进行。读取路径:消费者读数据时,热数据直接命中 page cache,根本不碰磁盘。
为什么不用 JVM 堆内缓存?因为 page cache 有四个堆内缓存给不了的好处:
- 躲开 GC:几十上百 GB 的数据若放 JVM 堆,GC 会被拖垮;page cache 在 OS 管理的堆外内存,与 GC 无关。
- 不双重缓存:数据本来就要经过 page cache 落盘,若应用再缓存一份,就是”page cache + 堆内”两份,白白浪费内存。
- 重启不失效:page cache 属于 OS,Kafka 进程重启,缓存仍在——重启后消费不会全部冷读。
- 天然高命中:Kafka 的访问模式是”刚写的数据马上被读”(追尾消费),这正是 page cache 最擅长的局部性。
一个推论:别给 Kafka 的 JVM 堆配太大(通常 6GB 左右足矣),把物理内存尽量留给 page cache。这和调优其他 Java 服务”堆越大越好”的直觉恰好相反——Kafka 的性能主要靠 OS 内存,不靠 JVM 堆。
4. 第三板斧:零拷贝(sendfile)
消费者拉数据时,broker 要把磁盘/page cache 里的段文件发到网络。传统做法要在用户态和内核态之间来回拷贝好几趟,纯属浪费。
Kafka 用操作系统的 sendfile 系统调用(Java 里是 FileChannel.transferTo):把段文件的数据从 page cache 直接送到网卡,中间不经过用户态缓冲区。数据”过一次内核”就走了,CPU 拷贝和上下文切换大幅减少。
这也是”read 不删除、按 offset 顺序读”和零拷贝的绝配:因为消费者读的就是段文件里连续的一段字节,Kafka 可以原样
sendfile出去,不需要在用户态反序列化/重组。 数据模型(连续日志)再一次直接喂给了性能优化。
一个重要的实战注意:
开启 SSL/TLS 会破坏零拷贝。 因为加密必须在用户态进行,数据不得不被拷回用户态加密再发出——零拷贝失效,吞吐下降、CPU 上升。所以”要不要给 broker 间/客户端加 TLS”是一个明确的性能取舍,不是免费的。
5. 第四板斧:日志分段与稀疏索引
一个分区的日志不是一个巨型文件,而是切成许多段文件(log segment)。这既是核心原理篇 §8 存储机制的物理基础,也是查找与保留高效的关键。
每个段由三个文件组成:
| 文件 | 内容 |
|---|---|
.log | 实际消息数据(一批批顺序追加的 record batch) |
.index | 稀疏的 offset 索引:相对 offset → 文件内物理位置(每隔几 KB 记一条,不是每条都记) |
.timeindex | 时间戳索引:timestamp → offset(支持按时间定位,如 offsetsForTimes) |
按 offset 定位一条消息的过程是两级二分 + 顺序扫:
- 段文件名就是该段的基准 offset,用它二分定位目标 offset 落在哪个段;
- 在该段的
.index里二分,找到不超过目标 offset 的最近索引项(物理位置); - 从那个物理位置开始顺序扫到目标记录。
为什么索引是稀疏的?因为”每条都建索引”太占空间,而”稀疏索引 + 短距离顺序扫”在有 page cache 的前提下几乎一样快。索引文件还通过
mmap内存映射访问——又省内存又快。分段 + 稀疏索引,是”既要能按 offset 随机定位、又要保持顺序 IO 优势”的精妙折中。
段还让保留策略变得廉价:删除过期数据(retention)或压缩(compact)都以整段为单位操作,从不去改动正在写的 active segment——顺序写和只读段的零拷贝因此都始终成立。
6. 加成项:批量与压缩
前面四板斧是”IO 路径”上的优化,还有一层是”传输的数据量”上的优化。
- 批量(batch):Producer 篇 讲透了攒批——它不只降延迟,也让每次系统调用/网络往返摊薄到更多消息上,是吞吐的乘数。
- 压缩(compression):Producer 端把一个 batch 整体压缩(
compression.type=lz4/zstd/snappy/gzip),broker 原样存储压缩后的批、消费者原样取回再解压——全链路只压一次、解一次,broker 不做重复的解压/压缩。
两个协同效应值得记住:①批越大,压缩比通常越高(更多相似数据可压),所以
linger.ms/batch.size调大既增吞吐又省带宽;②压缩省的是网络和磁盘带宽,代价是两端 CPU——zstd/lz4在压缩比与 CPU 间平衡最好,是当下主流。broker 原样存压缩批,也正是零拷贝(§4)能成立的前提之一。
7. 把”快”串起来:一条消息的高速公路
单独看四板斧还不够直观,把它们串成一条消息的全生命周期就清楚了:
- Producer:多条消息攒成 batch、压缩 → 一次网络请求发出(批量)。
- Broker 写入:batch 原样顺序追加到 active segment,先落 page cache(近内存速度),异步刷盘。
- 复制:follower 从 leader 顺序拉取并同样顺序写——复制路径也是顺序 IO。
- 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.ms | Producer | 攒批大小/等待,越大吞吐与压缩比越高(延迟换吞吐) |
compression.type | Producer | lz4 / zstd 主流,省带宽换 CPU |
log.segment.bytes | Broker/Topic | 单个段文件大小,影响保留粒度与文件数 |
log.index.interval.bytes | Broker/Topic | 稀疏索引的间隔(每多少字节记一条索引) |
log.flush.interval.messages/ms | Broker/Topic | 强制刷盘阈值;通常不设,靠副本保证持久 |
num.replica.fetchers | Broker | follower 复制并行度,影响复制吞吐 |
JVM 堆大小(KAFKA_HEAP_OPTS) | Broker | 通常 ~6GB,把内存留给 page cache |
socket.request.max.bytes / fetch.max.bytes | Broker/Consumer | 单请求/单次拉取的字节上限 |
10. 落到 AdTech:为什么”Netty + Kafka”能扛事件洪峰
核心原理篇 §13 提过高并发广告服务的经典栈 Netty + Kafka + Redis + Flink。现在能说清 Kafka 在其中”抗洪峰”的底气到底在哪:
- 写入抗洪峰:曝光/点击/转化在高峰期是无 key 的高吞吐洪流(Producer 篇 §10),Kafka 靠顺序写 + page cache + 批量压缩,把这股洪流以近内存速度吸下来、异步落盘——上游 Netty 接入层不会被下游处理速度拖垮。
- 多方消费不额外扛压:计费、特征、风控多条链路各成一组消费同一份日志,靠零拷贝几乎零成本地把同一批数据分发给多个消费者——“一份数据喂多方”不会线性放大 broker 的 CPU。
- 削峰解耦:洪峰先顺序落 Kafka,下游按自己节奏拉(pull 模型,核心原理篇 §5.1),Kafka 的顺序写吞吐远高于下游处理速度,天然是那个”定盘星缓冲区”。
一句话:广告事件洪峰之所以能被 Kafka 稳稳接住,靠的正是这篇讲的四板斧——顺序写吸洪流、page cache 供热读、零拷贝喂多方、分段索引管海量留存。
11. 生产反模式与踩坑
- 给 Kafka broker 配超大 JVM 堆:挤占 page cache、放大 GC。对策:堆 ~6GB,内存留给 OS。
- 无脑上 SSL 又追求极致吞吐:零拷贝失效还纳闷为什么慢。对策:认识到这是取舍,按需加机器。
- 大量消费者频繁回溯冷读:把热路径的 page cache 冲垮,拖慢所有人。对策:重放错峰/限速,或用分层存储/独立集群。
- 分区/段开太多:文件句柄、元数据、刷盘碎片化。对策:按吞吐反推分区数。
- 靠频繁 fsync 求”安全”:破坏异步顺序刷盘。对策:持久性靠副本(可靠性篇),不靠频繁刷盘。
- 不开压缩发大消息:带宽先打满。对策:
lz4/zstd+ 合理batch.size。
12. 常见误解 ↔ 正解
| 常见误解 | 正解 |
|---|---|
| Kafka 快是因为把数据放内存 | 数据落磁盘;快来自顺序写 + page cache + 零拷贝,不是”全内存” |
| 磁盘一定比内存慢很多 | 慢的是随机访问;顺序 IO 下磁盘带宽逼近内存 |
| broker JVM 堆越大越快 | 恰恰相反,堆太大挤占 page cache;通常 ~6GB,内存留给 OS |
| Kafka 自己实现了高效缓存 | 它刻意不自建缓存,把缓存交给 OS page cache |
| 加了 SSL 不影响性能 | SSL 破坏零拷贝,吞吐下降、CPU 上升,是明确取舍 |
| 索引是每条消息一条 | 是稀疏索引(每隔几 KB 一条)+ 短距离顺序扫 |
| broker 会解压再压缩消息 | broker 原样存压缩批;全链路只压一次解一次 |
| 分区/段越多性能越好 | 过多会打散顺序性、涨元数据与句柄、碎化刷盘 |
| 频繁 fsync 才安全 | 持久性靠副本;频繁 fsync 反而破坏顺序刷盘、拖慢吞吐 |
13. 速查表
- 四板斧:顺序写(随机 IO → 顺序追加)、页缓存(内存交给 OS)、零拷贝(
sendfile直送网卡)、日志分段 + 稀疏索引。 - 顺序写:只追加不改,接近磁盘线速;随机改能力换来的性能红利。
- 页缓存:躲 GC、不双重缓存、重启不失效、追尾高命中;别给 broker 配大 JVM 堆。
- 零拷贝:page cache → NIC 不经用户态;SSL 会破坏它。
- 分段索引:
.log/.index/.timeindex;文件名二分 + 稀疏索引二分 + 顺序扫;mmap;保留/压缩以整段为单位。 - 批量 + 压缩:批越大压缩比越高;broker 原样存压缩批(配合零拷贝);主流
lz4/zstd。 - 变慢信号:冷读、内存不足、SSL、段/分区过多、频繁 fsync。
至此,Kafka 深挖系列凑齐了完整闭环:是什么(日志)→ 怎么写(Producer)→ 怎么读(Consumer)→ 怎么不丢不重(可靠性/EOS)→ 凭什么这么快(性能内核)。把这五篇的主线连起来——顺序日志 + 分区并行 + 客户端管状态 + 副本保可靠 + 页缓存零拷贝保性能——你就握住了推导 Kafka 几乎所有行为与调优方向的完整地图。
延伸阅读
本系列内部串读:
- Kafka 核心原理精讲:从一条日志到分布式流平台:日志内核、存储分段、设计哲学的地基。
- Kafka Producer 深挖:分区策略与 Sticky Partitioner:批量与压缩在写入侧的杠杆。
- Kafka Consumer 深挖:poll 循环、offset 提交与 rebalance:追尾消费、冷读与 page cache 命中的关系。
- Kafka 可靠性与 Exactly-Once 深挖:为什么持久性靠副本而不靠频繁 fsync。
- 为什么 AdTech 偏爱 Kafka:广告事件中枢的五大典型场景与架构设计:顺序写、page cache、零拷贝怎么在广告事件洪峰里稳稳兜住。
一手资料与优质教程:
- Apache Kafka. Documentation — Design: Persistence & Efficiency:顺序 IO、page cache、零拷贝、批量与压缩的官方设计章节。
- Jay Kreps. The Log: What every software engineer should know about real-time data’s unifying abstraction:日志抽象与顺序化思想的源头。
- Confluent. Kafka Performance:吞吐/延迟调优与性能内核的系统讲解。
- Martin Kleppmann. Designing Data-Intensive Applications(第 3 章,日志结构存储):顺序写与日志结构存储引擎的通用原理。