
RocksDB db_stress 崩溃恢复验证Expected-State Trace 前缀恢复机制深度解析【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb导读本文基于 docs/components/stress_test/expected_state_trace.md 展开深入剖析 RocksDBdb_stress在模拟未同步数据丢失unsynced data loss场景下如何通过期望状态expected-state快照 写轨迹trace回放的侧车sidecaroracle 机制验证崩溃恢复满足无洞no hole前缀语义。读完本文你将掌握--expected_values_dir、--sync_fault_injection、--disable_wal、--manual_wal_flush_one_in等参数背后的完整生命周期、LATEST.state/N.state/N.trace各文件的作用与不变量、SaveAtAndAfter()/Restore()的精确算法以及如何用trace_analyzer离线检查崩溃轨迹。核心实现位于 db_stress_tool/expected_state.cc、db_stress_tool/expected_state.h 与 db_stress_tool/db_stress_driver.cc。背景LATEST.state作为 oracle 的局限LATEST.state是db_stress常规验证所依赖的预言机oracle它为每个逻辑 key 保存最新期望值验证阶段将数据库中的实际值与其比对。这在恢复必须精确保留最新状态的前提下是足够的。但在以下测试模式下允许丢失部分缓冲写入恢复后数据库可以回到最近写入的某个较早前缀--sync_fault_injection注入未同步数据丢失故障--disable_wal完全禁用 WAL--manual_wal_flush_one_in 0随机显式调用FlushWAL()缓冲的 WAL 可能在崩溃时丢失。上述模式下恢复结果只要求满足无洞属性恢复出的写入必须是崩溃前所有写入的前缀不允许恢复出较新写入的同时丢失较旧的写入。单一的LATEST.state快照无法刻画这种任意前缀语义因此需要一套基线快照 写轨迹回放的机制。FileExpectedStateManager的SaveAtAndAfter()在已知 DB 序列号N处对 oracle 拍照并开始追踪后续写入恢复时根据恢复后的 DB 序列号M回放轨迹中前M - N个写操作来重建 oracle。源码中的类注释对这一契约有精确描述见 db_stress_tool/expected_state.h 中SaveAtAndAfter()与Restore()的 API 文档。该路径何时激活历史状态追踪仅在db_stress使用文件型期望状态管理器时存在即--expected_values_dir非空。轨迹追踪tracing启动需同时满足三个条件压测模式跟踪期望状态IsStateTracked()返回 true--expected_values_dir非空MightHaveUnsyncedDataLoss()返回 true。从源码看MightHaveUnsyncedDataLoss()定义于 db_stress_tool/db_stress_test_base.h当前的判定为FLAGS_sync_fault_injection为 true或FLAGS_disable_wal为 true或FLAGS_manual_wal_flush_one_in 0。这比--expected_values_dir的 flag 帮助文本描述的范围更广——帮助文本仍写着历史值仅在设置--sync_fault_injection时被追踪见 db_stress_tool/db_stress_gflags.cc实际代码已扩展到三类场景。另外需要注意IsStateTracked()的变体差异默认的NoBatchedOpsStressTest返回 true而BatchedOpsStressTest、CfConsistencyStressTest、MultiOpsTxnsStressTest均返回 false见 db_stress_tool/no_batched_ops_stress.cc、db_stress_tool/batched_ops_stress.cc、db_stress_tool/cf_consistency_stress.cc 与 db_stress_tool/multi_ops_txns_stress.h因此这些专用变体不会启用历史轨迹。高层生命周期单个db_stress进程的完整流程如下打开 DBInitDb若存在历史快照/轨迹在启动验证之前先把LATEST.state恢复到与 DB 恢复后的序列号一致FinishInitDb内调用Restore基于重建后的LATEST.state运行验证在 DB 当前序列号处保存新的历史基线并开始追踪新的写入TrackExpectedState→SaveAtAndAfter运行压测操作崩溃或直接重开不显式关闭 trace。db_stress_driver.cc中几个关键的顺序约定见 db_stress_tool/db_stress_driver.ccFinishInitDb()在新一轮的 tracing 启动之前执行TrackExpectedState()在启动验证之后执行避免验证期间的大量Get()/MultiGet()与 DB 全局 trace 互斥锁竞争源码注释明确说明这一点模拟数据丢失的故障注入设置在TrackExpectedState()之后才启用fault_fs-SetInjectUnsyncedDataLoss(...)。该顺序保证在压测开始产生可能丢失的 DB 写入之前侧车 oracle 文件已经就绪。目录内的文件与不变量文件型管理器FileExpectedStateManager在--expected_values_dir目录内使用以下文件文件名常量定义见 db_stress_tool/expected_state.cc文件含义LATEST.state当前期望值 oracle用于常规验证mmap 映射的std::atomicuint32_t数组PERSIST.seqno独立的持久化序列号 oracle 元数据N.state在 DB 序列号N处的期望值历史快照N.trace序列号N之后发生写入的轨迹.name.tmp用于原子替换的临时文件同一时刻只关心一代历史saved_seqno_是*.state文件中LATEST.state除外的最大序列号更旧的*.state与*.trace文件被视为过期并清理。Open()还会修复一种特定的部分保存场景见 db_stress_tool/expected_state.cc 中FileExpectedStateManager::Open()若N.state存在而N.trace不存在则创建一个空的N.trace。这模拟的语义是崩溃发生在基线快照已创建、但 tracing 尚未真正开始之后。注释也说明LATEST.state与PERSIST.seqno的初始化也采用临时文件 rename的方式避免初始化中途被杀留下残缺文件。为什么 oracle 文件位于故障注入路径之外期望状态快照与轨迹均通过Env::Default()写入不走 DB 的故障注入文件系统包装器。这是有意设计这些文件属于测试 oracle 的一部分而不是被验证的数据库状态。如果它们与 DB 文件一样遭受模拟数据丢失那么在最需要 oracle 可靠的时刻 oracle 反而不可靠。此外SaveAtAndAfter()对 trace 文件禁用了WritableFileWriter缓冲soptions.writable_file_max_buffer_size 0从而去除用户态缓冲避免进程被杀时轨迹数据滞留于应用缓冲区。代码中还用一个FatalExpectedStateTraceWriter包装器包裹 trace writer一旦写轨迹失败立即向 stderr 打印错误并std::_Exit(1)——因为期望状态轨迹是崩溃恢复验证的一部分不是尽力而为的可观测性绝不允许历史发生偏离。保存 / 启动轨迹路径SaveAtAndAfterStressTest::TrackExpectedState()调用SharedState::SaveAtAndAfter()后者分发到FileExpectedStateManager::SaveAtAndAfter(DB*)见 db_stress_tool/expected_state.cc。保存路径依次执行读取 DB 序列号N db-GetLatestSequenceNumber()将LATEST.state复制到临时文件将临时文件重命名为N.state创建空的N.trace在 DB 上启动 RocksDB tracing写入N.trace若存在旧的old.state与old.trace将其删除。状态快照通过临时文件 rename原子创建trace 文件直接创建因为空 trace 本身就具有期望的含义崩溃发生在快照之后、tracing 之前等价于空 trace。TraceOptions 是关键过滤读操作设置kTraceFilterGet | kTraceFilterMultiGet | kTraceFilterIteratorSeek | kTraceFilterIteratorSeekForPrev写操作仍然被追踪preserve_write_order true。这里的 filter 位是排除位设置这些位意味着不追踪这些读操作。preserve_write_order true是必需的因为恢复依赖前缀语义——回放前M - N个被追踪的写操作要求轨迹顺序必须与 DB/WAL 的应用顺序一致否则轨迹中可能包含正确写入但顺序错乱前缀回放就会出错。轨迹覆盖契约Trace Coverage Contract对期望状态恢复而言轨迹必须满足如下性质任何可能出现在恢复后 DB 序列/WAL 状态中的写入必须已存在于轨迹中且保持相同的前缀顺序额外的写记录只要出现在恢复前缀之外就是可接受的。等价表述缺失轨迹记录是致命的fatal多余的后缀轨迹记录可容忍tolerated。这直接源于Restore()消费轨迹的方式回放长度取自db-GetLatestSequenceNumber()而非轨迹元数据或显式提交确认随后从轨迹中回放恰好这么多逻辑写操作。由此推出一个重要结论更晚的轨迹点可能严格劣于更早的轨迹点。若崩溃发生在 WAL/序列状态可恢复之后、但侧车轨迹文件尚未写入该记录之前则Restore()会欠回放under-replay验证失败。相反更早的轨迹点可能留下对未能幸存于恢复的写入的尾部记录——只要这些记录保持在恢复序列号所隐含前缀之后即可接受。一句话总结db_stress需要的是保持前缀的超集prefix-preserving superset的可恢复写入而非在轨迹点已知完整完成的写入的精确集合。生产者与消费者的关系该路径的语义由轨迹生产者与期望状态消费者共同定义通用生产者 API生产者使用通用的StartTrace()/Tracer/ReplayerAPI但本路径中活跃的消费者是FileExpectedStateManager::Restore()而非通用查询回放。回放进度来自 DB 序列空间Restore()并非回放到轨迹声称提交为止而是回放db-GetLatestSequenceNumber() - saved_seqno_个逻辑写操作。侧车轨迹文件N.trace通过Env::Default()写入故意位于故障注入 DB 路径之外WAL 持久性与轨迹持久性之间不存在原子耦合。有序前缀语义对本路径而言preserve_write_order意味着恢复的轨迹前缀必须与 DB/WAL 应用顺序一致。它本身并不决定轨迹包含的是已完成写入的精确集合还是可恢复写入的超集——这个要求来自Restore()如何解释轨迹。N.trace里到底有什么N.trace是 RocksDB 通用二进制查询轨迹文件由Tracer产生。在本db_stress路径中它包含一条kTraceBegin头部记录含轨迹 magic 与版本元数据零条或多条kTraceWrite记录可选的一条kTraceEnd尾部记录。由于读轨迹类型已被过滤实际载荷是头 写批次。每条kTraceWrite记录存储时间戳、轨迹类型、载荷 map以及原始WriteBatch::Data()字节。时间戳由通用 tracing 库记录但期望状态恢复路径完全不用时间——它只把Replayer::Prepare()与Replayer::Next()当作轨迹流的解析器使用。为什么截断或无 footer 的轨迹是常态db_stress在正常的崩溃/重开循环中不会显式调用DB::EndTrace()。这意味着轨迹常常没有kTraceEndfooter若进程在写轨迹中途死亡最后一条记录可能只写入了一部分。这不是偶然而是恢复逻辑有意容忍的场景。通用TraceReader在 EOF 处返回Status::Incomplete()通用回放栈已将其识别为未调用EndTrace()就杀掉进程引发的状况。FileExpectedStateManager::Restore()增加了期望状态特有的规则只有在已恢复足够多的写入之后EOF 或尾部损坏才可接受若在回放足量写入之前遇到 EOF则恢复失败若在回放足量写入之后遇到 EOF则恢复成功若在回放足量写入之后遇到尾部记录损坏恢复同样成功。源码中对应逻辑为tolerated_tail_corruption s.IsCorruption() handler_done且s.IsIncomplete()一律视为正常终止见 db_stress_tool/expected_state.cc 的Restore()回放循环。这是轨迹只需好到恢复 DB 序列号为止这一核心结论的落地。恢复路径Restore下一轮运行时FinishInitDb()检查shared-HasHistory()若存在历史则在常规验证之前、以及共享状态被挂载到 compaction filter factory 之前调用shared-Restore(db_)见 db_stress_tool/db_stress_test_base.cc。Restore(DB*)的步骤见 db_stress_tool/expected_state.cc读取恢复后的 DB 序列号M db-GetLatestSequenceNumber()要求M saved_seqno_否则 DB 回滚到比最旧可恢复基线更早的位置恢复失败返回Status::Corruption(DB is older than any restorable expected state)计算replay_write_ops M - saved_seqno_将saved_seqno_.state复制为临时LATEST.state打开saved_seqno_.trace构建默认Replayer调用Prepare()并反复调用Next()解码轨迹记录将每条解码出的TraceRecord交给自定义 handler更新临时期望状态文件一旦恰好应用了replay_write_ops个逻辑写操作恢复即拥有足够信息对 EOF 或尾部损坏转为容忍将临时LATEST.state原子 rename 到位删除saved_seqno_.state删除早于saved_seqno_.trace的旧轨迹但保留刚回放过的轨迹本身以便调试清除saved_seqno_。一个重要的细节默认Replayer并不用于对 DB 执行被追踪的操作只用于解析头部与记录格式。Restore()通过Next()取出TraceRecord然后自行调用record-Accept(custom_handler, result)。注意若replay_write_ops 0DB 恰好恢复到saved_seqno_则跳过打开与回放轨迹直接走状态提升与清理流程。回放如何更新 oracleExpectedStateTraceRecordHandler同时实现两个接口见 db_stress_tool/expected_state.ccTraceRecord::HandlerWriteBatch::Handler通用轨迹层向它提供解码后的TraceRecord对写记录它从追踪字节构造WriteBatch并迭代批次让 handler 逐个处理批次条目。读轨迹类型被忽略——实践中它们不应出现因为 trace options 已过滤但 handler 仍对其容忍。Key 解码handler 不在期望状态 oracle 中存储原始 RocksDB key而是把追踪的用户 key 映射回db_stress的逻辑整数 key去掉追踪 key 上的用户时间戳后缀StripTimestampFromUserKey配合FLAGS_user_timestamp_size用GetIntVal()解析剩余用户 key用得到的逻辑 key ID 修改期望状态数组。这也是调试日志关注两类问题的原因解析失败、原始 key 到逻辑 key 的往返不匹配roundtrip 检查会比较追踪原始 key 与Key(parsed_id)。每操作语义handler 只回放 oracle 所需的逻辑效果PutCF/TimedPutCF解析逻辑 key从追踪值字节读取value_base调用ExpectedState::SyncPut()PutEntityCF反序列化宽列实体WideColumnSerialization::DeserializeSimple校验列一致性取默认宽列值得到value_base调用SyncPut()DeleteCF解析逻辑 key调用SyncDelete()SingleDeleteCF在非预备事务prepared transaction外按DeleteCF回放在预备事务内则缓冲原始 single-delete 形式直到提交DeleteRangeCF解析 begin/end 逻辑 key调用SyncDeleteRange(begin, end)即使它影响多个逻辑 key也只计为一次回放的写操作MergeCF按PutCF回放——这与db_stress的 merge 算子一致其合并值由最新操作数推导而非更复杂的累积规则PutBlobIndexCFblob 直写追踪记录的是转换后的BlobIndex而非原始用户值字节因此 handler 将其视为对该逻辑 key 的又一次 put基于现有期望值推导下一个value_basestate_-Get(...).NextValueBase()。预备事务Prepared Transactions预备事务需要额外处理因为轨迹中可能包含 prepare 与 commit 标记而非立即应用的写入。handler 按事务 ID 在内存中缓冲预备写入MarkBeginPrepare()开始向临时WriteBatch缓冲缓冲期间遇到的写条目追加到该批次MarkEndPrepare(xid)将缓冲批次存入 mapMarkCommit(xid)将存储的批次重新喂给同一 handler 回放MarkRollback(xid)丢弃存储批次而不应用。这样期望状态 oracle 反映的是提交语义而非 prepare 时的可见性。为什么回放按写操作数计数而非轨迹记录数轨迹流由kTraceWrite记录构成但每条记录包含一个完整的WriteBatch而一个批次可包含多条独立写入条目。因此Restore()使用 handler 已应用的写操作数而非读取的轨迹记录数来计量回放进度目标计数为db-GetLatestSequenceNumber() - saved_seqno_在某个被追踪的WriteBatch内部handler 的Continue()方法在应用足量写操作后停止批次迭代外层恢复循环仍持续读取轨迹记录直到Next()返回 EOF、footer 或损坏届时恢复逻辑判定已消费的轨迹前缀是否足够。离线轨迹检查trace_analyzerN.trace使用 RocksDB 通用二进制查询轨迹格式因此可直接用现成的离线打印工具trace_analyzer源码位于 tools/trace_analyzer.cc转储为可读文本。在添加任何期望状态专属调试日志之前先用它把轨迹导出这是人类与 Agent 检查回放输入的最快途径。构建make -j128 trace_analyzer先创建输出目录再运行mkdir -p /tmp/trace_dump ./trace_analyzer \ -trace_path/path/to/N.trace \ -output_dir/tmp/trace_dump \ -output_prefixN \ -convert_to_human_readable_trace \ -try_process_corrupted_trace \ -no_print这会写出/tmp/trace_dump/N-human_readable_trace.txt。行格式为普通记录hex_key type_id cf_id value_size timestamp_us范围删除begin_hex end_hex type_id cf_id 0 timestamp_us实用 flag-no_key省略十六进制 key 列以减小输出体积-try_process_corrupted_trace对db_stress崩溃轨迹强烈推荐因为其尾部记录可能合法地截断或损坏。两个重要注意事项trace_analyzer要求-output_dir必须已存在期望状态回放只需要轨迹的写前缀但文件格式本身是通用 RocksDB 轨迹格式而非期望状态专属格式。编码在文件删除顺序中的崩溃安全规则代码中有多处刻意安排的删除顺序成功保存新基线后删除旧历史文件的顺序无关紧要——新的一对文件已经建立旧文件不会再被使用即使崩溃恢复成功后先删除旧的N.state再删除旧轨迹若先删轨迹再崩溃将没有任何办法回放到序列号N见Restore()中must delete the state file first的注释。Clean()见 db_stress_tool/expected_state.cc还会清理因Open()或SaveAtAndAfter()中断遗留的过期临时文件早于saved_seqno_的过期历史 state 文件早于saved_seqno_的过期轨迹文件。最小工作示例假设上一轮在序列号100处保存了基线100.state包含序列号 100 处的 oracle 快照100.trace包含序列号 100 之后的写入。随后进程又发出 10 个写操作后崩溃。恢复后的 DB 最新序列号为107。下一轮启动时Restore()将100.state复制为临时LATEST.state读取100.trace将前107 - 100 7个回放的写操作应用到临时 oracle忽略这 7 个操作之后的任何尾部——即使轨迹无 footer 结束或下一条记录被截断将重建后的临时文件 rename 为LATEST.state。重建后的 oracle 与恢复后的 DB 一致启动验证即可检查逻辑洞是否存在即验证不存在较新写入幸存而较旧写入丢失的情况。总结期望状态轨迹逻辑是一个前缀恢复 oracleSaveAtAndAfter()在序列号N处对 oracle 拍照并启动仅写、保序的轨迹过滤读、preserve_write_order true、禁用轨迹文件用户态缓冲Restore()获知恢复后的序列号M将前M - N个被追踪写操作回放到快照上重建LATEST.state只要M所需前缀完好截断或无 footer 的轨迹均可接受因此轨迹必须是可能幸存于恢复的写入的有序超集——精确过滤已成功写入并非正确的目标不变量。正是这套机制让db_stress能够在允许丢失未同步写入--sync_fault_injection、--disable_wal、--manual_wal_flush_one_in的前提下验证恢复结果满足无洞性质而不是要求精确保留最新的未同步写入。所有关键实现均可在 db_stress_tool/expected_state.cc 与 db_stress_tool/db_stress_driver.cc 中逐行核对。【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考