跳至正文
来两杯美式
返回

Kafka(中):吞吐量为什么这么大——日志、零拷贝、批量

By 来两杯美式
发布于更新于

上一篇讲完 Kafka 的定位和消息语义,本篇进入最硬核的部分——Kafka 凭什么能扛十万级 TPS。这一篇是面试高频题,也是 Kafka 设计哲学的精华所在。

一、吞吐量大 = 消息一入一出快

吞吐量大,本质就是消息的一入一出更快。Kafka 消息的一入一出,都做了哪些操作?

  1. 读取消息内容
  2. 将消息持久化到磁盘
  3. 消费者从磁盘读取消息消费

注意中间这一步——Kafka 是要把消息写到磁盘的。按直觉,写磁盘怎么会比 RabbitMQ 的纯内存快?答案就藏在「怎么写磁盘」上。

二、四大吞吐量支柱

支柱关键技术
顺序读写日志追加 + 磁盘顺序 IO
并行partition 机制
批量批量发送 / 接收 + 数据压缩
零拷贝sendfile(transferTo)

下面逐个拆。

三、日志顺序读写与快速检索

3.1 Kafka 日志的物理结构

Kafka 日志以 partition 为单位保存:

/order-events-0/
   ├── 000000000000000170410.index    ← 索引文件
   ├── 000000000000000170410.log      ← 数据文件
   ├── 000000000000000170420.index
   └── 000000000000000170420.log

3.2 日志分段(Segment)

日志是分段的——每个 partition 日志会分成 N 个大小相等的 segment:

顺序写的恐怖效率:在 SSD 上,磁盘顺序写可达 几百 MB/s,几乎与内存随机写持平。Kafka 把所有写入都做成顺序追加,是它高吞吐的地基

3.3 index + log 双文件 + 稀疏索引

每个 partition 日志文件由两个文件构成:index 文件和 log 数据文件。

Kafka segment 的 index/log 结构:稀疏索引 + 二分查找

index 文件存的是 offset → 物理位置(position)的映射:

相对 offsetposition
10
3348
4476
81325

注意是稀疏索引——并不是每条消息一条索引,而是每隔若干条消息记录一次

3.4 检索流程

index 前半部分是 partition 的 offset,根据 offset 从 index 中获取日志存储的 position;position 主要包含「消息头 + 消息内容长度」;定位到 position 后直接去 log 文件,根据消息头长度读取一条完整消息。

完整流程:

  1. 二分查找 index:定位到目标 offset 所在的区间
  2. 从 position 开始顺序扫描 log:找到精确的目标消息
查 offset 170415:
  index 二分 → 找到 (4, 476) 这一项最接近
  从 log position 476 开始顺序扫
    → Message170414 (476)
    → Message170415 (789) ✅ 命中

为什么用稀疏索引而不是稠密索引? 稠密索引会让 index 文件和 log 一样大,内存扛不住。稀疏索引牺牲一点点查询时间(多扫几条),换来 index 可以常驻内存——这是 Kafka 在「空间 vs 时间」上的精妙取舍。

四、partition 并行

partition 是 Kafka 的并行单元

Topic (6 partitions)         Consumer Group (3 consumers)
┌──────────────┐             ┌──────────────┐
│ Partition 0  │ ─────────►  │ Consumer 1   │ (消费 0, 1)
│ Partition 1  │ ─────────►  │              │
├──────────────┤             ├──────────────┤
│ Partition 2  │ ─────────►  │ Consumer 2   │ (消费 2, 3)
│ Partition 3  │ ─────────►  │              │
├──────────────┤             ├──────────────┤
│ Partition 4  │ ─────────►  │ Consumer 3   │ (消费 4, 5)
│ Partition 5  │ ─────────►  │              │
└──────────────┘             └──────────────┘

partition 数 = 并行度上限。设计 topic 时要预估消费端需要的并发数——partition 设少了,加 consumer 也没用(多余的 consumer 闲置)。

五、批量发送 + 数据压缩

Kafka Producer 不是「来一条发一条」,而是攒一批一起发

传统 MQ:1 msg = 1 次网络 IO
   msg ──► ────────► broker

Kafka:batch = 1 次网络 IO
   msg1 ┐
   msg2 ├─ batch(压缩)──► broker
   msg3 ┘

批量的双重红利:不仅网络 IO 次数减少,压缩后网络传输字节数也大幅下降。在大数据场景下,批量 + 压缩常常让 Kafka 的实际网络流量降一个数量级。

六、零拷贝(sendfile)

这是 Kafka 性能优化中最精彩的一笔。

6.1 标准文件读取的 4 次拷贝

正常情况下,Linux 的内核空间分用户进程系统进程。Kafka 读取一个文件并发送到网络,标准方式要走 4 步

传统文件发送:4 次拷贝、4 次上下文切换

  1. 磁盘 → 内核读取缓冲区(DMA 拷贝)
  2. 内核读取缓冲区 → 用户缓冲区(CPU 拷贝,进入应用上下文)
  3. 用户缓冲区 → socket 缓冲区(CPU 拷贝)
  4. socket 缓冲区 → 网卡接口(DMA 拷贝)
应用上下文    内核空间上下文
  │              │
  │         ┌────┴─────┐
  │         │ 磁盘文件  │
  │         └────┬─────┘
  │              │  ① DMA
  │         ┌────┴─────┐
  │         │ 内核读缓冲│
  │         └────┬─────┘
  │              │  ② CPU
  ┌──────────────┴┐
  │ 用户缓冲区     │
  └──────────────┬┘
  │              │  ③ CPU
  │         ┌────┴─────┐
  │         │ socket 缓冲│
  │         └────┬─────┘
  │              │  ④ DMA
  │         ┌────┴─────┐
  │         │ 网卡接口  │ ──► 消费者
  │         └──────────┘

期间还需要 4 次上下文切换(用户态 ↔ 内核态)。这个流程经过多次数据拷贝,CPU 也参与了 2 次搬运。

6.2 sendfile 的 2 次拷贝

Kafka 没有使用标准的 JVM 文件读取,而是使用了 sendfile 的方式:

sendfile 零拷贝:2 步、2 次上下文切换

sendfile 提供了一个 transferTo() 函数:

把上述 4 步缩减为 2 步

步骤 ①:磁盘 → 内核读取缓冲区(DMA 拷贝)
步骤 ②:内核读取缓冲区 → 网卡接口(DMA 拷贝)

避免了用户进程和系统内核的上下文切换(只剩 sendfile 调用与返回的 2 次切换)。

应用上下文    内核空间上下文
  │              │
  │  transferTo()│
  │ ───────────► │
  │              │
  │         ┌────┴─────┐
  │         │ 磁盘文件  │
  │         └────┬─────┘
  │              │  ① DMA
  │         ┌────┴─────┐
  │         │ 内核读缓冲│
  │         └────┬─────┘
  │              │  ② DMA(不经用户空间)
  │         ┌────┴─────┐
  │         │ 网卡接口  │ ──► 消费者
  │         └──────────┘
  │ ◄─────────── │  ack

6.3 为什么不用在 Windows 上

sendfile 是 Linux 系统实现的底层功能,Windows 系统并未实现——这也是不建议把 Kafka 服务部署到 Windows 上的根本原因。

现代视角(2026)补一句:现代 Linux(内核 4.x+)的 sendfile 配合 DMA gather copy,连「内核读缓冲区 → 网卡」这一步 CPU 拷贝都能省掉,只剩纯 DMA 传输——CPU 几乎不参与数据搬运。Kafka 的零拷贝红利在新内核上更明显。

七、四大支柱总结

优化解决的瓶颈节省的开销
顺序读写磁盘随机 IO 慢让磁盘 IO 接近内存
partition 并行单队列串行并行度 = partition 数
批量 + 压缩网络 IO 次数多网络流量降一个数量级
零拷贝 sendfile内核↔用户空间拷贝 + 上下文切换4 次拷贝 → 2 次,CPU 解放

这四条单独拿出来都不新鲜——日志结构、并行、批量、零拷贝都是 OS 课的老知识点。Kafka 的伟大在于把它们组合到一个系统里、用到极致

八、和小白们的常见误解

误解真相
「Kafka 用内存所以快」❌ Kafka 写磁盘。快是因为顺序写
「partition 越多越好」❌ partition 越多,broker 元数据压力越大、客户端内存越多
「零拷贝 = 完全没拷贝」❌ 仍是 2 次拷贝,省的是用户↔内核之间的拷贝
「批量发送会拖慢延迟」❌ 配合 linger.ms=0 可以做到「立即发」

partition 不是越多越好:每个 partition 都要在 broker 上有元数据、在客户端有缓冲区。几千 partition 的集群,broker 的元数据管理就开始吃力——这是 Kafka 早期 ZK 模式的痛点,也是 KRaft 模式要解决的问题。

九、小结

Kafka 高吞吐的设计哲学:用「顺序」换「随机」、用「并行」换「串行」、用「批量」换「单条」、用「内核态」换「用户态」

章节关键词
日志结构segment + index + log
顺序读写磁盘顺序 IO ≈ 内存
稀疏索引二分 + 顺序扫描
partition并行单元
批量网络 IO 次数 ↓
零拷贝sendfile / transferTo

下一篇「Kafka(下):消费者、有序性、命令与集群」收尾——讲清 groupid 怎么用、为什么消息会重复、本地怎么起 Kafka、怎么搭集群、设计一个 MQ 该考虑什么。

现代视角补一句(2026):四大支柱到 2026 年一条没变。新增的优化是 Partition Tiered Storage——热数据放本地磁盘,冷数据自动下沉到 S3/OSS。这让 Kafka 能堆 TB 级历史数据而不用每个 broker 都扛全量。底层原理还是这一套


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
Kafka
第 2 / 3 篇
查看系列全部文章
  1. 01.Kafka(上):特性、应用场景与消息传递保障
  2. 02.Kafka(中):吞吐量为什么这么大——日志、零拷贝、批量
  3. 03.Kafka(下):消费者、消息有序性、命令与集群搭建

上一篇
Kafka(下):消费者、消息有序性、命令与集群搭建
下一篇
Kafka(上):特性、应用场景与消息传递保障