跳转到内容

Kafka 底层原理


Kafka 单机可以达到百万级 TPS,远超大多数消息队列,背后依赖四个核心机制的协同:

写入路径:Producer ──批量发送──▶ PageCache ──顺序写──▶ 磁盘
读取路径:Consumer ──▶ PageCache ──零拷贝──▶ 网卡 ──▶ Consumer

机械磁盘随机写速度约 100 次/秒,但顺序写可以达到 600MB/s 以上,接近内存速度。Kafka 的每个 Partition 对应一组追加写的日志文件,消息只追加到文件末尾,不做随机插入或修改。

Partition 日志文件(只追加,不修改):
[msg0][msg1][msg2][msg3] ──追加──▶ [msg4][msg5]...
新消息永远写在末尾

Kafka 不维护自己的内存缓存,而是完全依赖操作系统的 PageCache(页缓存)。写入时,消息先写入 PageCache,由操作系统异步刷盘;读取时,若消息仍在 PageCache 中,直接从内存返回,无需读磁盘。

写入:Producer ──▶ PageCache(内存)──异步──▶ 磁盘
读取:Consumer ◀── PageCache(命中则不读磁盘)

优势:

  • Kafka 进程重启不影响 PageCache,缓存不会丢失(操作系统管理);
  • 避免了 JVM 堆内存的 GC 压力;
  • 充分利用操作系统的预读(Read-ahead)机制,顺序读性能极高。

传统文件传输需要 4 次数据拷贝

传统方式:
磁盘 ──(DMA)──▶ 内核缓冲区 ──(CPU)──▶ 用户空间 ──(CPU)──▶ Socket 缓冲区 ──(DMA)──▶ 网卡

Kafka 使用 sendfile 系统调用,实现零拷贝,只需 2 次拷贝

零拷贝:
磁盘 ──(DMA)──▶ 内核缓冲区 ──(DMA)──▶ 网卡
数据不经过用户空间,CPU 几乎不参与

这使得 Kafka 在消费者追上生产速度时(消息仍在 PageCache),可以以接近网卡带宽的速度传输数据。

生产者不是每条消息单独发送,而是将多条消息攒批后一起发送,减少网络请求次数:

spring:
kafka:
producer:
batch-size: 16384 # 批次大小上限(16KB)
linger-ms: 5 # 最长等待时间(ms),时间到了不管批次是否满都发送
compression-type: lz4 # 压缩算法:none / gzip / snappy / lz4 / zstd

批量 + 压缩的效果:

配置网络传输量延迟
无批量无压缩100%最低
批量不压缩100%略高
批量 + lz4 压缩~30%略高

每个 Partition 在磁盘上对应一个目录,目录下包含多个 Segment(段文件),每个 Segment 由三个文件组成:

/kafka-logs/order.events-0/ ← Partition 0 的目录
├── 00000000000000000000.log ← 数据文件(消息内容)
├── 00000000000000000000.index ← 稀疏索引(offset → 文件位置)
├── 00000000000000000000.timeindex ← 时间索引(timestamp → offset)
├── 00000000000000001000.log ← 第二个 Segment(从 offset=1000 开始)
├── 00000000000000001000.index
└── 00000000000000001000.timeindex

文件命名规则:文件名是该 Segment 第一条消息的 base offset,固定 20 位,不足补零。

.log 文件(数据文件):存储实际消息,每条消息包含:

┌────────────────────────────────────────────────────────────┐
│ offset(8B) │ size(4B) │ CRC(4B) │ attributes │ key │ value │
└────────────────────────────────────────────────────────────┘

.index 文件(稀疏偏移量索引):不是每条消息都建索引,而是每隔一定字节数记录一个索引项:

索引项格式:相对 offset(4B) + 文件物理位置(4B)
示例:
相对offset=0 → 物理位置=0
相对offset=100 → 物理位置=4096
相对offset=200 → 物理位置=8192

.timeindex 文件(时间索引):记录时间戳与 offset 的映射,支持按时间查找消息。

通过 offset 查找消息分两步:

Step 1:二分查找定位 Segment
目标 offset=1500
Segment 列表:[0, 1000, 2000, ...]
→ 定位到 00000000000000001000.log(1000 ≤ 1500 < 2000)
Step 2:在 .index 中二分查找,定位到物理位置
相对 offset = 1500 - 1000 = 500
在 .index 中找到最近的索引项,如 offset=480 → position=20480
→ 从 position=20480 开始顺序扫描 .log 文件,找到 offset=500 的消息

这种稀疏索引 + 顺序扫描的设计,在索引文件极小的前提下实现了 O(log n) 的查找效率。

当 Consumer Group 内的消费者数量或订阅 Topic 的分区发生变化时,Kafka 需要重新分配各消费者负责的 Partition,这个过程称为 Rebalance(再均衡)

触发条件:

  • 消费者加入或离开 Consumer Group(启动、关闭、崩溃)
  • Topic 的分区数发生变化
  • 消费者订阅的 Topic 发生变化

Rebalance 期间,所有消费者停止消费,直到重新分配完成,类似于”Stop the World”:

正常消费:Consumer1(P0) Consumer2(P1) Consumer3(P2)
↓ Consumer2 下线
触发 Rebalance:所有消费者暂停消费
重新分配:Consumer1(P0,P1) Consumer3(P2)
恢复消费

Rebalance 时间从几百毫秒到几分钟不等,频繁 Rebalance 会严重影响消费吞吐量。

1. 合理配置心跳与会话超时

spring:
kafka:
consumer:
properties:
# 消费者向 Coordinator 发送心跳的间隔,应小于 session.timeout.ms 的 1/3
heartbeat.interval.ms: 3000
# Coordinator 超过此时间未收到心跳,认为消费者宕机,触发 Rebalance
session.timeout.ms: 10000
# 两次 poll() 调用的最大间隔,超时则认为消费者卡死,触发 Rebalance
max.poll.interval.ms: 300000

2. 避免消费者频繁启停(如滚动发布时使用优雅停机)

3. 使用 StickyAssignor 分配策略,Rebalance 时尽量保留原有分配关系,减少分区迁移数量:

spring:
kafka:
consumer:
properties:
partition.assignment.strategy: >
org.apache.kafka.clients.consumer.CooperativeStickyAssignor

4. 使用 CooperativeStickyAssignor(增量再均衡)

传统 Rebalance 是全量停止再重分配(EAGER 协议),CooperativeStickyAssignor 采用增量方式,只迁移需要变动的分区,未变动的分区继续消费,大幅减少停顿时间。

传统 EAGER 协议:
所有消费者放弃所有分区 → 重新分配 → 恢复消费(全部停顿)
增量 COOPERATIVE 协议:
只迁移需要变动的分区 → 其余分区持续消费(局部停顿)

为消费者设置固定的 group.instance.id,消费者重启后 Kafka 会识别为同一成员,在 session.timeout.ms 内不触发 Rebalance:

spring:
kafka:
consumer:
properties:
group.instance.id: consumer-instance-1 # 每个实例唯一
session.timeout.ms: 30000 # 给重启留出足够时间

适合滚动发布、短暂重启等场景。

┌──────────────────────────────────────────────────────────────┐
│ 高吞吐量来源 │
│ │
│ Producer Broker Consumer │
│ ┌────────┐ 批量+压缩 ┌──────────────┐ 零拷贝 │
│ │ 消息攒批 │ ──────────▶ │ PageCache │ ──────────▶ 网卡 │
│ └────────┘ │ (操作系统) │ │
│ └──────┬───────┘ │
│ 顺序写 │ 异步刷盘 │
│ ┌──────▼───────┐ │
│ │ 磁盘 │ │
│ │ .log/.index │ │
│ └──────────────┘ │
└──────────────────────────────────────────────────────────────┘
机制解决的问题效果
顺序写磁盘磁盘随机写慢写速度接近内存
页缓存减少磁盘 I/O热数据直接从内存读
零拷贝数据传输 CPU 开销大减少 2 次拷贝,CPU 占用极低
批量发送网络请求次数多显著减少网络开销
压缩网络带宽占用高传输量减少 60%~70%
稀疏索引索引文件过大索引小,查找 O(log n)
CooperativeStickyRebalance 停顿时间长局部迁移,减少停顿