Kafka 为什么这么快?
结论:Kafka 的快不是单点优化,而是六个设计叠在一起——分区顺序追加写、操作系统页缓存(Page Cache)、零拷贝(Zero-Copy)发送、批量发送与压缩、分区级并行、稀疏索引定位。它把"随机写 + 多次内存拷贝 + 逐条网络往返"这三类开销同时消掉了。
一、六项优化
| 优化 | 做法 | 省掉了什么 |
|---|---|---|
| 顺序写 | 每个分区只追加到日志段末尾 | 磁盘随机寻道,顺序写可接近磁盘带宽上限 |
| 页缓存 | 消息不存 JVM 堆,读写都走 OS Page Cache | 对象开销与 GC 停顿,热数据直接命中内存 |
| 零拷贝 | sendfile 把页缓存直接送到网卡缓冲区 | 内核态与用户态之间的两次数据拷贝 |
| 批量 + 压缩 | 按 batch.size / linger.ms 攒批,整批压缩 | 每条消息的协议头与网络往返 |
| 分区并行 | 一个 Topic 拆多个分区,分散到多台 Broker | 单机磁盘与网卡的瓶颈 |
| 稀疏索引 | .index 每 4KB 记一条,二分后顺序扫 | 全量索引的内存占用 |
二、零拷贝到底省了什么
传统 read + write(4 次拷贝、4 次上下文切换)
磁盘 ──DMA──> 内核页缓存 ──CPU──> 用户缓冲区 ──CPU──> Socket 缓冲区 ──DMA──> 网卡
sendfile(2 次拷贝、2 次上下文切换)
磁盘 ──DMA──> 内核页缓存 ──────────DMA──> 网卡
(仅传递描述符,不经过用户态)
Kafka 消费端读消息时,数据本来就在页缓存里,sendfile 让内核直接把页缓存内容交给网卡,应用层完全不碰消息体。这也是为什么 Kafka 的堆内存可以设得很小(6~8GB 足够),而机器内存越大缓存命中率越高。
注意:启用 TLS 后 sendfile 无法直接使用(需要用户态加密),零拷贝收益会打折扣,这是"上 TLS 后吞吐下降"的主要原因之一。
三、生产端的关键参数
# 攒批:batch.size 与 linger.ms 共同决定"多大、等多久"
batch.size=131072 # 128KB 一批
linger.ms=10 # 最多等 10ms,用延迟换吞吐
buffer.memory=67108864 # 发送缓冲区 64MB,满了会阻塞 send
compression.type=lz4 # 压缩算法,lz4 兼顾 CPU 与压缩比
acks=all
enable.idempotence=true
调优顺序建议:先确认 buffer.memory 够用(否则 send 阻塞或抛异常),再调 batch.size 与 linger.ms,最后选压缩算法。盲目把 linger.ms 调到 100ms 会让端到端延迟明显变差。
四、Broker 侧与硬件
num.io.threads=16 # 磁盘 IO 线程,建议不小于磁盘数
num.network.threads=8 # 网络线程
log.segment.bytes=1073741824 # 单日志段 1GB,切段后才可被删除
log.flush.interval.messages=10000
实践中的三条经验:
- 磁盘用顺序写友好的介质,RAID10 或本地 NVMe 都行,但不要把日志目录和系统盘混用;
- JVM 堆不要过大,堆越大 Full GC 越久,页缓存反而变少;
- 文件系统预留空间(
log.retention与磁盘水位),写满会直接导致 Broker 不可写。
五、消费端为什么也快
消费侧的吞吐同样靠"批量 + 零拷贝":
| 参数 | 默认值 | 调优方向 |
|---|---|---|
fetch.min.bytes | 1 | 调到 1MB 左右,避免没数据也频繁往返 |
fetch.max.bytes | 50MB | 大消息场景适当调大 |
max.partition.fetch.bytes | 1MB | 必须大于单条最大消息,否则永远拉不动 |
max.poll.records | 500 | 按单条处理耗时下调,防止处理超时 |
Broker 不主动推送,只在消费者请求时按批返回,并把位点维护交给客户端。这样 Broker 侧几乎无状态,扩容与故障恢复都更简单。
六、常见追问
- 顺序写为什么比随机写快这么多? 机械盘随机写每次都要寻道,顺序写几乎无寻道;即使是 SSD,顺序写也能减少写放大与 GC 压力。
- 零拷贝是不是就不经过内核了? 仍经过内核,只是不再把数据复制到用户态;DMA 负责磁盘到内存、内存到网卡的搬运。
- 批量发送会不会丢消息? 不会,但会增加延迟;
linger.ms=0时批很小,吞吐上不去,属于用延迟换吞吐的典型取舍。 - 分区越多越快吗? 到一定程度后反而变慢:分区多意味着更多文件句柄、更多副本同步任务,Controller 故障恢复也更慢。
小结
- Kafka 快的本质是"顺序 IO + 页缓存 + 零拷贝 + 批量",每一层都针对一类开销。
- 零拷贝依赖数据留在页缓存,堆内缓存反而会拖慢并增加 GC。
- 吞吐调优优先看攒批参数与压缩,其次才是分区与线程数。
- 分区数不是越多越好,超过消费者与磁盘的承载能力后收益为负。