来自 #296 的架构级评审建议。不阻塞合入,仅供参考是否有更好的架构解法。
⚠️ [重要 · 存储] 回收需在 WAL checkpoint 之后 storage/engine.go:522
问题根因:standalone 模式下,回收删除已被投递的 SSTable 文件后,若该文件的记录仍存在于 WAL 中(即尚未被 checkpoint 重写剪枝),一旦 WAL replay 到这些记录,会把这些已回收数据重新写回 memtable 并最终再次 flush 成新 SSTable——「已回收的数据复活」。ReclaimUpTo 只与 compaction 互斥(fileMu),并未与 Checkpoint(cpMu.Lock)协调,也未确认被回收文件的数据是否已在 WAL 中剪枝。若回收前 WAL 中仍残留这些 key 的记录,重启后 replay 会把它们写回——彻底违反『已投递数据不再读回』的回收语义。
为什么低级解法不够:在 ReclaimDelivered 加 cpMu 锁不够,因为 checkpoint 间隔是 2×MaxMemTableSize 次写,无法保证回收时 WAL 已含被回收键的 checkpoint。低修(调用前强制一次 Checkpoint)有效但把回收变成了同步强刷,性能代价高,且未在架构层面确立『回收与 WAL 剪枝的一致性』。
架构级方案:确立不变式:回收任何 SSTable 前,必须保证其数据已从 WAL 中剪枝(即已 checkpoint 到不含这些 key 的活跃快照)。实现:ReclaimDelivered 在 ReclaimUpTo 前调用一次 kv.Checkpoint()(持 cpMu 独占静默),或者更优——把回收纳入 checkpoint 流程:每当投递游标推进使某个文件整体被越过时,先 checkpoint、再回收。若担忧频繁 checkpoint 的性能,可把回收粒度从「每批投递」放宽到「每 N 批」并配齐 WAL 剪枝保证。若不做,回收是无效的(重启后数据复活),保留期语义名存实亡。
代价/收益:收益:standalone 下回收真正成立,崩溃重启后不会复活已回收数据。代价:每批回收需先同步 checkpoint,若保留期开启场景频繁回收会引入 checkpoint 开销;可通过批量回收(每 N 批一次)摊销。这是保留期功能正确性的必要前提,不可省。
storage/engine.go:522问题根因:standalone 模式下,回收删除已被投递的 SSTable 文件后,若该文件的记录仍存在于 WAL 中(即尚未被 checkpoint 重写剪枝),一旦 WAL replay 到这些记录,会把这些已回收数据重新写回 memtable 并最终再次 flush 成新 SSTable——「已回收的数据复活」。ReclaimUpTo 只与 compaction 互斥(fileMu),并未与 Checkpoint(cpMu.Lock)协调,也未确认被回收文件的数据是否已在 WAL 中剪枝。若回收前 WAL 中仍残留这些 key 的记录,重启后 replay 会把它们写回——彻底违反『已投递数据不再读回』的回收语义。
为什么低级解法不够:在 ReclaimDelivered 加 cpMu 锁不够,因为 checkpoint 间隔是 2×MaxMemTableSize 次写,无法保证回收时 WAL 已含被回收键的 checkpoint。低修(调用前强制一次 Checkpoint)有效但把回收变成了同步强刷,性能代价高,且未在架构层面确立『回收与 WAL 剪枝的一致性』。
架构级方案:确立不变式:回收任何 SSTable 前,必须保证其数据已从 WAL 中剪枝(即已 checkpoint 到不含这些 key 的活跃快照)。实现:ReclaimDelivered 在 ReclaimUpTo 前调用一次 kv.Checkpoint()(持 cpMu 独占静默),或者更优——把回收纳入 checkpoint 流程:每当投递游标推进使某个文件整体被越过时,先 checkpoint、再回收。若担忧频繁 checkpoint 的性能,可把回收粒度从「每批投递」放宽到「每 N 批」并配齐 WAL 剪枝保证。若不做,回收是无效的(重启后数据复活),保留期语义名存实亡。
代价/收益:收益:standalone 下回收真正成立,崩溃重启后不会复活已回收数据。代价:每批回收需先同步 checkpoint,若保留期开启场景频繁回收会引入 checkpoint 开销;可通过批量回收(每 N 批一次)摊销。这是保留期功能正确性的必要前提,不可省。