cleanup.policy=compact 的 topic 不按时间或大小删数据,而是保证"每个 Key 至少保留最新的一条"。这次给 10 个 Key 各写 5 轮更新,中途给其中一个 Key 写一条墓碑(value 为 null),跨三个 Segment,然后读出清理前后的磁盘文件和消息内容,看 broker 到底保留了什么、删掉了什么。
命令 · shell $ kafka-topics.sh --create --topic compact-demo --partitions 1 --replication-factor 1 \ --config cleanup.policy=compact --config min.cleanable.dirty.ratio=0.01 \ --config segment.ms=5000 --config delete.retention.ms=1000 # Java Producer:k0~k9 各写 v1~v5 共 5 轮(其中 v1~v3 一批,7 秒后 v4~v5 一批), # 再给 k3 写一条 value=null 的墓碑,7 秒后再写一条 roll=tail 触发 Segment 滚动 $ kafka-console-consumer.sh --topic compact-demo --partition 0 --offset 0 \ --property print.key=true --property print.offset=true
写入完成后 log end offset = 52,磁盘上 3 个 Segment: 2160 00000000000000000000.log 1510 00000000000000000030.log 76 00000000000000000051.log 清理之后消费到的全部消息: Offset:40 k0 v5 Offset:41 k1 v5 Offset:42 k2 v5 Offset:44 k4 v5 Offset:45 k5 v5 Offset:46 k6 v5 Offset:47 k7 v5 Offset:48 k8 v5 Offset:49 k9 v5 Offset:50 k3 null Offset:51 roll tail
52 条消息清理后只剩 11 条:k0、k1、k2、k4 到 k9 各保留最新的 v5,k3 的位置被墓碑 null 占着,加上最后的 roll。最值得注意的是 offset:保留下来的消息还是原来的 offset(40、41、42、44……),中间空出来的 43 是 k3 v5,它被后面的墓碑取代了。这说明压缩不会重新编号,offset 序列里会出现"洞",所以按 offset + 1 一条条读的逻辑在压缩 topic 上不成立,消费者拿到的下一个 offset 可能是跳着的。
清理前:2160 + 1510 + 76 字节,三个文件 清理后: 720 00000000000000000000.log <- 前两个 Segment(offset 0~50)合并重写成一个文件 76 00000000000000000051.log <- 活跃 Segment,未被改动 旧文件:00000000000000000000.log.deleted (2160)、00000000000000000030.log.deleted (1510) t+54s:两个 .deleted 文件被物理删除
前两个 Segment 一共 3670 字节,被合并重写成一个 720 字节的 00000000000000000000.log,旧文件同样先改名成 .deleted,约 54 秒后才被删除(我开始轮询时清理已经完成,所以这个时间点只能说明延迟删除的现象和上一篇 Retention 一致,不能精确对应到 60 秒)。最后那个只有 76 字节的 00000000000000000051.log 没有变化。这是压缩的关键约束:只有已经滚动的 Segment 才会被清理,活跃 Segment 里的重复 Key 不会被清理,所以我在实验里特意用 segment.ms=5000 加上一条尾部消息把前面的 Segment 滚出来。如果不滚动会怎样,这次没有单独实测。
命令 · shell # 之后再写 roll2=x,等 7 秒,再写 roll3=y,等 20 秒,重新读取全部
Offset:40 k0 v5 ... Offset:49 k9 v5 Offset:51 roll tail Offset:52 roll2 x Offset:53 roll3 y
之前保留的 Offset:50 k3 null 这次消失了,k3 在整个 topic 里再也没有任何记录——不是"值为空",而是这个 Key 不存在了。这个 topic 的 delete.retention.ms 我设成了 1000(默认 24 小时):墓碑保留期过了以后,下一次清理才会把它一起删除。默认值是 86400000 毫秒(查配置得到),也就是一天,设计意图应该是给消费者留出时间看到这条删除记录,但这一点本文没有实测。这里的实验没有验证"如果消费者在墓碑被清理后才开始读"会怎样,但从结果可以看出:此时读到的就是一份完全没有 k3 的数据。
压缩的效果是:已经滚动的 Segment 里,每个 Key 只留最后一条;保留的消息 offset 不变、序列有洞;活跃 Segment 不清理,所以最新的几条更新总是可能留有重复 Key;删除一个 Key 要靠墓碑,墓碑在 delete.retention.ms 之后被清理。它保证的是"每个 Key 的最新值一定在",不保证"历史值一定没有"。
每个key保留的最终状态和offset,都是本文上面几节消费compact-demo这个topic得到的原始结果。