bigywater.tech
Kafka 内部原理 · 13

Log Segment:一个 Partition 的日志如何分段

一个 Partition 在磁盘上不是一个大文件,而是一个目录,里面是按大小切开的多个 Segment。这次写入约 5MB 数据到 segment.bytes=1MB 的 topic,直接看磁盘上的文件、用 kafka-dump-log.sh 核对文件名的含义,再设置 retention.bytes 观察删除到底以什么为单位。

验证:文件名就是这个 Segment 的第一个 offset,写满 1MB 就滚动出新文件
2500 条消息产生了 5 个 .log 文件,前 4 个都在 1MB 左右(略超 1MB,因为滚动是以整个 bat
验证:每个 Segment 旁边有 .index 和 .timeindex,活跃 Segment 的索引文件被预分配到 10MB
上面 ls 输出里同一个基准 offset 对应三个文件: 已经写满的 Segment,索引文件只剩 72–144 字节

验证:文件名就是这个 Segment 的第一个 offset,写满 1MB 就滚动出新文件

命令 · shell $ kafka-topics.sh --create --topic log-segment-demo --partitions 1 --replication-factor 1 # 写入 2500 条 x 约 2000 字节 $ ls -l /private/tmp/kafka-lab/node2/log-segment-demo-0/ $ kafka-dump-log.sh --files .../00000000000000000512.log | grep baseOffset | head -2
真实实测 · 本机执行输出
00000000000000000000.log   1031522
00000000000000000512.log   1031656
00000000000000001024.log   1032144
00000000000000001536.log   1032144
00000000000000002048.log    911243   <- 还没写满的活跃 Segment

--- 00000000000000000512.log 里的第一批 ---
baseOffset: 512 lastOffset: 543 count: 32
--- 00000000000000002048.log 里的第一批 ---
baseOffset: 2048 lastOffset: 2079 count: 32
--- 00000000000000000000.log 里的最后一批 ---
baseOffset: 480 lastOffset: 511 count: 32

2500 条消息产生了 5 个 .log 文件,前 4 个都在 1MB 左右(略超 1MB,因为滚动是以整个 batch 为单位判断的),最后一个 911243 字节还没写满,是当前接收写入的活跃 Segment。文件名不是序号,而是这个 Segment 里第一条消息的 offset:00000000000000000512.log 里第一批的 baseOffset 恰好就是 512,上一个文件最后一批的 lastOffset 是 511,首尾严格衔接。所以给定任意 offset,只需要比较文件名就能知道它在哪个文件里。

验证:每个 Segment 旁边有 .index 和 .timeindex,活跃 Segment 的索引文件被预分配到 10MB

上面 ls 输出里同一个基准 offset 对应三个文件:

真实实测 · 本机执行输出
00000000000000000512.index       120     <- 已滚动的 Segment,索引被裁剪到实际大小
00000000000000000512.timeindex    72
00000000000000002048.index  10485760     <- 活跃 Segment,索引预分配 10MB
00000000000000002048.timeindex 10485756

已经写满的 Segment,索引文件只剩 72–144 字节;仍在写入的活跃 Segment,.index 是 10485760 字节(正好 10MB),.timeindex 是 10485756 字节。这是可见的差别:活跃 Segment 的索引文件先按上限预分配,滚动时才被裁剪成实际内容大小。这也解释了为什么 du 或 ls 看到一个刚建好的空 Partition 也会有 20MB 的文件。索引本身怎么用来定位消息,是下一篇 Offset Index 的内容。

验证:保留策略以整个 Segment 为单位删除,不会删掉半个文件

命令 · shell $ kafka-configs.sh --alter --entity-type topics --entity-name log-segment-demo --add-config retention.bytes=2097152 # 等 20 秒(这个实验室的 log.retention.check.interval.ms=1000) $ ls -l .../log-segment-demo-0/ | grep -E '\.log|\.deleted' $ kafka-get-offsets.sh --topic log-segment-demo --time -2 # 最早 offset $ kafka-get-offsets.sh --topic log-segment-demo --time -1 # 最新 offset
真实实测 · 本机执行输出
1031522 00000000000000000000.log.deleted
1031656 00000000000000000512.log.deleted
1032144 00000000000000001024.log
1032144 00000000000000001536.log
 911243 00000000000000002048.log

log-segment-demo:0:1024     <- 最早 offset,从 0 跳到 1024
log-segment-demo:0:2500     <- 最新 offset

把 retention.bytes 设成 2MB 后,最老的两个 Segment(起始 offset 0 和 512)整体被改名成 .deleted,配套的 .index、.timeindex、.snapshot 一起被标记,剩下三个文件合计约 3MB。最早可读 offset 从 0 直接变成 1024,恰好是第三个 Segment 的文件名——没有出现"offset 0 到 300 被删、300 之后保留"这种中间状态。删除的最小单位就是 Segment,这也是为什么 retention.bytes 不是一个精确的上限:这次设置 2MB,实际还保留了约 3MB,因为要保证总大小不超过阈值就必须继续删整段,而当前活跃 Segment 是不能删的。

结论

Segment 分段带来三件可验证的事:文件名(起始 offset)让定位可以只靠比较文件名完成;索引文件随 Segment 走、写满后被裁剪;过期数据的清理是直接删整个文件,不需要改写任何内容。这次结果里最容易被忽略的一点是:retention.bytes=2MB 之后磁盘上仍有约 3MB,因为清理粒度是 Segment,不是字节。

分步演示:一个Partition目录下的文件,是怎么一个个滚动出来又被清理的

每一步的文件名、大小和offset,都是本文上面几节ls磁盘目录、用kafka-dump-log.sh核对得到的原始结果。

2500条消息滚动出5个Segment,retention按整个文件删除
刚滚动出的Segment更早的Segment被retention标记删除

    参考与说明

    • 数据来自本机 3 节点 Kafka 4.3.1 集群,topic 使用 segment.bytes=1048576(集群实验配置,默认值是 1GB,见 kafka-configs.sh --describe --all 里的 synonyms),文件是直接从 /private/tmp/kafka-lab/node2/log-segment-demo-0/ 读出来的。
    • 和《Page Cache:Kafka 为什么不自己管理缓存》一起看:每个 Segment 是独立文件,读取热点数据时命中的正是这些文件对应的 page cache。
    • 下一篇《Offset Index》会解读 .index 文件的内容,本文只展示了它们的存在和大小变化。
    • 没有涉及:segment.ms 按时间滚动、retention.ms 按时间清理,以及 cleanup.policy=compact 的日志压缩,都没有在本文验证;本文的删除实验只覆盖 retention.bytes 这一条路径。
    京ICP备2026067502号-1京公网安备11010502063487号