Skip to content

fix(sstable): 重载时从块索引取 MaxKey,修复重启后已刷盘数据全丢#153

Merged
NeverENG merged 1 commit into
mainfrom
fix/sstable-reload-maxkey
Jul 19, 2026
Merged

fix(sstable): 重载时从块索引取 MaxKey,修复重启后已刷盘数据全丢#153
NeverENG merged 1 commit into
mainfrom
fix/sstable-reload-maxkey

Conversation

@NeverENG

Copy link
Copy Markdown
Owner

致命 bug:standalone 重启后已刷盘数据全部丢失

现象

小 memtable 阈值下写入 50 个 key(多数刷到 SSTable),停机重启后 45/50 读不到,只剩还在 memtable/WAL 的尾部数据。运行时(未重启)读取一切正常。

根因

LoadSSTableMetaList 只读首键作 MinKey,MaxKey 交给 EnsureMeta 懒加载。但 EnsureMeta[klen][key][vlen][val] 顺序扫到文件尾,而 SSTable 数据段之后还接了块索引/布隆/footer——越过数据段边界后错位解析,实测 MaxKey 退化成空串。于是 getFromSSTables[MinKey,MaxKey] 范围过滤对任意真实 key 都判 key > MaxKey整段跳过该文件

修复

LoadSSTableMetaList 直接用块索引末项的 LastKey 作 MaxKey 并置 MaxKeyLoaded=true,绕开会越界的 EnsureMeta 扫描。老格式无 footer 时保留懒加载兜底(纯数据文件顺序扫描本就正确)。

验证

新增 TestReloadRecoversFlushedKeys:小阈值逼多轮刷盘 → 停机 → 同目录重载 → 断言 50/50 恢复。全量 storage/service 测试通过。

🤖 Generated with Claude Code

LoadSSTableMetaList 此前只读首键作 MinKey,MaxKey 留待 EnsureMeta 懒加载。
但 EnsureMeta 按 [klen][key][vlen][val] 顺序扫描到文件尾,而 SSTable 数据段之后
还接了块索引/布隆/footer——扫过边界后错位解析,MaxKey 实测退化成空串。
于是 getFromSSTables 的 [MinKey,MaxKey] 范围过滤对任意真实 key 都判 key>MaxKey
而整段跳过该文件,导致 standalone 重启后所有已刷盘 key 全部读不到(实测 45/50 丢失,
仅剩还在 memtable/WAL 的尾部数据)。

修复:LoadSSTableMetaList 直接用块索引末项的 LastKey 作 MaxKey 并置 MaxKeyLoaded=true,
绕开会越界的 EnsureMeta 扫描。老格式无 footer(loadBlockIndexFromFile 返回 nil)时
保留原懒加载兜底——纯数据文件的顺序扫描本就正确。

新增 TestReloadRecoversFlushedKeys:小阈值逼多轮刷盘,停机后同目录重载,断言 50/50 恢复。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NeverENG
NeverENG merged commit 16ea756 into main Jul 19, 2026
3 checks passed
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@NeverENG, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 50591b6e-97d5-4a6d-8587-f19881165bcb

📥 Commits

Reviewing files that changed from the base of the PR and between 2ba209c and ad63088.

📒 Files selected for processing (2)
  • storage/zstorage/SSTable.go
  • storage/zstorage/reload_recover_test.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sstable-reload-maxkey

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

🐯 BanGD 数据库内核评审

整体风险:🔴 高

变更总结:该 PR 修复了一个致命的 SSTable 加载 bug:重启后已刷盘数据全部丢失。根因是 LoadSSTableMetaList 对 MaxKey 的懒加载走 EnsureMeta 的顺序扫描,而 SSTable 新格式(含块索引/布隆/footer)让扫描越过了数据段边界、错位解析出空 MaxKey,导致 getFromSSTables 的范围过滤将所有 key 整段跳过。修复方式为:LoadSSTableMetaList 中直接从块索引末项的 LastKey 取 MaxKey 并标记已加载,老格式保留 EnsureMeta 顺序扫描兜底。同时新增回归测试 TestReloadRecoversFlushedKeys 验证修复。

本评审不阻塞合入;架构级建议以 Issue 形式跟踪,普通问题在下方内联列出。

架构问题(共 3 项)

普通问题(共 2 项)

⚠️ [重要 · 逻辑错误] storage/zstorage/SSTable.go:148 loadBlockIndexFromFile 被重复调用,第一次的结果未缓存

  • 第 136 行调用了 ss.loadBlockIndexFromFile(fullPath) 来获取 MaxKey,该函数内部会读取并解析整个块索引并返回。但是这份返回的 idx 切片没有写入 ss.indexCache。随后第 149 行 go ss.getBlockIndex(meta.Filepath) 会因缓存 miss 而再次调用 loadBlockIndexFromFile,导致同一文件的块索引在启动时被重复解析两次。第二次可能因 ss.idxMu 的并发无重复读保护(getBlockIndex 中用 RWMutex 做 miss-then-load,多个 goroutine 可能同时 miss),造成多次重复读盘。
  • 建议:在 loadBlockIndexFromFile 调用后、启动异步预热前,将 idx 写入 indexCache:
if idx := ss.loadBlockIndexFromFile(fullPath); len(idx) > 0 {
    meta.MaxKey = idx[len(idx)-1].LastKey
    meta.MaxKeyLoaded = true
    ss.idxMu.Lock()
    ss.indexCache[fullPath] = idx
    ss.idxMu.Unlock()
}

然后第 149 行的 go ss.getBlockIndex(meta.Filepath)LoadSSTableMetaList 中可改为条件执行:只在未缓存时启动 goroutine(或者直接移除,因为已缓存)。

💡 [建议 · 错误处理] storage/zstorage/reload_recover_test.go:15 全局 config 被测试修改但 defer 恢复顺序不安全

  • 测试函数在 defer 中恢复 config.G.WALPath/SSTablePath/MaxMemTableSize。如果在测试执行过程中 panic(而非 t.Fatalf),defer 会执行。但 defer 的执行顺序是栈顺序(后进先出),而代码中保存旧值的顺序与 defer 顺序一致,恢复时也是按原序。目前看来没有死锁风险,但若测试并发运行(t.Parallel 未显式调用所以不会,但未来可能有人加),修改全局 config.G 会导致竞态。这是一个测试可维护性问题。
  • 建议:建议使用 defer func() { config.G.WALPath = oldWAL; ... }() 的恢复顺序与原保存顺序一致即可。更进一步的改进:考虑为测试提供临时 config 替换的 helper(如 t.Setenv 风格),或者避免在测试中修改全局单例,改为依赖注入的方式(但这是更大范围的改动,当前测试可接受)。

本次评审消耗 token:共 62015 tokens(输入 57113,输出 4902,缓存命中 0,缓存写入 0)|维度 [concurrency, memory, lock, storage, performance]|对抗式复核 3 票/条,过滤疑似误报 0 条

NeverENG added a commit that referenced this pull request Jul 19, 2026
记录本轮从「PUT 225 qps 反常」出发的完整排障链:实测归因 fsync 瓶颈→
group commit→写恢复测试挖出 SSTable 重载致命 bug(重启丢数据)→WAL 自清洁→
自审堵上原子重写窗口。关联 PR #150/#153/#157/#159/#158。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant