bigywater.tech
Kafka 内部原理 · 14

Offset Index:Kafka 如何快速定位一条消息

上一篇看到每个 Segment 旁边都有 .index 文件。这次直接用 kafka-dump-log.sh 把的索引内容读出来,和 .log 里每个 batch 的位置逐条对照,再手工做一次"查 offset 1200"的定位,看索引到底存了什么、查找是怎么走的。

验证:索引是稀疏的,每个条目是 (offset, 文件内字节位置)
这个 Segment 的起始 offset 是 1024,下一个 Segment 从 1536 开始,里面有 512 条
验证:手工查 offset 1200,先在索引里找不大于它的最近条目,再从那个位置顺序扫描 .log
用数据走一遍查找过程。

验证:索引是稀疏的,每个条目是 (offset, 文件内字节位置)

命令 · shell $ kafka-dump-log.sh --files /private/tmp/kafka-lab/node2/log-segment-demo-0/00000000000000001024.index
真实实测 · 本机执行输出
offset: 1087 position: 64509
offset: 1119 position: 129018
offset: 1151 position: 193527
offset: 1183 position: 258036
offset: 1215 position: 322545
offset: 1247 position: 387054
offset: 1279 position: 451563

这个 Segment 的起始 offset 是 1024,下一个 Segment 从 1536 开始,里面有 512 条消息,但索引条目每隔 32 个 offset 才一条,不是每条消息一条。相邻条目的 position 之差都是 64509 字节,恰好是一个 batch(32 条 x 约 2000 字节)的大小。index.interval.bytes 的值是 4096(kafka-configs.sh --describe --all 查到的默认值):每写入超过 4096 字节就可以记一条索引,而这个测试里每个 batch 就有 64509 字节,所以每个 batch 一条。也就是说,索引的稀疏程度取决于消息大小,消息越小,同样的字节数里跨过的 offset 越多,条目之间的 offset 间隔越大。

验证:手工查 offset 1200,先在索引里找不大于它的最近条目,再从那个位置顺序扫描 .log

命令 · shell $ kafka-dump-log.sh --files .../00000000000000001024.log | grep -E "baseOffset: (1152|1184|1216)" $ kafka-console-consumer.sh --topic log-segment-demo --partition 0 --offset 1200 --max-messages 2 --property print.offset=true
真实实测 · 本机执行输出
baseOffset: 1152 lastOffset: 1183   position: 258036
baseOffset: 1184 lastOffset: 1215   position: 322545
baseOffset: 1216 lastOffset: 1247   position: 387054

Offset:1200   ...
Offset:1201   ...

用数据走一遍查找过程。第一步,按文件名定位 Segment:1200 落在 00000000000000001024.log(起始 offset 1024,下一个 Segment 从 1536 开始)。第二步,在这个 Segment 的索引里找不大于 1200 的最大条目,是 offset: 1183 position: 258036。第三步,从 .log 的 258036 字节处开始顺序扫描:先遇到 baseOffset 1152–1183 的 batch,不包含 1200;下一个 batch 是 1184–1215,位置 322545,包含 1200,命中。整个过程只读了两个 batch,而不是从 Segment 开头扫描 1200−1024 条消息。用 --offset 1200 消费,返回的第一条就是 Offset:1200,第二条是 1201,和手工推导的位置一致。这里索引条目里的 offset 和 position 正好对应同一个 batch(1183 是 batch 1152–1183 的最后一个 offset,258036 是这个 batch 的起始位置),所以按"不大于目标的最大条目"查到的位置,需要多扫一个 batch 才到目标。

验证:.timeindex 是另一份独立索引,存的是 (时间戳, offset)

命令 · shell $ kafka-dump-log.sh --files .../00000000000000001024.timeindex
真实实测 · 本机执行输出
timestamp: 1790213884996 offset: 1087
timestamp: 1790213884997 offset: 1183
timestamp: 1790213884998 offset: 1247
timestamp: 1790213884999 offset: 1311

.timeindex 里的 offset 和 .index 的条目基本能对上(1087、1183、1247、1311),但它存的不是文件位置,而是时间戳,用于"按时间查 offset"(比如 --time 参数、按时间重置消费位置)。查询时先用时间戳找到 offset,再走一遍上面的 offset 索引定位到文件位置,两步合起来才是按时间查消息。这个实验的消息是几百毫秒内写完的,所以时间戳只差几毫秒,看不出时间跨度,仅能确认结构。

结论

Offset Index 的结构是:每个 Segment 一份,稀疏地记录 (offset, position),查找时先按文件名选 Segment、再在索引里找不大于目标的条目、最后从该位置顺序扫描很短一段。它用少量条目换来常数级的文件读取量,代价是命中后需要多扫描一小段。这次的数据里,一个 batch 就跨过 4096 字节的阈值,所以每个 batch 都有一条索引。

分步演示:一次的offset查找,走了哪几步

索引条目和batch范围,都是本文上面几节用kafka-dump-log.sh解析.index/.log文件得到的原始数据。

查offset=1200,是怎么用稀疏索引只读2个batch就定位到的
正在检查的索引条目命中的"不大于目标"条目

    参考与说明

    • 数据来自本机 3 节点 Kafka 4.3.1 集群,索引文件和 .log 均取自上一篇创建的 log-segment-demo-0 目录,用 kafka-dump-log.sh 直接解析。
    • 和《Log Segment:一个 Partition 的日志如何分段》一起看:那篇说明了文件名就是起始 offset,这篇说明了文件内部怎么继续往下找。
    • 没有涉及:没有测量有索引与无索引的查找耗时对比;索引条目里存储的是相对 offset(相对 Segment 起始 offset)这一格式细节,本文的 dump 工具已经还原成了绝对 offset,没有直接看二进制;.timeindex 在时间跨度较大时的查找行为也没有验证。
    京ICP备2026067502号-1京公网安备11010502063487号