bigywater.tech
Kafka 内部原理 · 16

Retention:消息什么时候真正被删除

retention.ms 到期,消息就消失了吗?这次把一个 topic 的 retention.ms 设成 20 秒,写入 1200 条消息(跨三个 Segment),然后每秒观察一次磁盘目录,记录文件到底在什么时候被改名、什么时候真正从磁盘消失,以及消息还能不能被读到。

验证:过期后先改名为 .deleted,约 60 秒后才真正删除文件
过期之后磁盘上发生了两个阶段:t+12s 时三个旧 Segment 同时被改名成 .log.deleted,同时出现一个
验证:活跃 Segment 也会被删除,broker 会先滚出一个新的空 Segment
上面输出里,被改名的三个文件包括 00000000000000001024.log

验证:过期后先改名为 .deleted,约 60 秒后才真正删除文件

命令 · shell $ kafka-topics.sh --create --topic retention-demo --partitions 1 --replication-factor 1 --config retention.ms=20000 # 写入 1200 条 x 约 2000 字节,落在 3 个 Segment:0、512、1024 # 之后每秒 ls 一次 /private/tmp/kafka-lab/node3/retention-demo-0,只在变化时打印
真实实测 · 本机执行输出
写入结束时(09:50:26):
1031522  00000000000000000000.log
1031656  00000000000000000512.log
 354830  00000000000000001024.log

t+0s  : 00000000000000000000.log  00000000000000000512.log  00000000000000001024.log
t+12s : 00000000000000000000.log.deleted  00000000000000000512.log.deleted
        00000000000000001024.log.deleted  00000000000000001200.log
t+73s : 00000000000000001200.log
最终  : 0  00000000000000001200.log

过期之后磁盘上发生了两个阶段:t+12s 时三个旧 Segment 同时被改名成 .log.deleted,同时出现一个新的空 Segment 00000000000000001200.log;t+73s 时 .deleted 文件才真正消失,中间隔了约 61 秒。查到的配置是 file.delete.delay.ms=60000,两者吻合(轮询间隔 1 秒,所以有 1 秒误差)。所以"逻辑删除"和"物理删除"是分开的:改名之后消息已经不可读,但磁盘空间要再过一个延迟期才释放。这个 topic 的 retention.ms 是 20 秒,但轮询是在写入结束之后才开始的,所以 12 秒这个数字不能直接理解成"过期后 12 秒才处理",只能确认这个 broker 的检查间隔(log.retention.check.interval.ms=1000)很短,过期后很快就被处理。

验证:活跃 Segment 也会被删除,broker 会先滚出一个新的空 Segment

上面输出里,被改名的三个文件包括 00000000000000001024.log——它在写入结束时只有 354830 字节,远没写满 1MB,是当时正在接收写入的活跃 Segment。它也被改名成 .deleted,同时新出现了 00000000000000001200.log(1200 正是下一条消息将要使用的 offset)。也就是说,当整个活跃 Segment 里的消息都已经过期,broker 会先把它滚动成一个新的空 Segment,再把旧的整体删除,而不是留着这个"半满"的文件。上一篇 Log Segment 里用 retention.bytes 实验时活跃 Segment 是被保留的,两个实验的差别在于触发条件:按大小清理的那次,活跃 Segment 被保留了;按时间清理的这次,活跃 Segment 里的消息全部过期,它也被清掉了。为什么会有这个差别,这次没有去读源码验证。

验证:删除之后,最早 offset 追上最新 offset,从 offset 0 读不到任何消息

命令 · shell $ kafka-get-offsets.sh --topic retention-demo --time -2 # 最早 $ kafka-get-offsets.sh --topic retention-demo --time -1 # 最新 $ kafka-console-consumer.sh --topic retention-demo --partition 0 --offset 0 --max-messages 1 --timeout-ms 6000
真实实测 · 本机执行输出
retention-demo:0:1200
retention-demo:0:1200

TimeoutException
Processed a total of 0 messages

最早 offset 和最新 offset 都是 1200:offset 0 到 1199 已经不存在,但 offset 没有回退——新消息会从 1200 继续编号,这也是 offset 在 Partition 生命周期里单调递增的证明。从 offset 0 开始消费,结果是读到 0 条消息,命令因为 --timeout-ms 到期以 TimeoutException 结束,没有出现"offset 越界"之类的报错。这里没有继续验证客户端在这种情况下具体走了哪条 offset 重置策略,只能确认:读不到数据,也没有报越界错误。

结论

Retention 的过程是"过期 → 改名 .deleted(此时消息已不可读)→ 等待 file.delete.delay.ms(默认 60 秒)→ 文件真正删除"。清理以 Segment 为单位,时间策略下活跃 Segment 里的消息全部过期时也会被清理,broker 先滚动出新的空 Segment。offset 不会因为数据被删而回退。

分步演示:一次的retention.ms过期,磁盘目录是怎么一步步变化的

每个时间点的文件状态,都是本文上面几节每秒轮询磁盘目录得到的原始记录。

过期之后,文件是先改名再延迟删除的,不是立刻消失
刚被标记为.deleted的Segment

    参考与说明

    • 数据来自本机 3 节点 Kafka 4.3.1 集群,实验 topic 的 Leader 在 broker3,文件状态直接从 /private/tmp/kafka-lab/node3/retention-demo-0/ 每秒轮询得到。实验室 broker 配置了 log.retention.check.interval.ms=1000(默认是 5 分钟),默认配置下这个检查会慢得多。
    • 尝试过把 segment.bytes 设成 100000 来得到更小的 Segment,broker 返回了 Invalid value 100000 for configuration segment.bytes: Value must be at least 1048576,所以 1MB 是这个版本里 Segment 大小的下限,实验因此使用默认的 1MB Segment。
    • 和《Log Segment:一个 Partition 的日志如何分段》一起看:那篇验证了 retention.bytes 按整段删除,这篇验证了 retention.ms 的过程和延迟删除。
    • 没有验证:Segment 内部分消息过期、部分未过期时的行为(这次三个 Segment 的消息是同一时刻写入的,一起过期,没有构造"部分过期"的场景);retention.ms 和 retention.bytes 同时设置时谁先生效;日志压缩(cleanup.policy=compact)下的保留行为。
    京ICP备2026067502号-1京公网安备11010502063487号