Kafka 存储原理
概述
Kafka 的高吞吐源自存储设计:顺序写、页缓存、零拷贝、日志分段与稀疏索引。消息按分区顺序追加,配合紧凑索引定位,实现 O(1) 的读写。本文讲透 Kafka 存储层全部机制。
一、存储模型总览
1.1 存储单元
Topic → 多个 Partition
每个 Partition 一个日志目录(log dir)
目录内多个 Segment
每个 Segment = .log + .index + .timeindex目录结构示例:
/data/kafka/topic-0/
├── 00000000000000000000.log
├── 00000000000000000000.index
├── 00000000000000000000.timeindex
├── 00000000000000368769.log
├── 00000000000000368769.index
└── 00000000000000368769.timeindex1.2 关键设计
| 设计 | 好处 |
|---|---|
| 顺序追加 | 磁盘顺序写极快 |
| 分段存储 | 删除/过期简单 |
| 稀疏索引 | 内存占用小 |
| 页缓存 | 读写高性能 |
| 不可变 | 简化并发 |
二、日志分段(Segment)
2.1 分段规则
Segment 滚动条件:
达到大小上限(log.segment.bytes,默认 1GB)
达到时间上限(log.roll.ms)
达到索引上限
滚动后旧 Segment 只读
新消息写入新 Segment2.2 命名规则
命名:第一个消息的 baseOffset(19 位补零)
00000000000000368769.log
→ 该 Segment 第一条消息 offset=368769
全局 offset 映射:
offset / segment 大小 → 定位 Segment
再在 Segment 内查索引2.3 分段的好处
好处:
删除方便:直接删整个文件
恢复快:损坏只影响单段
索引小:按段索引
并发清理:不阻塞写入三、索引文件
3.1 稀疏索引
.index 文件:
记录 offset → 物理位置的映射
稀疏:每写入 log.index.interval.bytes(默认 4KB)
才插一条索引
查找:
二分查找最近的索引项
从该位置顺序扫到目标| 字段 | 含义 |
|---|---|
| relativeOffset | 相对 Segment 起始的偏移 |
| position | 物理文件位置 |
3.2 查找流程
定位消息步骤:
1. 目标 offset → 找到 Segment
2. 索引二分查 ≤ 目标的最大项
3. 从该项 position 顺序读取
4. 比较消息 offset 定位
复杂度:
索引查 ≈ O(log n)
顺序扫描差距小(稀疏 4KB)3.3 timeindex 时间索引
作用:按时间戳定位(消息时间查找)
应用:根据时间消费/回溯
结构与 .index 类似
(timestamp → offset)四、Compaction 日志压缩
4.1 两种清理策略
| 策略 | 说明 | 适用 |
|---|---|---|
| delete | 按时间/大小删除 | 日志/事件流 |
| compact | 保留 key 最新值 | 状态类数据 |
配置:
log.cleanup.policy=delete | compact
可组合:compact,delete4.2 Compact 原理
压缩过程:
按 key 去重,只保留每个 key 最新一条
旧消息被"墓碑"标记或删除
压缩后消息总览:
k1 → v1,k1 → v2,k2 → v3,k1 → v4
压缩后:k1 → v4,k2 → v3
注意:
key 为 null 的消息不压缩
压缩是后台异步进行4.3 墓碑与删除
墓碑(Tombstone):
写入 key 的 null 消息 → 标记删除
压缩时该 key 全部删除
(留墓碑,直到其他消息也删)
应用:
状态表同步(如用户配置)
CDC 场景的删除事件五、日志清理与过期
5.1 过期删除
删除条件:
log.retention.hours(时间,默认 168h/7 天)
log.retention.bytes(大小,默认 -1 不限)
实现:
定时任务扫描
删除过期 Segment 文件
只删除不被消费的段5.2 清理机制对比
| 机制 | 触发 | 动作 |
|---|---|---|
| 时间过期 | 段最后修改时间超阈值 | 删段 |
| 大小限制 | 日志总大小超限 | 从最旧删 |
| Compact | key 重复 | 段内去重 |
六、磁盘读写优化
6.1 顺序写
顺序写为什么快:
机械盘:磁头无需寻道
SSD:写放大极小
吞吐接近磁盘极限
设计:
只追加不修改
消费者从任意 offset 读6.2 零拷贝(Zero Copy)
传统拷贝(4 次):
磁盘 → 内核缓冲 → 用户缓冲 → 内核 Socket 缓冲 → 网卡
零拷贝 sendfile(2 次):
磁盘 → 内核缓冲 → 网卡
用户态不参与 → 减少上下文切换| 对比 | 拷贝次数 | 上下文切换 |
|---|---|---|
| 传统 | 4 | 4 |
| 零拷贝 | 2 | 2 |
6.3 批量与缓冲
批量写入:
生产者批次发送(batch.size/linger.ms)
减少网络请求次数
消费批量:
一次拉取多条(fetch.max.bytes)
顺序读返回七、页缓存
7.1 页缓存机制
Kafka 不自己做缓存:
利用 OS 页缓存(Page Cache)
读写都经过页缓存
写入:写入页缓存即返回(视 acks)
读取:优先页缓存,未命中再读盘7.2 为什么不用自建缓存
| 原因 | 说明 |
|---|---|
| 简单可靠 | 交给 OS 管理 |
| 免 GC | 无 JVM 堆压力 |
| 全局共享 | 读写共用缓存 |
| 无需预热 | OS 自动缓存热点 |
内存建议:
Broker 堆内存给 4-6GB 即可
剩余内存留给页缓存
页缓存越大命中率越高7.3 页缓存与读写
写入路径:
消息 → 页缓存(内存写)→ 后台刷盘
刷盘由 OS 控制(脏页阈值)
读取路径:
读请求 → 页缓存命中 → 直接返回
未命中 → 磁盘读入页缓存 → 返回八、存储相关配置
| 配置 | 默认 | 说明 |
|---|---|---|
| log.segment.bytes | 1GB | 分段大小 |
| log.index.interval.bytes | 4KB | 索引稀疏间隔 |
| log.retention.hours | 168 | 保留时间 |
| log.cleanup.policy | delete | 清理策略 |
| log.roll.hours | 168 | 分段滚动时间 |
| log.dirs | /tmp/kafka | 存储目录 |
调优建议:
大吞吐 → 加大 segment
状态数据 → compact
多磁盘 → 目录配置多个盘
索引间隔 → 平衡内存与查找九、常见问题
9.1 磁盘写满怎么办
排查:
保留时间/大小是否合理
是否有高冗余副本
清理是否被卡住(消费者慢)
处理:
调小保留策略
压缩/迁移数据
扩容磁盘9.2 为什么读比写慢
原因:
读可能随机(不同 offset)
页缓存未命中走磁盘
消费积压(数据在冷区)
应对:
监控页缓存命中率
合理设置保留策略
顺序消费9.3 消息重复/乱序与存储的关系
存储层面:
顺序按 offset 保证
重复由生产/消费语义决定
消费者从 offset 重读 → 重复消费
(存储保证有序,语义由应用保证)十、小结
Kafka 存储的核心一句话:顺序写 + 页缓存 + 零拷贝 + 分段稀疏索引。顺序写把磁盘性能拉满,页缓存让读写走内存,零拷贝省掉不必要的数据搬运,分段索引让定位高效。掌握这四点,就理解了 Kafka 高吞吐的底层原因。