ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Turso(Limbo)MVCC 恢复与检查点语义深度解析:基于持久化制品与 WAL-Last 排序的崩溃安全设计

Turso(Limbo)MVCC 恢复与检查点语义深度解析:基于持久化制品与 WAL-Last 排序的崩溃安全设计 TursoLimboMVCC 恢复与检查点语义深度解析基于持久化制品与 WAL-Last 排序的崩溃安全设计【免费下载链接】tursoA SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.项目地址: https://gitcode.com/GitHub_Trending/tu/turso本文围绕 TursoLimbo一个用 Rust 实现、兼容 SQLite 的数据库MVCC 模块的恢复与检查点语义展开讲解其如何在.db、.db-wal、.db-log与元数据表四个持久化制品之间建立一致的崩溃恢复边界。读者将掌握其启动状态分类逻辑、stop-the-world 阻塞式检查点的执行序列、逻辑日志logical log的磁盘格式与 CRC 链校验机制以及SyncMode::Off等配置对持久性的影响。文档主体源自 RECOVERY_SEMANTICS.md并辅以 core/mvcc/database/mod.rs、core/mvcc/database/checkpoint_state_machine.rs、core/mvcc/persistent_storage/logical_log.rs 的源码级佐证。MVCC 恢复的总体设计Turso 的 MVCC 模块在 SQLite 兼容层之上维护一份事务日志每个已提交事务把行级操作upsert / delete追加到独立的逻辑日志中而数据本身的物理页仍由 pager / WAL 负责。文档 DESIGN.md 用一个跨三个事务的例子说明了该模型T0 插入id1、T1 插入id2、T2 执行DELETE WHERE id1与INSERT (id) VALUES (3)其中删除只是把行版本的 end 时间戳改为当前事务号而非立即物理抹除。当数据库启动时MVCC 模块通过重放redo事务日志把内存状态恢复到崩溃前的提交边界。因此恢复正确性的关键在于什么时候该相信 WAL什么时候该相信逻辑日志以及二者之间的衔接点在哪里。整个恢复/检查点语义就是围绕这个核心问题设计的。四个持久化制品与恢复决策的输入启动决策依赖四个持久化制品见 RECOVERY_SEMANTICS.md制品路径作用主数据库文件.db已被检查点固化进 B-tree 的数据页WAL 文件.db-wal尚未检查点的物理页提交SQLite 兼容层MVCC 逻辑日志.db-log已提交 MVCC 事务的操作帧行级MVCC 元数据表行__turso_internal_mvcc_meta(kpersistent_tx_ts_max)持久化的重放边界durable replay boundary源码中元数据表名与键名分别以常量形式定义在 core/mvcc/database/mod.rsMVCC_META_TABLE_NAME __turso_internal_mvcc_meta、MVCC_META_KEY_PERSISTENT_TX_TS_MAX persistent_tx_ts_max。该表在检查点过程中通过 pager 事务写入 WAL进而被检查点进主数据库文件因此persistent_tx_ts_max的存储位置本质上是主数据库文件/WAL——它与数据页共享同一条物理提交链路。persistent_tx_ts_max是整个恢复系统的重放边界恢复阶段只重放commit_ts persistent_tx_ts_max的逻辑日志帧早于该边界的帧早已被检查点固化进数据库文件重放它们反而会引入重复写入。逻辑日志头部与帧格式逻辑日志的头部固定为 56 字节源码常量LOG_HDR_SIZE 56见 core/mvcc/persistent_storage/logical_log.rs字段如下magic: u32LOG_MAGIC 0x4C4D4C32即 LML2 小端字节序version: u8当前格式版本LOG_VERSION 3历史版本 v2 亦被识别flags: u8bit 1..7 必须为 0bit 0 当前保留hdr_len: u16必须 56salt: u64每次日志截断时重新生成的随机盐reserved: [u8; 36]当前格式必须为 0hdr_crc32c: u32该字段自身置零后对头部计算 CRC32C帧与帧之间的 CRC 采用链式校验crc32c_append(prev_frame_crc, data)首帧的种子由盐派生crc32c(salt.to_le_bytes())。这意味着任何对帧序列的篡改、丢失或乱序都会被链式 CRC 侦测到。盐在每次日志截断truncate时重新生成截断后旧链立即失效从根上杜绝了旧帧被误认为新帧的可能。每个事务帧TX Frame的布局为┌──────────────┬──────────────────────────────┬───────────┐ │ TX Header │ Payload (Op 条目, 可变长) │ Trailer │ │ (24B 明文) │ Op₀ | Op₁ | Op₂ | ... │ CRC End │ └──────────────┴──────────────────────────────┴───────────┘TX Header24 字节frame_magic(u32)、payload_size(u64)所有 op 条目的总字节数加密前计算、op_count(u32)、commit_ts(u64)。当存在可移植扩展块时帧头部扩展为 40 字节TX_EXT_HEADER_SIZE额外携带extension_size(u64)、extension_record_count(u32)与frame_flags(u32)。Payload未加密时是op_count个 op 条目每个条目为tag(u8)flags(u8)table_id(i32必须为负)payload_len(sqlite varint)payload。op 类型包括OP_UPSERT_TABLErowid_varint || table_record_bytes、OP_DELETE_TABLErowid_varint、OP_UPSERT_INDEX、OP_DELETE_INDEX。OP_FLAG_BTREE_RESIDENT表示该行在 MVCC 开始跟踪之前已存在于 B-tree 中恢复时必须保留该位因为检查点/GC 逻辑依赖它。TX Trailer8 字节crc32c(u32)覆盖 TX Header 与 Payload 的链式 CRC与end_magic(u32)。开启加密时payload 按固定 32 KB 明文块分块每块独立执行 AEAD 加密后以ciphertext(plain_len tag_size) | nonce落盘。日志头、TX 头与 TX trailer 始终明文日志头的 salt 与 TX 头的op_count、commit_ts、最终块的明文长度作为 AEAD 附加数据AAD绑定进密文任何对它们的篡改都会导致解密失败。op 可能跨越 32 KB 分块边界读取端用 carry buffer 跨块拼接后再解析详见 core/mvcc/persistent_storage/logical_log.rs。启动Bootstrap顺序MvStore::bootstrap()在 core/mvcc/database/mod.rs 中实现同步 shim最终由bootstrap_nonblock状态机驱动其执行顺序为maybe_complete_interrupted_checkpoint()若上次检查点被中断先完成 WAL→DB 回填与 WAL 截断的协调reparse_schema()重新解析 schema确保元数据表存在在非法状态下 fail closed依据 schema 构建内存中的 table-id / root-page 映射bootstrap_map_root_pagestable_id -1 * root_pagemaybe_recover_logical_log()按元数据边界重放逻辑日志把 bootstrap 连接提升为常规 MVCC 连接。关键约束是任何已提交的 WAL 状态都会在逻辑日志重放之前先被对账reconcile重放边界来自元数据行。从源码看maybe_complete_interrupted_checkpoint_nonblockcore/mvcc/database/mod.rs先尝试读取逻辑日志头try_read_header_nonblock并结合wal.get_max_frame_in_wal()判断 WAL 中是否有已提交帧WAL 有已提交帧且日志头有效复用现有头保持 salt 链驱动wal.checkpoint(CheckpointMode::Truncate)回填若无法完整回填则返回Corrupt错误WAL 有已提交帧但日志头缺失除非开启experimental_mvcc_passive_checkpoint实验特性否则返回CorruptWAL has committed frames but logical log header is missingWAL 有已提交帧但日志头损坏/撕裂fail closed返回CorruptWAL 无已提交帧直接截断/丢弃 WAL 尾部字节DriveEarlyTruncate继续逻辑日志恢复。启动状态分类7 种情形恢复逻辑用两个检查对启动状态分类WAL 是否含有已提交帧wal.get_max_frame_in_wal() 0try_read_header()的返回值Valid、Invalid或NoLog。情形启动时的制品状态恢复行为1WAL 有已提交帧 日志头有效完成中断的检查点WAL 回填进 DB、fsync DB、截断 WAL随后按元数据边界执行逻辑日志恢复2WAL 有已提交帧 日志头缺失NoLogFail closed报Corrupt3WAL 有已提交帧 日志头无效/撕裂Fail closed报Corrupt4WAL 无已提交帧截断/丢弃 WAL 尾部字节继续逻辑日志恢复5无 WAL 日志头无效/撕裂Fail closed报Corrupt6无 WAL 头有效但无帧大小 LOG_HDR_SIZE无需重放时间戳状态取自元数据行7无 WAL 空日志0 字节 /NoLog若元数据行存在则从中加载时间戳状态不重放需要特别说明的边界语义检查点完成后日志被截断为 0 字节重启时即落入情形 7——这是正常稳态不是异常日志体中的撕裂尾部被当作 EOF 处理其前的帧仍然有效前缀保留与 SQLite WAL 的可用性语义一致前向扫描中遇到的第一个无效帧即终止扫描前缀保留不尝试从无效帧之后继续当元数据表理应存在而元数据行缺失或损坏时视为损坏fail closed。这套分类的核心思想是能确定性地恢复就恢复无法确定就明确报错绝不进行尽力而为的模糊恢复。例如情形 2/3 中WAL 存在已提交帧而逻辑日志头缺失或撕裂说明崩溃发生在检查点协调的关键窗口内且状态不可判读此时宁可报Corrupt也不冒险以避免数据损坏的传播。检查点序列阻塞式stop-the-world模型恢复语义文档明确指出检查点模型是stop-the-world阻塞式检查点锁。一次完整检查点的 11 步序列如下获取阻塞式检查点锁开启 pager 事务把已提交的 MVCC 表/索引版本写入 pager在同一 pager 事务内 upsert 元数据行persistent_tx_ts_max提交 pager 事务——此时 WAL 中同时含有数据帧与元数据行的已提交帧内存中的durable_txid_max在该转换点上推进检查点 WAL把 WAL 帧回填进 DB 文件fsync DB 文件除非SyncMode::Off把逻辑日志截断为 0内存中重新生成 salt头部随下一帧写入fsync 逻辑日志除非SyncMode::Off截断 WAL收尾GC 已检查点的版本释放锁。第 4 步是整条链的锚点persistent_tx_ts_max与数据在同一 pager 事务内原子推进因此二者要么同时持久化、要么同时缺失不存在数据已固化但边界未更新或反过来的窗口。测试test_meta_checkpoint_case_10_metadata_upsert_is_atomic_with_pager_commitcore/mvcc/database/tests.rs专门验证这一点。WAL 截断必须是最后一步——直到 DB 文件与逻辑日志清理都持久化完成之前WAL 始终保持权威恢复来源的地位。这正是WAL-Last排序的含义若崩溃发生在第 8/9 步之间逻辑日志虽已清空但 WAL 中的元数据边界与数据帧仍可重放恢复不会丢失任何已提交状态。测试test_checkpoint_truncates_wal_lastcore/mvcc/database/tests.rs断言了必须先在逻辑日志截断状态下见到 WAL 仍然存在随后 WAL 才被截断为 0这一顺序。从源码看上述序列被实现为显式状态机CheckpointStateMachinecore/mvcc/database/checkpoint_state_machine.rs其状态枚举完整对应了每个阶段PrepareCheckpoint→AcquireLock→BuildLocalSchemaView→CollectTableRows→CollectIndexRows→BeginPagerTxn→WriteRow/WriteIndexRow含删除状态机→CompactSequences→CommitPagerTxn→CheckpointWal→SyncDbFile→TruncateLogicalLog→FsyncLogicalLog→TruncateWal→GcTableRows/GcIndexRows→Finalize。其中SyncDbFile状态的注释明确指出其动机如果在 WAL 截断之后、DB fsync 之前崩溃数据就会丢失因此必须先 fsync DB 再截断 WAL。状态机还内置了CheckpointYieldPoint以支持测试注入 yieldcheckpoint_state_machine.rs从而可以精确验证持久化边界推进后、“pager 提交前”等关键窗口的崩溃行为。正确性不变量恢复与检查点设计必须满足以下 9 条不变量源自 RECOVERY_SEMANTICS.md启动要么到达一个一致状态要么 fail closed——不存在尽力而为的模糊态已提交的 WAL 状态绝不被忽略逻辑日志中无效的尾部帧绝不重放撕裂或无效的尾部字节绝不被解释为已提交操作重放仅应用commit_ts persistent_tx_ts_max的帧检查点期间persistent_tx_ts_max与 pager 提交原子推进同进程检查点重试从 pager 已提交的边界继续——即使后续检查点阶段失败也不会重复应用已固化的数据逻辑时钟重置为max(persistent_tx_ts_max, max_replayed_log_commit_ts) 1中断检查点对账完成后WAL 被截断。第 8 条在源码中有直接对应maybe_recover_logical_log重放完成后调用clock.reset(persistent_tx_ts_max 1)core/mvcc/database/mod.rs且带断言重放时钟不得回卷到元数据边界之下max_commit_ts_seen persistent_tx_ts_max见 mod.rs。逻辑日志恢复还会把写偏移恢复到last_valid_offset最后一个有效帧的结束位置使后续写入直接覆盖撕裂尾部字节而非在其后追加logical_log.rs。SyncMode::Off 下的行为SyncMode::Off跳过 fsync 调用检查点第 7、9 步以及每次提交后的 fsync 均被跳过。这削弱的是持久性崩溃时已提交但尚未落盘的数据可能丢失。但它不改变逻辑顺序也不改变 fail-closed 的校验行为——日志/帧结构校验、CRC 校验、启动状态分类依然照常执行。也就是说SyncMode::Off影响的只是数据能撑过多长时间不落盘而不是恢复时如何解读磁盘状态。从源码看检查点回填后need_db_sync的判断条件正是get_sync_mode() ! SyncMode::Off wal_checkpoint_backfilled 0core/mvcc/database/mod.rs且connection.get_sync_mode()被传入 WAL 检查点与状态机以控制 fsync 时机。测试覆盖如何验证崩溃安全恢复语义的每一项论断都有对应测试锚定主要位于 core/mvcc/database/tests.rstest_bootstrap_completes_interrupted_checkpoint_with_committed_waltests.rsWAL 含已提交帧 日志有效时重启后数据完整、日志保留、WAL 被截断为 0test_bootstrap_recovers_committed_wal_without_log_fileWAL 有提交但日志文件缺失的对账路径test_bootstrap_rejects_torn_log_header_with_committed_waltests.rs日志头撕裂 WAL 有提交 → fail closedtest_bootstrap_rejects_corrupt_log_header_without_waltests.rs无 WAL 日志头损坏 → fail closed情形 5test_bootstrap_handles_committed_wal_when_log_truncatedtests.rs日志被截断0 字节时 WAL 提交的协调test_bootstrap_ignores_wal_frames_without_commit_markertests.rs无提交标记的 WAL 帧被忽略test_empty_log_recovery_loads_checkpoint_watermarktests.rs空日志从元数据行加载检查点水位情形 7test_meta_checkpoint_case_10_metadata_upsert_is_atomic_with_pager_committests.rs元数据 upsert 与 pager 提交的原子性test_meta_checkpoint_case_11_auto_checkpoint_failure_after_commit_remains_recoverabletests.rs自动检查点在提交后失败仍可恢复不变量 7。逻辑日志本身的损坏与撕裂尾部测试则位于 core/mvcc/persistent_storage/logical_log.rs 的测试模块中覆盖last_valid_offset恢复、链式 CRC 校验、加密分块解析等。配合状态机中的注入式 yield 点测试能够精确模拟任意阶段崩溃的中间状态验证恢复路径的确定性。小结TursoLimbo的 MVCC 恢复语义可以概括为三句话以persistent_tx_ts_max为锚它存储在主数据库文件/WAL 中与数据在同一 pager 事务内原子推进是所有重放决策的边界以 WAL 为兜底检查点序列坚持 WAL-Last 截断逻辑日志清理完成前 WAL 始终是权威恢复来源以 fail-closed 为底线任何无法确定性判读的制品组合日志头撕裂、元数据缺失、WAL 提交与日志不一致都直接报Corrupt绝不进入模糊恢复状态。这套设计把崩溃后能恢复到哪一步从概率问题变成了确定性问题为 GC.md 所描述的版本回收与更上层的并发事务模型提供了坚实的持久性基础。读者若想深入实现细节建议按 RECOVERY_SEMANTICS.md → core/mvcc/persistent_storage/logical_log.rs → core/mvcc/database/mod.rs → core/mvcc/database/checkpoint_state_machine.rs → core/mvcc/database/tests.rs 的顺序阅读。【免费下载链接】tursoA SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.项目地址: https://gitcode.com/GitHub_Trending/tu/turso创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表