feat: 保留期回收——按已投递位点丢弃整份数据文件 - #296
Conversation
作为「写入前置缓冲」,此前已投递的数据永不回收,本地只增不减,长跑必然涨满磁盘。 回收按整个 SSTable 文件丢弃,而非逐 key 写墓碑:墓碑会让写入量翻倍,且自身还要再经一轮 compaction 才消失;文件级丢弃是 O(1),无写放大。判据是「该文件 MaxKey 严格小于已提交的 投递游标」——投递按 key 升序推进,故游标之前的数据已全部被读过。 保守之处(宁可少回收,不可误删): - 仅在 MaxKey 可信时回收。没有可读 footer 的文件 MaxKey 未知,一律跳过。 - 用严格小于:恰好含游标的文件保留,因为游标本身尚未被消费。 - 游标为空(尚无提交)时不回收。 - 回收与 compaction 共用 fileMu 串行。二者都删文件,若交错,compaction 正在读的源文件 可能被删——POSIX 下已打开的 fd 仍可读,那批已回收的数据会被写进合并输出,即「已回收的 数据复活」。 - 游标先被压到 offset 保留前缀之下:游标自身以 `__offset__/<sink>` 存在同一 KV 空间, 若某文件同时含业务数据与游标而游标又大于它,整份文件会连游标一起删掉,投递将从头重投。 回收挂在 offset 提交成功之后(装饰 OffsetStore,投递主体不改):游标落地才代表这批不会 再被重投,此时回收才不会删掉仍需重投的数据。默认关闭——开启即改变读语义,必须由使用方明示。 顺带修一处扫描的复杂度问题:KVServer.Scan 此前总把从游标到键空间末尾的条目(上限 1 万) 全部物化,而调用方只取前几百条,每批代价 O(剩余数据量)。limit 现在一路传到扫描里,并按 文件的 [MinKey,MaxKey] 先排除与区间无交集的 SSTable。实测取 201 条的耗时随游标推进从 13ms 降到 0.9ms。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
📝 WalkthroughWalkthroughChangesRetention-aware delivery
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🐯 BanGD 数据库内核评审整体风险:🟡 中 变更总结:本 PR 为「写入前置缓冲」新增保留期回收能力:按已投递的 offset 游标,将 MaxKey 严格小于游标的整份 SSTable 文件从磁盘删除,使本地缓冲不再只增不减。核心架构:在 offset 提交成功之后、以装饰器(reclaimingOffsetStore)挂在投递路径上调用 KVServer.ReclaimDelivered → Engine.ReclaimUpTo,按文件级 O(1) 丢弃(而非墓碑+compaction),并用与 compaction 共用的 fileMu 串行化所有删文件动作,防止「已回收数据复活」。同时把 KVServer.Scan 的 limit 一路传到扫描内部,并按文件的 [MinKey,MaxKey] 先排除区间无关的 SSTable,修复了按游标分批取数时的 O(剩余数据量) 每批代价、总体 O(n²) 的扫描复杂度问题。此外文档化了一个既有的投递语义边界:KVSource 按 key 升序推进游标,无法覆盖乱序到达的写入。
架构问题(共 4 项)
普通问题(共 1 项)💡 [建议 · 边界条件]
本次评审消耗 token:共 295004 tokens(输入 234695,输出 11413,缓存命中 48896,缓存写入 0)|维度 [concurrency, memory, lock, storage, performance]|补充阅读周边文件 [service/delivery/offset/store.go, storage/sstable.go, service/delivery/deliverer.go, storage/options.go, service/offset_committer.go]|对抗式复核 3 票/条,过滤疑似误报 3 条 |
作为「写入前置缓冲」,此前已投递的数据永不回收,本地只增不减,长跑必然涨满磁盘。
为什么按整文件丢弃,而不是逐 key 写墓碑
墓碑会让写入量翻倍,而且自身还要再经一轮 compaction 才消失。文件级丢弃是 O(1)、无写放大——这正是日志结构系统回收空间的方式。
判据:该 SSTable 的
MaxKey严格小于已提交的投递游标。投递按 key 升序推进,故游标之前的数据已全部被读过。保守之处(宁可少回收,不可误删)
MaxKey不可信则跳过fileMu__offset__/<sink>存在同一 KV 空间。若某文件同时含业务数据与游标、而游标又大于它,整份文件会连游标一起删掉,投递将从头重投全部数据挂载点:offset 提交成功之后(装饰
OffsetStore,投递主体不改)。游标落地才代表这批不会再被重投,此时回收才不会删掉仍需重投的数据。默认关闭(
RetentionEnabled):开启即意味着已投递的数据不再能从本地读回,这是「缓冲」而非「存储」的语义,必须由使用方明示。顺带修一处扫描复杂度问题
KVServer.Scan此前总把从游标到键空间末尾的条目(上限 1 万)全部物化,而调用方只取前几百条 —— 每批代价 O(剩余数据量),总代价随数据量平方增长。现在limit一路传到扫描里,并按文件的[MinKey,MaxKey]先排除与区间无交集的 SSTable。实测取 201 条的耗时随游标推进:13ms → 3.5ms → 0.9ms。
测试
4 个用例,含回收与 compaction 并发(
-race)与 MaxKey 不可信时跳过。核心用例断言:整份已投递的文件被删、跨越游标的文件保留、保留文件里的数据仍可读、已回收的数据确实读不到。过程中发现的一条既有限制(已写入代码注释)
E2E 验证时投递只走了约 310/2000,追下去不是回收的问题,也不是我引入的:
KVSource按 key 升序推进游标,IdempotentFileSink按「已投递的最大 key」过滤 —— 两者都假设投递顺序与 key 顺序一致。而压测用 20 个并发 writer 乱序写入,投递一启动就沿 key 顺序冲到很后面,此后才落地的较小 key 永远排在游标之前,再也不会被投递。对照实测:2000 条若在投递启动前写完,10 轮取满 2000/2000;与并发 writer 同时进行则只投出约 310 条。
要覆盖乱序到达,游标需改为按「写入序」而非「key 序」推进(为每条写入分配单调序号并按其建索引),属独立设计。当前实现适用于时间序 key(如
imu:dev0:<ts>)这类天然单调的摄入场景 —— 这一边界已写进source.go的包内注释。验证
go build ./...(含-tags pprof)、go vet、gofmt、go test -race ./...连跑 2 次全绿。🤖 Generated with Claude Code
Summary by CodeRabbit