本文是大数据系列第 65 篇,深入解析 Kafka 的日志存储机制,理解底层存储原理有助于进行针对性的性能调优和故障排查。
日志存储架构
Kafka 的每个分区对应磁盘上的一个目录,目录命名规则为 <topic>-<partition>。分区目录下存储若干个 LogSegment(日志段),每个 LogSegment 由一组文件组成:
/kafka-logs/my-topic-0/
├── 00000000000000000000.log # 数据文件(消息内容)
├── 00000000000000000000.index # 偏移量索引文件
├── 00000000000000000000.timeindex # 时间戳索引文件
├── 00000000000000000500.log
├── 00000000000000000500.index
├── 00000000000000000500.timeindex
└── ...
文件名为 20 位零填充的十进制数,表示该 Segment 中第一条消息的绝对偏移量。当前活跃写入的 Segment(activeSegment)始终是最后一个。
LogSegment 分段设计
将日志切分为多个 Segment 的好处:
- 方便清理:过期数据以 Segment 为粒度删除,不影响其他数据
- 减少查找范围:通过文件名可快速定位消息所在 Segment
- 控制单文件大小:避免单个文件过大导致的 IO 性能问题
分段触发条件
满足以下任一条件时,Kafka 会滚动创建新的 Segment:
| 参数 | 默认值 | 含义 |
|---|---|---|
log.segment.bytes | 1073741824(1GB) | Segment 文件大小上限 |
log.roll.hours | 168(7天) | Segment 最大存活时间 |
log.index.size.max.bytes | 10485760(10MB) | 索引文件大小上限 |
索引机制
Kafka 使用两种索引加速消息查找,均采用稀疏索引设计——不为每条消息建立索引,而是每写入一定量数据(由 log.index.interval.bytes 控制,默认 4096 字节)建立一个索引条目。
偏移量索引(.index)
索引条目存储两个值:
- 相对偏移量(Relative Offset):消息偏移量减去 Segment 起始偏移量,用 4 字节存储(节省空间)
- 物理位置(Position):消息在
.log文件中的字节偏移量,用 4 字节存储
以 Segment 起始偏移量为 500 为例:
索引条目:[相对偏移量=10, 位置=1024]
表示:绝对偏移量 510 的消息位于 .log 文件第 1024 字节处
索引文件使用 mmap(内存映射文件) 技术加载到内存,使索引查找速度接近内存访问速度。
时间戳索引(.timeindex)
时间戳索引每条记录包含:
- 时间戳(Timestamp):该 Segment 在该时刻写入的最新消息时间戳
- 相对偏移量:对应消息的相对偏移量
用于支持按时间范围查找消息(如消费者重置偏移量到某个时间点)。
查找流程示例
以查找绝对偏移量 368776 的消息为例:
- 定位 Segment:对所有 Segment 文件名做二分查找,找到起始偏移量 ≤ 368776 且最大的 Segment(如
00000000000368000.log) - 查找索引:在该 Segment 的
.index文件中二分查找,找到不超过相对偏移量 776 的最大索引条目,假设得到物理位置pos=8192 - 顺序扫描:从
.log文件的pos=8192开始顺序读取,直到找到偏移量为 368776 的消息
稀疏索引的代价是步骤 3 需要少量顺序扫描,但因为 log.index.interval.bytes 控制了间距,扫描范围非常小,实际影响可忽略不计。
日志写入机制
数据只追加写入 activeSegment(顺序写入),这是 Kafka 高吞吐的基础之一——顺序磁盘 IO 性能接近内存随机访问。
写入流程:
1. 消息追加到 activeSegment 的 .log 文件末尾
2. 每隔 log.index.interval.bytes 字节,在 .index 文件追加一个索引条目
3. 当 activeSegment 达到滚动条件,创建新的 activeSegment
Kafka 还利用操作系统的 Page Cache 延迟刷盘,通过 log.flush.interval.messages 和 log.flush.interval.ms 控制刷盘时机,在性能和持久性之间取得平衡。
消息保留与清理策略
基于时间的保留
# 消息保留时长(优先级:ms > minutes > hours)
log.retention.ms=604800000 # 7 天(毫秒)
log.retention.minutes=10080 # 7 天(分钟)
log.retention.hours=168 # 7 天(小时,默认)
基于大小的保留
# 单个分区日志总大小上限(-1 表示不限制)
log.retention.bytes=-1
注意:log.retention.bytes 是分区级别的限制,而非整个 Topic。
清理粒度
Kafka 以 Segment 为最小清理单位,不会删除单条消息。只有当整个 Segment 中所有消息都过期(最新消息时间戳超过保留时长)时,才会删除该 Segment。
这意味着实际存储量可能略超过配置的保留大小,最多超出一个 Segment 的大小。
日志压缩(Log Compaction)
除删除策略外,Kafka 还支持日志压缩:
log.cleanup.policy=compact
压缩策略保留每个 Key 的最新消息,删除同一 Key 的旧版本。适用于需要保留最新状态的场景(如数据库变更日志、用户配置存储)。
关键配置汇总
| 参数 | 默认值 | 说明 |
|---|---|---|
log.segment.bytes | 1GB | Segment 滚动大小阈值 |
log.roll.hours | 168h | Segment 最大存活时间 |
log.index.interval.bytes | 4096B | 索引稀疏度(越小索引越密) |
log.retention.hours | 168h | 消息保留时长 |
log.retention.bytes | -1 | 分区最大存储量(-1 不限) |
log.cleanup.policy | delete | 清理策略(delete/compact) |
log.flush.interval.messages | Long.MAX_VALUE | 消息数触发刷盘阈值 |
Kafka 的存储设计充分利用顺序写、稀疏索引和 mmap 等技术,在保证高吞吐的同时实现了灵活的消息检索和生命周期管理。理解这些机制,是进行 Kafka 集群调优和容量规划的基础。