RocksDB 崩溃恢复正确性验证:用 db_stress 检测“丢失的缓冲写入“造成的恢复空洞 RocksDB 崩溃恢复正确性验证用 db_stress 检测丢失的缓冲写入造成的恢复空洞【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb本文围绕 RocksDB 官方在 2022 年 10 月发布的博客《Verifying crash-recovery with lost buffered writes》系统讲解 RocksDB 如何为崩溃后恢复数据出现空洞hole这一正确性隐患设计端到端测试方案包括四种典型崩溃场景的界定、基于轨迹trace重放的测试预言机test oracle扩展以及用TestFSWritableFile在进程内模拟系统崩溃的巧妙手法。读完本文你将理解 RocksDB 崩溃恢复的缓冲语义、为什么恢复结果是写入历史的前缀是必须保证的性质以及如何从源码层面验证db_stress与db_crashtest.py这套测试框架的具体实现。写入路径上的多层缓冲与两种崩溃类型RocksDB 的每一次写入在真正持久化之前会经过多个可能引入缓冲的层级。这些缓冲层会延迟数据的持久化时机而根据缓冲所在位置的不同崩溃时会丢失的数据范围也截然不同进程崩溃process crash只会丢失保存在进程内存中的缓冲写入系统崩溃system crash除了进程内存还会丢失保存在**操作系统内存OS page cache**中的写入因为系统掉电后 page cache 内容同样无法保留。一个写入需要经过的典型持久化路径包括进程内存中的 memtable、WAL 写入缓冲、WAL 文件写入 OS page cache、以及最终落盘。是否调用Sync/Fsync决定了数据是否到达可抗系统崩溃的持久化点。新引入的测试覆盖要验证的核心不变量是在两类崩溃下恢复出来的数据都不能存在空洞。所谓空洞指的是某一条恢复出来的写入比某一条丢失的写入更新即恢复数据在时间线上出现断层有效无空洞的恢复所有恢复的写入1 和 2都早于所有丢失的写入3 和 4无效有空洞的恢复一条恢复出的写入4比一条丢失的写入3更新这个保证对很多应用至关重要例如以恢复出的最新写入作为复制起点的场景如果复制起点处存在空洞从节点可能永远错过某些写操作或者以错误的顺序应用数据。需要说明的是这套测试覆盖对使用场景做了明确假设所有写入使用相同的缓冲/持久化相关选项例如不覆盖WAL 交替开启/关闭的场景即不覆盖WriteOptions::disableWAL交替取值的情况崩溃本身不会带来意外后果例如不会损坏已持久化的数据。为什么验证无空洞很困难多种合法恢复结果测试恢复无空洞的难点在于合法的恢复结果不止一种。由于缓冲层可能丢失任意一部分写入恢复后 DB 中既可能出现最新的值也可能出现较早的值只要满足没有空洞即可——即恢复出的写操作必须是写入历史上连续的一段前缀。传统方案是逐 key 校验最新值但它在丢失缓冲写入的场景下会失效恢复出旧值是被容忍甚至预期的行为。因此需要一种能校验恢复结果 写入历史前缀的机制。RocksDB 的解法是追踪trace所有写入操作然后验证恢复结果恰好匹配写入轨迹的一个前缀。只要恢复出的写操作数量为 M由 DB 自身的 sequence number 决定并且这 M 个写操作与轨迹中的前 M 个操作完全一致就能证明恢复无空洞。覆盖的四种崩溃场景测试覆盖从以下四种新场景开始它们已被纳入 RocksDB 内部 CI周期性对 main 分支最新提交运行。四种场景分别组合了不同的缓冲/持久化选项与崩溃类型#场景涉及选项会丢失的写入范围1WAL 关闭 进程崩溃WriteOptions::disableWAL1自上次 memtable flush 以来的写入2WAL 开启 系统崩溃WriteOptions::disableWAL0配合WriteOptions::sync1、SyncWAL()或FlushWAL(true /* sync */)自上次 memtable flush 或 WAL sync 以来的写入3手动 WAL flush 进程崩溃DBOptions::manual_wal_flush1自上次 memtable flush 或手动FlushWAL()以来的写入4手动 WAL flush 系统崩溃DBOptions::manual_wal_flush1自上次 memtable flush 或已 sync 的手动 flushFlushWAL(true)或FlushWAL(false)之后再做 WAL sync以来的写入这些选项在 db_stress 的 gflags 定义 中均有对应入口DEFINE_bool(disable_wal, false, If true, do not write WAL for write.)见 db_stress_gflags.cc 第 799 行对应场景 1/2manual_wal_flush_one_in见 db_stress_gflags.cc 第 97 行以概率触发手动 WAL flush 路径且设为大于 0 即隐含Options::manual_wal_flush true对应场景 3/4expected_values_dir见 db_stress_gflags.cc 第 740 行指定保存历史期望值.state/.trace 文件的目录是场景 3/4 等历史校验能力的基础。新覆盖发现并修复的真实问题这套覆盖上线后立刻发挥了价值暴露了三个此前未被发现的缺陷均为 RocksDB 官方 Pull Request编号与标题如下PR #10185track_and_verify_wals_in_manifest与 WAL sync 之间存在竞态导致系统崩溃后误报数据损坏false detection of corruptionPR #10560WAL sync 过程中的竞态导致系统崩溃后出现未被检测到的恢复空洞undetected hole in recoveryPR #10573关键元数据文件缺少目录 syncdirectory sync导致系统崩溃后恢复失败recovery failure。其中第二个问题尤其值得警惕——它说明空洞缺陷是真实存在的而不仅仅是理论风险正因如此恢复必须是轨迹前缀这类结构性验证才必不可少。方案总览db_stress 压力程序 db_crashtest.py 崩溃注入脚本正确性测试框架由两部分组成db_stress压力测试程序见 db_stress_tool/db_stress_tool.cc负责操作 DB 并维护测试预言机oracledb_crashtest.py包装脚本见 tools/db_crashtest.py负责管理多个db_stress实例——启动它们、注入崩溃。工作流程如下启动时db_stress依据测试预言机校验 DB跳过上次崩溃时仍有 pending 写入的 key随后db_stress对 DB 施加随机读写压力并持续更新测试预言机。这里的预言机就是Latest values file最新值文件——正如其名它只记录每个 key 的最新值。正因如此这套基础设置无法验证涉及丢失缓冲写入的恢复场景当允许恢复出旧值时逐 key 的最新值比对就不再适用。关键扩展一verifiedSeqno 快照与轨迹重放为了让预言机支持恢复结果必须是无空洞前缀的校验RocksDB 将预言机扩展为新增两个文件verifiedSeqno.state在 sequence number 为verifiedSeqno时的期望值文件快照。verifiedSeqno是上一次成功校验时的 DB sequence numberverifiedSeqno.trace该 sequence number 之后所有操作的轨迹文件。在 expected_state.cc 中可以看到这些文件约定的直接实现见 第 231–237 行const std::string FileExpectedStateManager::kStateFilenameSuffix .state; const std::string FileExpectedStateManager::kTraceFilenameSuffix .trace; const std::string FileExpectedStateManager::kPersistedSeqnoBasename PERSIST;恢复路径由 DB 学习 M由文件系统学习 N重放 M−N 个操作当上一个db_stress实例可能丢失了缓冲写入时当前实例必须在启动校验前重建最新值文件。这里定义两个关键序列号M当前db_stress实例的恢复 sequence number从 DB 自身学习即db-GetLatestSequenceNumber()N上一个db_stress实例的恢复 sequence number从文件系统学习——通过解析*.{trace,state}文件名获得。之后LATEST.state最新值文件即可通过在 N.state 之上重放 N.trace 中前 M−N 个追踪到的操作来重建。由于 M 是 DB 实际恢复出的写入数重放出的期望值与 DB 逐 key 比对一致即可证明恢复是轨迹的前缀、无空洞。这一逻辑在源码中对应FileExpectedStateManager::Restore()见 expected_state.cc 第 777 行它用db-GetLatestSequenceNumber()计算replay_write_ops seqno - saved_seqno_通过ExpectedStateTraceRecordHandler处理轨迹记录中的写操作Put/Delete/DeleteRange/Merge/PutEntity 等见 expected_state.cc 第 474 行 起的 Handler 实现把轨迹中的写操作按序应用到期望状态上同时它还妥善处理了若干边界情况例如轨迹末尾因db_stress崩溃写入而损坏的记录——只要已重放出所需数量的写操作尾部损坏可以被容忍见 expected_state.cc 第 842–870 行。写入路径保存 M.state 并从 M.trace 开始记录反过来当当前db_stress实例可能丢失缓冲写入时即开启历史校验的实例运行时它会把当前期望值保存为 M.state开始在 M.trace 中记录更新的操作。对应实现是FileExpectedStateManager::SaveAtAndAfter()见 expected_state.cc 第 387 行。值得注意的是其中两个工程细节原子性seqno.state 先写临时文件再RenameFile()原子改名避免进程在初始化中途被杀导致文件不完整若崩溃恰好发生在 state 创建之后、trace 创建之前则按trace 存在但为空处理见 expected_state.cc 第 400–420 行轨迹的可靠性轨迹写入被封装为FatalExpectedStateTraceWriter——一旦轨迹写失败进程直接std::_Exit(1)终止绝不继续运行导致历史分叉见 expected_state.cc 第 353–383 行。同时轨迹过滤掉 Get/MultiGet/IteratorSeek 等读操作只保留写操作见 expected_state.cc 第 435–444 行。这套历史恢复机制在db_stress的共享状态中也有体现db_stress_shared_state.h提供SetPersistedSeqno()记录已持久化的序列号见 db_stress_shared_state.h 第 448 行并由 db_stress_listener.h 中的监听器在每次写提交时同步见 db_stress_listener.h 第 75 行。关键扩展二TestFSWritableFile 在进程内模拟系统崩溃直接触发真实系统崩溃在运维上极其困难。RocksDB 的解法是在进程内模拟让本应写入 OS page cache 的未同步数据滞留在进程内存中从而进程崩溃即可等价于系统崩溃。实现方式是引入TestFSWritableFile见 utilities/fault_injection_fs.h 第 245 行其核心行为与文档描述完全一致Append()把写入缓冲在本地std::string即 fault_injection_fs.h 第 228 行 的buffer_中而不是调用write()落盘Sync()才把本地std::string的内容转交给PosixWritableFile::Append()后者真正write()到 OS page cache。这样一来现有的db_stress进程崩溃机制直接 kill 进程天然会丢失所有未 sync 的写入恰好模拟出系统崩溃中page cache 数据丢失的效果。该文件在注释中明确说明了设计意图数据先缓存在 buffer 中只有调用 Sync 时才持久化可模拟未被 Sync 保护的文件数据或整个文件的丢失见 fault_injection_fs.h 第 11–13 行。尚未覆盖的保证与后续工作作者在Next steps中坦诚指出了该方案目前的边界用户显式 flush 的写入必须全部恢复一个尚未被测试覆盖的保证是——RocksDB 会恢复所有用户显式从崩溃丢失的缓冲中 flush 出去的写入。由于内部也可能触发缓冲 flush实际恢复出的写入可能多于这个下界但绝不应少于。为此预言机需要进一步扩展追踪预期能在崩溃中存活的 sequence number 下界更真实的系统崩溃模拟当前模拟只丢弃未同步的普通文件数据尚未覆盖未同步的目录项directory entries。而目录项丢失恰恰是上面 PR #10573关键元数据文件缺目录 sync 导致恢复失败所涉及的问题类型将其纳入模拟是后续的重要方向。致谢与参考实现该功能的落地离不开多位贡献者Hui Xiao 增加了手动 WAL flush 覆盖并让方案兼容TransactionDBZhichao Cao 实现了系统崩溃模拟多位 RocksDB 团队成员贡献了该特性所依赖的基础设施。感兴趣的读者可以继续在仓库中探索以下实现与测试文件测试预言机核心db_stress_tool/expected_state.cc.state/.trace 文件的创建、保存与重放逻辑与 db_stress_tool/expected_state.h压力测试入口与选项db_stress_tool/db_stress_tool.cc、db_stress_tool/db_stress_gflags.cc、db_stress_tool/db_stress_test_base.cc崩溃注入脚本tools/db_crashtest.py系统崩溃模拟utilities/fault_injection_fs.h、utilities/fault_injection_fs.cc。【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考