本文是大数据系列第 65 篇,深入解析 Kafka 的日志存储机制,理解底层存储原理有助于进行针对性的性能调优和故障排查。

完整图文版(含截图):CSDN 原文 | 掘金

日志存储架构

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.bytes1073741824(1GB)Segment 文件大小上限
log.roll.hours168(7天)Segment 最大存活时间
log.index.size.max.bytes10485760(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 的消息为例:

  1. 定位 Segment:对所有 Segment 文件名做二分查找,找到起始偏移量 ≤ 368776 且最大的 Segment(如 00000000000368000.log
  2. 查找索引:在该 Segment 的 .index 文件中二分查找,找到不超过相对偏移量 776 的最大索引条目,假设得到物理位置 pos=8192
  3. 顺序扫描:从 .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.messageslog.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.bytes1GBSegment 滚动大小阈值
log.roll.hours168hSegment 最大存活时间
log.index.interval.bytes4096B索引稀疏度(越小索引越密)
log.retention.hours168h消息保留时长
log.retention.bytes-1分区最大存储量(-1 不限)
log.cleanup.policydelete清理策略(delete/compact)
log.flush.interval.messagesLong.MAX_VALUE消息数触发刷盘阈值

Kafka 的存储设计充分利用顺序写、稀疏索引和 mmap 等技术,在保证高吞吐的同时实现了灵活的消息检索和生命周期管理。理解这些机制,是进行 Kafka 集群调优和容量规划的基础。