Kafka 底层原理
1. 高吞吐量的实现原理
Section titled “1. 高吞吐量的实现原理”Kafka 单机可以达到百万级 TPS,远超大多数消息队列,背后依赖四个核心机制的协同:
写入路径:Producer ──批量发送──▶ PageCache ──顺序写──▶ 磁盘读取路径:Consumer ──▶ PageCache ──零拷贝──▶ 网卡 ──▶ Consumer1.1. 顺序写磁盘
Section titled “1.1. 顺序写磁盘”机械磁盘随机写速度约 100 次/秒,但顺序写可以达到 600MB/s 以上,接近内存速度。Kafka 的每个 Partition 对应一组追加写的日志文件,消息只追加到文件末尾,不做随机插入或修改。
Partition 日志文件(只追加,不修改):[msg0][msg1][msg2][msg3] ──追加──▶ [msg4][msg5]... ↑ 新消息永远写在末尾1.2. 页缓存(PageCache)
Section titled “1.2. 页缓存(PageCache)”Kafka 不维护自己的内存缓存,而是完全依赖操作系统的 PageCache(页缓存)。写入时,消息先写入 PageCache,由操作系统异步刷盘;读取时,若消息仍在 PageCache 中,直接从内存返回,无需读磁盘。
写入:Producer ──▶ PageCache(内存)──异步──▶ 磁盘 ↑读取:Consumer ◀── PageCache(命中则不读磁盘)优势:
- Kafka 进程重启不影响 PageCache,缓存不会丢失(操作系统管理);
- 避免了 JVM 堆内存的 GC 压力;
- 充分利用操作系统的预读(Read-ahead)机制,顺序读性能极高。
1.3. 零拷贝(Zero Copy)
Section titled “1.3. 零拷贝(Zero Copy)”传统文件传输需要 4 次数据拷贝:
传统方式:磁盘 ──(DMA)──▶ 内核缓冲区 ──(CPU)──▶ 用户空间 ──(CPU)──▶ Socket 缓冲区 ──(DMA)──▶ 网卡Kafka 使用 sendfile 系统调用,实现零拷贝,只需 2 次拷贝:
零拷贝:磁盘 ──(DMA)──▶ 内核缓冲区 ──(DMA)──▶ 网卡 ↑ 数据不经过用户空间,CPU 几乎不参与这使得 Kafka 在消费者追上生产速度时(消息仍在 PageCache),可以以接近网卡带宽的速度传输数据。
1.4. 批量发送与压缩
Section titled “1.4. 批量发送与压缩”生产者不是每条消息单独发送,而是将多条消息攒批后一起发送,减少网络请求次数:
spring: kafka: producer: batch-size: 16384 # 批次大小上限(16KB) linger-ms: 5 # 最长等待时间(ms),时间到了不管批次是否满都发送 compression-type: lz4 # 压缩算法:none / gzip / snappy / lz4 / zstd批量 + 压缩的效果:
| 配置 | 网络传输量 | 延迟 |
|---|---|---|
| 无批量无压缩 | 100% | 最低 |
| 批量不压缩 | 100% | 略高 |
| 批量 + lz4 压缩 | ~30% | 略高 |
2. Kafka 存储结构
Section titled “2. Kafka 存储结构”2.1. Partition 的文件组织
Section titled “2.1. Partition 的文件组织”每个 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 位,不足补零。
2.2. Segment 文件
Section titled “2.2. Segment 文件”.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 的映射,支持按时间查找消息。
2.3. 消息查找过程
Section titled “2.3. 消息查找过程”通过 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) 的查找效率。
3. 分区再均衡(Rebalance)
Section titled “3. 分区再均衡(Rebalance)”3.1. 什么是 Rebalance
Section titled “3.1. 什么是 Rebalance”当 Consumer Group 内的消费者数量或订阅 Topic 的分区发生变化时,Kafka 需要重新分配各消费者负责的 Partition,这个过程称为 Rebalance(再均衡)。
触发条件:
- 消费者加入或离开 Consumer Group(启动、关闭、崩溃)
- Topic 的分区数发生变化
- 消费者订阅的 Topic 发生变化
3.2. Rebalance 的问题
Section titled “3.2. Rebalance 的问题”Rebalance 期间,所有消费者停止消费,直到重新分配完成,类似于”Stop the World”:
正常消费:Consumer1(P0) Consumer2(P1) Consumer3(P2) ↓ Consumer2 下线触发 Rebalance:所有消费者暂停消费重新分配:Consumer1(P0,P1) Consumer3(P2)恢复消费Rebalance 时间从几百毫秒到几分钟不等,频繁 Rebalance 会严重影响消费吞吐量。
3.3. 减少 Rebalance 的方法
Section titled “3.3. 减少 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: 3000002. 避免消费者频繁启停(如滚动发布时使用优雅停机)
3. 使用 StickyAssignor 分配策略,Rebalance 时尽量保留原有分配关系,减少分区迁移数量:
spring: kafka: consumer: properties: partition.assignment.strategy: > org.apache.kafka.clients.consumer.CooperativeStickyAssignor4. 使用 CooperativeStickyAssignor(增量再均衡)
传统 Rebalance 是全量停止再重分配(EAGER 协议),CooperativeStickyAssignor 采用增量方式,只迁移需要变动的分区,未变动的分区继续消费,大幅减少停顿时间。
传统 EAGER 协议:所有消费者放弃所有分区 → 重新分配 → 恢复消费(全部停顿)
增量 COOPERATIVE 协议:只迁移需要变动的分区 → 其余分区持续消费(局部停顿)3.4. 静态成员(Static Membership)
Section titled “3.4. 静态成员(Static Membership)”为消费者设置固定的 group.instance.id,消费者重启后 Kafka 会识别为同一成员,在 session.timeout.ms 内不触发 Rebalance:
spring: kafka: consumer: properties: group.instance.id: consumer-instance-1 # 每个实例唯一 session.timeout.ms: 30000 # 给重启留出足够时间适合滚动发布、短暂重启等场景。
4. 底层原理总览
Section titled “4. 底层原理总览”┌──────────────────────────────────────────────────────────────┐│ 高吞吐量来源 ││ ││ Producer Broker Consumer ││ ┌────────┐ 批量+压缩 ┌──────────────┐ 零拷贝 ││ │ 消息攒批 │ ──────────▶ │ PageCache │ ──────────▶ 网卡 ││ └────────┘ │ (操作系统) │ ││ └──────┬───────┘ ││ 顺序写 │ 异步刷盘 ││ ┌──────▼───────┐ ││ │ 磁盘 │ ││ │ .log/.index │ ││ └──────────────┘ │└──────────────────────────────────────────────────────────────┘| 机制 | 解决的问题 | 效果 |
|---|---|---|
| 顺序写磁盘 | 磁盘随机写慢 | 写速度接近内存 |
| 页缓存 | 减少磁盘 I/O | 热数据直接从内存读 |
| 零拷贝 | 数据传输 CPU 开销大 | 减少 2 次拷贝,CPU 占用极低 |
| 批量发送 | 网络请求次数多 | 显著减少网络开销 |
| 压缩 | 网络带宽占用高 | 传输量减少 60%~70% |
| 稀疏索引 | 索引文件过大 | 索引小,查找 O(log n) |
| CooperativeSticky | Rebalance 停顿时间长 | 局部迁移,减少停顿 |