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=3、acks=all、min.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)限制,避免读到未提交事务。

参考 ​