Turso MVCC 可移植逻辑日志格式(PORTABLE_FORMAT)深入解析:让原始日志消费者在数据库实例之外解读恢复流
【免费下载链接】tursoA SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.项目地址: https://gitcode.com/GitHub_Trending/tu/turso
导读
Turso 的 MVCC 恢复日志是数据库唯一的操作流(operation stream)。当原始日志消费者(raw-log consumer)需要在原始数据库实例之外解读这条恢复流时,内存中的 schema 与 MVCC table-id 映射已不可用。本文以 docs/internals/mvcc/PORTABLE_FORMAT.md 为骨架,结合turso_core中core/mvcc/portable_logical.rs、core/mvcc/persistent_storage/logical_log.rs及 serializer.rs 等源码,完整讲解 Portable MVCC Log Metadata(可移植 MVCC 日志元数据)格式的设计目标、帧布局、恢复操作编码、事务扩展、操作级扩展、提交期构造流程与过滤规则。读完本文,你将掌握如何在恢复 payload 之外用一套紧凑的可移植元数据解释任意 MVCC 事务帧,并理解其与加密、CDC、同步引擎的边界划分。
一、为什么需要可移植元数据:单操作流原则
MVCC 恢复日志是唯一的操作流,这意味着它必须同时服务两类读者:
- 本地恢复(local recovery):在原始数据库实例内,拥有内存 schema 与 MVCC table-id 映射,只需读取恢复操作(recovery ops)。
- 原始日志消费者(raw-log consumer):在数据库实例之外(如 CDC、同步引擎的下游)读取同一份日志,此时 in-memory schema 与 table-id 映射均不可用,必须依赖日志内嵌的可移植元数据来解释每条恢复操作。
设计目标在文档中明确为:
- 保持 MVCC 恢复日志紧凑且权威(compact and authoritative);
- 避免在第二条可移植操作流中重复恢复行、schema 行与头部更新——即不复制一份数据;
- 将负数 MVCC table id与未 checkpoint 的 rootpage解析为可移植对象名;
- 通过通用事务元数据(generic transaction metadata)把同步引擎特有字段隔离在核心格式之外;
- 开启数据库加密时,可移植元数据与恢复 payload一同加密和认证;
- 编码归属
turso_core拥有,不依赖同步引擎或服务器协议类型。
从源码结构看,turso_core通过core/mvcc/portable_logical.rs中的PortableLogicalBuilder独立实现这套编码,而同步引擎侧(如 sync/engine/src/database_sync_operations.rs)则消费OP_FLAG_PORTABLE_EXTENSION标记,印证了"核心格式归 turso_core、同步字段不外泄"的边界设计。
二、帧布局:扩展块先于恢复 payload
一个 MVCC 事务帧包含恢复 payload,并可能带一个扩展块(extension block):
tx header: payload_size // recovery payload bytes op_count commit_ts extension_size extension_record_count frame_flags body: extension records recovery ops trailer: crc/auth tag/end magic- 本地恢复只读取 recovery ops;
- 原始日志消费者按顺序读取同一帧:先可移植元数据,再恢复操作,恢复操作中的 table id 通过该元数据解析。
在物理格式上,core/mvcc/persistent_storage/logical_log.rs说明了这一布局的实现:当扩展块存在时,事务体为extension_block || recovery_payload,帧头使用EXT_FRAME_MAGIC标记(区别于紧凑恢复帧的FRAME_MAGIC),帧头 40 字节扩展字段包括extension_size、extension_record_count、frame_flags。
加密时的明文边界
启用加密时,被加密的明文是:
extension_block || recovery_payload因此可移植名称、元数据与操作级扩展字节都不是明文旁路数据(plaintext side data)——它们与恢复数据一起被 AEAD 加密与认证。serializer.rs中的encrypt_payload_in_place展示了分块加密实现:日志头 salt、op_count、commit_ts 与最终块的明文大小作为 AAD 绑定密文,篡改任意字段都会导致解密失败;而日志头、TX 头与 trailer 始终以明文写入。
为什么扩展块必须在前
事务级扩展块故意放在恢复 payload 之前,这样流式消费者可以在读取第一条恢复操作之前,先加载字符串表(string table)与对象映射(object map)。insert_portable_extension(serializer.rs)正是在已序列化的操作流头部之前插入扩展记录,配合with_stable_end_offset的定点迭代(fixpoint iteration),让嵌入的end_offset与帧实际大小自洽(varint 宽度变化会影响帧大小,因此需要迭代到稳定点)。
三、恢复操作(Recovery Ops):全部已提交效果的单一来源
恢复 payload 描述全部已提交效果:
- 表 upsert:
rowid_varint || table_record_bytes - 表 delete:
rowid_varint - 索引 upsert / delete
- 数据库头部更新(database-header updates)
- DDL 对应的
sqlite_schema行 upsert / delete
关键设计是:schema 变更与头部变更不会作为可移植操作重复出现。schema DDL 由恢复流中的sqlite_schema行变更(含存储在这些行中的 SQL)表示;头部更新由恢复头部操作(OP_UPDATE_HEADER)表示。serializer.rs 的serialize_op_entry对应实现了OP_UPSERT_TABLE、OP_DELETE_TABLE、OP_UPSERT_INDEX、OP_DELETE_INDEX四种操作编码,每条操作布局为tag(1) | flags(1) | table_id(4) | payload_len(varint) | payload,且断言 table_id 必须为负数(对应 MVCC 用负 rootpage 表示未 checkpoint 对象的约定)。
四、事务扩展(Transaction Extension):string table / object map / meta
事务扩展是一个在turso_core内部编码的 protobuf 风格信封,当前只包含三类内容:
string_table:帧局部驻留字符串(frame-local interned strings);object_map:从 MVCC table id 到驻留名称的绑定;meta:通用字符串键/值事务元数据。
没有可移植的行、schema 或头部操作。
PortableTxn { end_offset: uint64 // added by the frame wrapper commit_ts: uint64 // added by the frame wrapper repeated string_table: bytes repeated object_map: ObjectMap repeated meta: MetaField } ObjectMap { mv_table_id: sint64 name_ref: uint64 } MetaField { key_ref: uint64 value_ref: uint64 }字段编号与编码细节(源码级印证)
portable_logical.rs定义了确切的 protobuf 字段编号:
- 事务层:
TX_FIELD_STRING_TABLE = 12、TX_FIELD_OBJECT_MAP = 13、TX_FIELD_META = 14; - 对象映射:
OBJECT_FIELD_MV_TABLE_ID = 1(sint64,zigzag 编码)、OBJECT_FIELD_NAME_REF = 2(varint); - 元数据:
META_FIELD_KEY_REF = 1、META_FIELD_VALUE_REF = 2。
PortableLogicalBuilder通过intern_string完成帧内字符串驻留(同一字符串只编码一次,返回索引),add_object_map写入mv_table_id与name_ref,add_metadata写入键值引用对,finish按 string table → object map → meta 的顺序输出事务字段。
mv_table_id是同一帧内恢复操作所使用的 id,可能为负数,因为 MVCC 对未 checkpoint 的对象使用负 rootpage;name_ref指向帧字符串表。行级恢复操作保持紧凑的 MVCC table id,外部读者通过同帧 object map解析该 id。
测试 core/mvcc/database/tests.rs 中的test_mvcc_portable_changes_encoder_matches_metadata_wire_golden用黄金字节序列验证了编码正确性:例如mv_table_id = -5(zigzag 后为0x08 0x09)、name_ref = "items"的 object map 编码为0x6a 0x04 0x08 0x09 0x10 0x02,而meta中"client" -> "client-a"编码为0x72 0x04 0x08 0x00 0x10 0x01。
五、操作扩展(Operation Extensions):随操作携带的 protobuf 字节
恢复操作保留OP_FLAG_PORTABLE_EXTENSION标志,用于操作局部(operation-local)的 protobuf 字节:
op { tag flags // includes OP_FLAG_PORTABLE_EXTENSION table_id payload_len payload extension_len extension_bytes }重要约束(serializer.rs 中serialize_op_entry与log_write!宏共同落实):
- 扩展附着在某一条恢复操作上,而不是作为与帧并行的第二条可移植操作流;
- 恢复过程必须能够忽略操作局部扩展;
- 如果数据是崩溃恢复所必需的,它必须属于主恢复 payload,而不是扩展。
当前 Delete 扩展字段
DeleteExtension { identity_record: bytes // field 1, only for sqlite_schema deletes pk_record: bytes // field 2, SQLite record of primary-key values rowid: sint64 // field 3, present for rowid-table deletes }- 对普通用户表删除,core 在提交连接仍持有 schema 上下文时计算主键投影(primary-key projection),扩展只携带投影后的键记录与 rowid,而不是被删除的整行(
encode_delete_portable_extension实现,serializer.rs); - 对无显式主键的表,省略
pk_record,rowid 即删除身份; - 对
INTEGER PRIMARY KEY别名,投影键使用 rowid 值而非存储的表记录槽位,与 SQLite rowid 语义一致; sqlite_schema删除是例外:它把旧 schema 记录作为identity_record附加,因为object-map 构造需要被删除对象的名字与 rootpage。
文档还指出,同一操作扩展机制未来可以承载 DDL 语句文本(如 ALTER 类操作)或触发器来源(trigger provenance)等字段,而无需改变恢复操作的外形。
六、构造流程:提交时刻的七步管线
文档给出的提交时刻构造顺序如下:
- 序列化每条恢复操作,仅当操作需要局部身份元数据时附加操作局部可移植字节;
- 构建恢复 payload;
- 用恢复解析器解析该 payload;
- 仅解码
sqlite_schema行变更以发现 object-map 条目; - 在 core 仍持有 schema 上下文时,将表行操作解析到 object-map 条目;
- 把每个连接的全部 MVCC 日志元数据快照为通用键/值字段;
- 输出字符串表、对象映射与元数据,作为事务扩展 payload。
源码侧,core/mvcc/database/mod.rs的populate_portable_changes(约 L2712 起)完整实现上述管线:遍历事务内的行版本与 schema 变更,用portable_schema_row_from_record解码sqlite_schema记录(要求至少 5 列,取 type/name/rootpage),通过portable_table_id_from_rootpage将 rootpage 转为负 MVTableId,再用PortableLogicalBuilder生成扩展载荷。对 DDL 场景(如ALTER TABLE RENAME、DROP + CREATE同名重建),测试 core/mvcc/database/tests.rs 验证了名称映射会随 schema 演进更新,且旧/新表身份会同时出现在 object map 中。
失败语义:宁可提交失败,不可静默丢失映射
如果启用了可移植日志,而某条用户表变更无法解析到可移植 object map,提交将失败。静默省略用户表映射会让外部重放产生歧义——这是格式的权威性(authoritative)承诺在提交路径上的强制保障。
七、过滤规则:跳过实现所属对象
可移植扩展会跳过实现所属对象(implementation-owned objects):
sqlite_*__turso_internal_*turso_sync_*- 保留的 CDC 记账表(CDC bookkeeping tables)
这些对象要么是 SQLite 实现状态,要么是 Turso 本地状态或 sync/CDC 记账数据,应由接收端数据库重建,不属于可移植扩展暴露的用户 schema 契约。源码中 core/mvcc/portable_logical.rs 的is_portable_logical_name用三个前缀常量("sqlite_"、"__turso_internal_"、"turso_sync_")加两个 CDC 表名常量精确实现这一过滤,is_portable_schema_row则进一步限定table/index/trigger/view四种 schema 行类型(rootpage 非零且类型为 table 才算is_portable_table_schema_row)。
sqlite_sequence的特殊处理
sqlite_sequence值得消费者特别注意:它是 SQLite 内部表,但序列状态派生自对AUTOINCREMENT表的用户可见插入。重放用户表写入才是可移植的真相来源;直接拷贝生产者的sqlite_sequence行会导入本地分配器状态,对不同的接收端而言可能是错误的。
八、评审关注点(Review Concerns):内存、日志大小、加密与命名的取舍
文档最后总结的评审关注点,正是这套格式在设计权衡上的最终答案:
- 内存:没有第二条可移植行操作列表,也没有为每条被删除行准备旁路缓冲区;删除身份在操作序列化期间即时投影。
- 日志大小:upsert 行、schema 行与头部更新不被重复;数据表删除只携带主键投影与 rowid,而非完整行镜像。
- 加密:扩展记录与恢复字节一同加密和认证,无明文旁路数据。
- 表 id:恢复用到的每个用户表 id 都有显式的同帧 object-map 条目。
- 名称:名称每帧驻留一次(intern),object map 只引用索引。
- 元数据:所有连接元数据以通用键/值字段保留;
"client"在格式中没有任何特殊地位——测试黄金字节0x62 0x06 b"client" ...印证它只是普通 meta 字段,这是"不把同步引擎字段写死进核心格式"原则的直接体现。
九、如何查看与验证这套格式
当前仓库提供两种观察入口,适合深入学习的读者自行验证:
- 源码路径:core/mvcc/portable_logical.rs(编码器与名称过滤)、core/mvcc/persistent_storage/logical_log/serializer.rs(操作/扩展/加密序列化)、core/mvcc/persistent_storage/logical_log.rs(物理帧布局与恢复规则)、core/mvcc/database/mod.rs(提交期构造管线)。
- 测试路径:core/mvcc/database/tests.rs 中的
test_mvcc_portable_changes_*系列测试,覆盖黄金字节编码、默认关闭行为、用户 schema/行收录、RENAME 映射更新、同名 DROP/RECREATE 双身份等场景;测试需在conn_raw_apifeature 下运行。
阅读顺序建议:先通读本文配套的 docs/internals/mvcc/PORTABLE_FORMAT.md,再结合 docs/internals/mvcc/DESIGN.md 与 docs/internals/mvcc/RECOVERY_SEMANTICS.md 了解整体 MVCC 恢复语义,最后回到上述源码与测试做逐字节核对,即可完整掌握从帧布局到提交管线的可移植日志机制。
【免费下载链接】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),仅供参考