Skip to content

Kafka 存储机制

Kafka 的核心不是把消息暂存在内存里,而是把消息持久化为磁盘上的追加日志。每个 Topic 被拆成多个 Partition,每个 Partition 在 Broker 上对应一个日志目录,消息按 Offset 递增追加写入。

存储层级

log.dirs=/data/kafka-logs
└── orders-0/                    # Topic=orders, Partition=0
    ├── 00000000000000000000.log
    ├── 00000000000000000000.index
    ├── 00000000000000000000.timeindex
    ├── 00000000000001000000.log
    ├── 00000000000001000000.index
    └── leader-epoch-checkpoint
层级说明
Topic逻辑消息分类
PartitionTopic 的物理分片,也是顺序性和并行度的基本单位
Log一个 Partition 对应一条追加日志
SegmentLog 被切分成多个段文件,便于滚动、删除和查找
Record BatchProducer 批量发送后的消息批次,Kafka 的实际写入单位

Segment 文件

Kafka 不会把一个 Partition 写成单个巨大文件,而是拆成多个 Segment。每个 Segment 的文件名是该段的起始 Offset

文件作用
.log保存实际消息数据
.indexOffset 到物理位置的稀疏索引
.timeindex时间戳到 Offset 的稀疏索引
.txnindex事务索引,用于过滤未提交或已中止事务
leader-epoch-checkpointLeader Epoch 与 Offset 映射,用于副本截断和故障恢复

Segment 滚动

常见滚动条件:

配置说明
log.segment.bytesSegment 达到指定大小后滚动
log.roll.ms / log.roll.hoursSegment 达到指定时间后滚动
log.index.size.max.bytes索引文件达到上限后滚动

只有当前活跃 Segment 会继续追加写入,旧 Segment 变为只读,后续可被清理或压缩。

Offset 与索引

Kafka 的 Offset 是 Partition 内的逻辑编号,不是文件位置。读取某个 Offset 时,Kafka 需要把逻辑 Offset 转成 .log 文件里的物理位置。

目标 Offset = 10023

  ├── 找到 baseOffset <= 10023 的 Segment
  │     例如 00000000000001000000.log

  ├── 在 .index 中二分查找相近 Offset
  │     relativeOffset=23 -> position=4096

  └── 从 .log 的 position 顺序扫描到目标消息

Kafka 的索引是稀疏索引,不是每条消息都有索引项。这样可以减少索引文件大小,查找时先定位到附近位置,再顺序扫描少量数据。

Page Cache

Kafka 依赖操作系统 Page Cache,而不是在 JVM 堆里缓存大量消息。

Producer 写入

  ├── Broker 追加写入文件
  ├── 数据先进入 OS Page Cache
  └── 由操作系统异步刷盘

Consumer 读取

  ├── 优先从 Page Cache 命中
  └── 未命中时再从磁盘读取

这种设计的好处:

  • 避免 JVM 堆缓存导致 GC 压力过大
  • 让操作系统统一管理缓存淘汰和预读
  • 顺序写入配合 Page Cache 可以获得接近内存的读写性能
  • Broker 重启后 Page Cache 可以被操作系统逐步重新预热

刷盘机制

Kafka 默认不在每条消息写入后强制 fsync,而是依赖操作系统刷盘。数据可靠性主要由副本机制保证,而不是单机同步刷盘保证。

配置说明
log.flush.interval.messages每写入多少条消息强制刷盘
log.flush.interval.ms间隔多久强制刷盘
acks=all等待 ISR 副本确认,提升集群级持久性
min.insync.replicas限制最少同步副本数

生产环境通常不建议频繁强制刷盘,否则会显著降低吞吐。更常见的做法是使用 replication.factor=3acks=allmin.insync.replicas=2

日志保留

Kafka 的消息消费后不会立即删除,而是按保留策略清理。

策略配置说明
按时间log.retention.ms / log.retention.hours超过保留时间后删除旧 Segment
按大小log.retention.bytesPartition 日志总大小超过阈值后删除旧 Segment
删除策略cleanup.policy=delete默认策略,直接删除过期 Segment
压缩策略cleanup.policy=compact按 Key 保留最新值,适合状态类 Topic

Delete 与 Compact

delete 适合普通事件流,例如订单事件、日志、埋点。消息超过保留时间后整段删除。

compact 适合保存“当前状态”,例如用户配置、设备状态、维表数据。对于相同 Key,Kafka 最终只保留最新值。

Key=user-1, value=v1
Key=user-2, value=v1
Key=user-1, value=v2

Log Compaction 后:
Key=user-2, value=v1
Key=user-1, value=v2

写入路径

Producer

  ├── 批量压缩 Record Batch
  ├── 发送到 Partition Leader


Broker Leader

  ├── 校验请求和权限
  ├── 追加写入 active Segment
  ├── 更新 LEO
  ├── 等待 Follower 拉取并进入 ISR 确认
  └── 推进 HW 后返回 ACK

关键点:

  • 写入是追加模式,避免随机写
  • Broker 按 Partition 顺序写入,因此 Partition 内天然有序
  • Producer 批量发送,Broker 批量落盘
  • HW 之前的数据才对 Consumer 可见

读取路径

Consumer Fetch(offset=N)

  ├── Broker 根据 Offset 定位 Segment
  ├── 使用 .index 找到物理位置
  ├── 从 .log 顺序读取 Record Batch
  └── 返回不超过 HW 的消息

Consumer 读取受两个 Offset 影响:

Offset说明
LEO当前副本写到的最新位置
HW已被 ISR 确认的最高位置,普通 Consumer 最多读到这里

事务场景下,如果 isolation.level=read_committed,Consumer 还会受到 LSO(Last Stable Offset)限制,避免读到未提交事务。

参考