bigywater.tech
Kafka 内部原理 · 15

Time Index:按时间查消息是怎么做到的

上一篇的 Offset Index 解决"给定 offset 找位置",而"给定一个时间点,找到从那时起的第一条消息"靠的是另一份索引:.timeindex。这次写入 300 条时间戳每条相隔 1 秒的消息,读出的时间索引,再用的时间戳查询看结果,最后试探 broker 对时间戳的边界检查。

验证:.timeindex 存的是 (时间戳, offset),条目数量由 index.interval.bytes 决定
.log 文件总共 22580 字节、300 条消息,平均每条约 75 字节。
验证:按时间戳查询返回"时间戳大于等于它的第一条消息"的 offset
查 base+150000 得到 offset 150,那条消息的时间戳恰好等于查询值;

验证:.timeindex 存的是 (时间戳, offset),条目数量由 index.interval.bytes 决定

命令 · shell $ kafka-topics.sh --create --topic time-index-demo --partitions 1 --replication-factor 1 --config index.interval.bytes=200 # Java Producer 发送 300 条,第 i 条的时间戳 = base + i*1000 毫秒,base=1790214409427 $ kafka-dump-log.sh --files /private/tmp/kafka-lab/node1/time-index-demo-0/00000000000000000000.timeindex
真实实测 · 本机执行输出
timestamp: 1790214412427 offset: 3
timestamp: 1790214415427 offset: 6
timestamp: 1790214418427 offset: 9
timestamp: 1790214421427 offset: 12
...
timestamp: 1790214700427 offset: 291
timestamp: 1790214703427 offset: 294
timestamp: 1790214706427 offset: 297

.log 文件总共 22580 字节、300 条消息,平均每条约 75 字节。这里把 index.interval.bytes 调小到 200(默认是 4096),所以大约每 3 条消息记一条时间索引,条目里的时间戳正好是 base + offset*1000。如果保持默认的 4096 字节,22580 字节大约只会产生 5 个条目(按字节数估算,未实测)。这也是上一篇结论的另一面:索引密度由字节数决定,消息越小,两个条目之间隔的 offset 越多。

验证:按时间戳查询返回"时间戳大于等于它的第一条消息"的 offset

命令 · shell $ kafka-get-offsets.sh --topic time-index-demo --time <base + 0> $ kafka-get-offsets.sh --topic time-index-demo --time <base + 150000> $ kafka-get-offsets.sh --topic time-index-demo --time <base + 150500> $ kafka-get-offsets.sh --topic time-index-demo --time <比所有消息都晚的时间戳> $ kafka-console-consumer.sh --topic time-index-demo --partition 0 --offset 151 --max-messages 2 --property print.timestamp=true
真实实测 · 本机执行输出
base+0ms       -> time-index-demo:0:0
base+150000ms  -> time-index-demo:0:150
base+150500ms  -> time-index-demo:0:151
(比所有消息都晚的时间戳)-> 无输出,退出码 0

CreateTime:1790214560427   Offset:151   v151
CreateTime:1790214561427   Offset:152   v152

查 base+150000 得到 offset 150,那条消息的时间戳恰好等于查询值;查 base+150500(落在第 150 条和第 151 条之间)得到 151,即第一条时间戳不小于查询值的消息。用 --offset 151 消费验证,第一条消息的 CreateTime 是 1790214560427,等于 base + 151000,与 --time 的结果一致。查询一个比所有消息都晚的时间戳,命令没有报错(退出码 0),但没有输出任何 offset。写脚本时如果直接拿这个命令的输出当 offset 用,需要先判断空输出。

验证:时间戳默认由 Producer 提供(CreateTime),broker 只拒绝超出未来 1 小时的值

命令 · shell $ kafka-configs.sh --entity-type topics --entity-name time-index-demo --describe --all | grep message.timestamp # Java Producer 分别发送时间戳为"当前 +30 分钟"和"当前 +120 分钟"的消息
真实实测 · 本机执行输出
message.timestamp.type=CreateTime
message.timestamp.after.max.ms=3600000
message.timestamp.before.max.ms=9223372036854775807

未来 30 分钟的时间戳:写入成功
未来 120 分钟的时间戳:InvalidTimestampException: Timestamp 1790221727540 of message with offset 0 is out of range.
    The timestamp should be within [-9223370246640247943, 1790218127864]

message.timestamp.type=CreateTime 表示消息里的时间戳是 Producer 填的,broker 不改。Producer 可以随意指定时间戳(上面第一个实验就是这么做的),broker 只做一道范围检查:默认允许比当前时间晚最多 1 小时(after.max.ms=3600000),过去方向没有限制(before.max.ms 是 Long 最大值)。结果印证了这一点:+30 分钟成功,+120 分钟被拒绝,错误信息里给出的上界 1790218127864 正是当前时间加 1 小时。所以时间索引查的是"消息里的时间"而不是"消息到达 broker 的时间",业务时间乱序或者 Producer 时钟不准,会直接影响按时间查询的结果。

结论

按时间查消息的流程是:用 .timeindex 把时间戳映射到 offset,再用 Offset Index 找到文件位置。查询语义是"第一条时间戳不小于目标的消息";查询超出范围时没有输出;时间戳默认由 Producer 决定,broker 只限制不能比当前晚超过 1 小时。

分步演示:用不同时间戳查询/写入,结果分别是什么

每一步都是本文上面两节执行kafka-get-offsets.sh和发送消息得到的原始结果。

按时间戳查消息返回的是"第一条不早于它的消息",写入时间戳只被限制不能太超前
这一步新验证的查询/写入

    参考与说明

    • 数据来自本机 3 节点 Kafka 4.3.1 集群,topic 使用 1 分区 1 副本,Leader 在 broker1,索引文件直接读自 /private/tmp/kafka-lab/node1/time-index-demo-0/。
    • 和《Offset Index:Kafka 如何快速定位一条消息》一起看:那篇是 offset 到位置,这篇是时间到 offset。
    • 没有验证:message.timestamp.type=LogAppendTime(由 broker 写入时间戳)的行为;时间戳乱序(后写入的消息时间戳更早)时索引和查询的表现;这个 topic 只有一个 Segment,没有验证跨 Segment 的时间查询。
    • 补充说明:写入"未来 30 分钟"的那条消息已经留在这个演示 topic 里,它在上面 --time 查询之后才写入,没有影响前面的输出。
    京ICP备2026067502号-1京公网安备11010502063487号