为什么用Rust重写SQLite?工程逻辑与挑战解析

发布时间:2026/9/2 21:46:55
为什么用Rust重写SQLite?工程逻辑与挑战解析 大家好最近在关注 P99 CONF 2025 的议题时被 Glauber Costa 的一场演讲深深吸引了主题是“Why We’re Rewriting SQLite in Rust”。P99 CONF 是面向性能工程、SRE 和底层系统开发者的技术会议能在这种场合谈“重写 SQLite”本身就带着一种硬核技术挑战的意味。Glauber Costa 是 Turso 的创始人兼 CEOTurso 是一家基于 libSQL 构建分布式 SQLite 服务的公司libSQL 则是 SQLite 的一个开源分支。这场演讲没有停留在“Rust 比 C 更安全”的层面而是深入讨论了嵌入式数据库在存储引擎、事务处理、SQL 执行链路和内存管理上的重重难题。本文将围绕这场演讲的核心内容展开结合 SQLite 本身的架构原理、Rust 语言在系统编程中的优势以及我从 C/C 转向 Rust 实际开发中的经验分析“为什么重写 SQLite 不是一时冲动而是一次深思熟虑的工程决策”。文章会覆盖 SQLite 的核心组件、Rust 重写的关键动机、需要解决的技术难点、兼容性策略以及这场技术变革对整个数据库生态可能带来的影响。同时也会给出一些工程上可落地的建议包括 Rust 开发环境的搭建、性能对比思路和常见误区。如果你是 Rust 爱好者、数据库内核开发者或者对“用更安全的语言重写关键基础设施”感兴趣这篇文章值得耐心读完。1. 演讲背景与核心主题1.1 P99 CONF 2025 带来了什么话题P99 CONF 是围绕“P99 延迟”展开的技术会议它关注的不是平均响应时间而是系统在极端压力下的长尾延迟。对数据库来说P99 延迟往往决定了用户体验的稳定性和系统的可预测性。Glauber Costa 在 P99 CONF 2025 上的演讲核心议题是解释 Turso 团队为什么决定用 Rust 重写 SQLite以及他们在重写过程中遇到的实际挑战。Turso 团队的思路很明确SQLite 是目前世界上部署最广泛的数据库引擎从手机、浏览器到嵌入式设备几乎无处不在。但 SQLite 的 C 代码库已经发展了二十多年如果要继续扩展它的能力比如支持分布式复制、更好地利用多核 CPU、提供更安全的内存管理那么在原有 C 代码上修修补补难度和风险都会越来越大。与其如此不如基于 Rust 重新实现一套兼容 SQLite 语义和文件格式的引擎。1.2 这一重写项目的技术目标从演讲中能看出Turso 团队并不是为了“重写而重写”他们的目标非常具体保持与 SQLite 的 SQL 语法和文件格式兼容让现有 SQLite 用户可以无缝迁移。利用 Rust 的所有权和借用检查机制在编译期消除大量内存安全漏洞。改进并发模型让数据库在读写混合场景下能更好地利用现代多核 CPU。提供一个更开放、更现代的开发基础让新功能可以更快地迭代。从这些目标看这次重写更像是一次“渐进式重写”不是立刻推翻所有 C 代码而是先用 Rust 逐步替换核心模块同时保持对外行为一致。1.3 为什么这个话题值得关注SQLite 在数据库领域地位特殊它是很多应用的基础组件。如果你在手机里跑过本地存储、在浏览器里用过 IndexedDB或者在小型 Web 服务里存放过数据背后很可能就是 SQLite。一个如此普及的数据库引擎被用 Rust 重写背后牵扯的问题很多Rust 是否足够成熟重写是否能保持性能文件格式兼容怎么做这些问题不仅关乎 Turso 一家公司也关乎所有依赖 SQLite 的开发者。所以这篇文章不是单纯赞美 Rust而是想和你一起拆解这场重写背后的工程逻辑。2. SQLite 架构与 Rust 重写的关键动机2.1 SQLite 核心架构概览在分析重写动机之前有必要先了解 SQLite 的内部结构。SQLite 采用分层架构每一层职责清晰接口层Interface提供sqlite3_open、sqlite3_prepare、sqlite3_step等 C API连接应用和数据库内核。SQL 编译器SQL Compiler包括词法分析器Tokenizer、语法分析器Parser、代码生成器Code Generator最终将 SQL 文本转化为虚拟机指令。虚拟数据库引擎VDBESQLite 的核心执行引擎它执行字节码指令完成数据检索、更新、事务控制等操作。B-Tree 层负责索引和表数据的组织是 SQLite 存储引擎的关键部分。Pager 层负责管理页面缓存、文件读写、事务回滚日志是 SQLite 事务与持久化的基石。OS 抽象层OS Abstraction Layer封装不同操作系统的文件 I/O、锁、共享内存等底层能力。--------------------- | Interface | --------------------- | SQL Compiler | | - Tokenizer | | - Parser | | - Code Generator | --------------------- | VDBE | --------------------- | B-Tree | --------------------- | Pager | --------------------- | OS Abstraction | ---------------------这套架构非常紧凑所有模块都为了一个目标服务在嵌入式环境下用最少资源处理 SQL。2.2 C 代码库的长期维护成本SQLite 的 C 代码质量在业界公认很高测试覆盖率也惊人但这并不意味着维护成本低。C 语言缺少现代语言提供的安全保障内存错误、空指针、数据竞争等问题只能靠开发者的纪律和工具链来排查。在 Glauber Costa 的演讲逻辑中C 代码库的扩展性限制是重写的核心动机之一。SQLite 最初设计为单写入者模型数据库层面的锁定粒度是一个数据库文件。这种设计在嵌入式场景下非常合适但在云原生环境、多租户平台或数据密集型应用里就很容易成为瓶颈。2.3 为什么最终选择 RustRust 在系统编程领域的优势已经得到广泛验证内存安全所有权Ownership、借用Borrowing、生命周期Lifetime机制在编译期阻止了绝大多数内存安全错误比如使用已释放内存、重复释放、缓冲区溢出。零成本抽象Rust 的抽象不会带来运行时额外开销这对数据库引擎至关重要。并发安全Rust 编译器能发现数据竞争这是 C 语言很难提前发现的错误。现代化工具链Cargo 包管理、内置测试框架、文档生成、交叉编译支持都让团队协作和项目维护更顺畅。Turso 选择 Rust不是因为它“酷”而是因为它在不牺牲性能的前提下让团队能更安全地开发复杂系统。3. 重写 SQLite 的技术难点与核心挑战3.1 SQL 解析与语法兼容重写 SQLite 最先面对的就是 SQL 解析。SQLite 支持的 SQL 语法非常丰富窗口函数、CTECommon Table Expressions、UPSERT、生成列、部分索引等。要完整复现这些语法需要编写一个新的词法分析器和语法分析器并且确保 Rust 版本的解析结果与 C 版本一致。一个更隐蔽的问题是 SQLite 对 SQL 语句的处理顺序。例如PRAGMA指令、sqlite_master系统表、类型亲和性Type Affinity规则、空值排序规则这些细节决定了一个 SQL 语句在执行时是否合法、结果是否正确。Turso 团队的做法是大量采用“模糊测试 差异化测试”随机生成 SQL 语句同时在 C 版 SQLite 和 Rust 版引擎上执行对比输出结果和错误码从而找出差异并修复。3.2 VDBE 虚拟机与字节码执行SQLite 的 SQL 并不会直接操作 B-Tree而是由代码生成器编译成一套字节码再由 VDBE 执行。这套设计让 SQLite 的 SQL 层和执行层解耦也为调试和事务控制提供了便利。在 Rust 中重写 VDBE需要注意字节码的数据结构设计和循环执行效率。Rust 的match表达式很适合实现字节码分发但也要注意指令解释执行时的缓存友好性。如果用简单粗暴的match为每条指令做分支判断可能导致性能下降。// 伪代码示例VDBE 指令分发的简化思路 enum OpCode { Add, Integer, OpenRead, Column, ResultRow, Halt, } struct Vdbe { pc: usize, // 程序计数器 registers: VecValue, // 寄存器组 } impl Vdbe { fn execute(mut self, program: [Instruction]) - Result(), DbError { while self.pc program.len() { let ins program[self.pc]; match ins.op { OpCode::Add { let rhs self.registers[ins.operand1 as usize].clone(); let lhs self.registers[ins.operand2 as usize].clone(); self.registers[ins.operand3 as usize] Value::Integer(lhs.as_i64() rhs.as_i64()); } OpCode::Halt return Ok(()), _ { /* 其他指令 */ } } self.pc 1; } Ok(()) } }这里要强调的是真实的 VDBE 实现远比这个复杂。操作数类型、类型转换、错误处理、子查询、游标状态管理都需要仔细设计。重写过程中最怕的就是只看到接口表面而忽略了内部状态机的隐式语义。3.3 事务、回滚日志与 WAL 模式SQLite 的事务机制是其可靠性的基石。传统回滚日志Rollback Journal模式下写入数据前会先记录原始页面到日志文件WALWrite-Ahead Logging模式下写入操作会追加到 WAL 文件然后异步检查点回主数据库文件。在 Rust 中实现事务机制时需要面对这些挑战文件锁的细节不同操作系统上锁的语义不同flock、fcntl和 Windows 上锁机制各有差异。WAL 索引共享内存SQLite 在 WAL 模式下使用共享内存来实现读并发多个连接并发访问时内存映射的生命周期和同步是难点。崩溃恢复模拟掉电、进程崩溃时必须保证数据库文件不会损坏要么回滚事务要么重放 WAL 日志。Rust 代码无法天然避开这些逻辑复杂度它只是让内存操作更安全持久化正确性仍然需要认真设计。3.4 Rust 的所有权模型在存储引擎中的适配Rust 的所有权模型要求“同一时刻只有一个可变引用或多个只读引用”这在传统数据库内核中是很强的约束。数据库缓存常常需要一个全局页缓存Page Cache缓存管理器需要不断读取、修改、驱逐页面。如果用RefCell或Mutex包裹全局缓存的引用可能引入运行时开销如果用无锁数据结构又需要仔细设计内存模型。一种常见的设计是采用竞技场分配器Arena Allocator 页面 ID 索引而不是把页面对象到处传递引用。这样代码中更多是操作页面 ID 和偏移量而不是直接持有mut Page能有效避开借用检查器带来的阻碍。pub struct Page { pub id: u32, pub data: Vecu8, } pub struct Pager { pages: VecPage, free_list: Vecu32, } impl Pager { pub fn get_page_mut(mut self, id: u32) - Optionmut Page { self.pages.iter_mut().find(|p| p.id id) } }这个简化示例展示了通过可变借用获取页面的方式。实际操作中还需要处理页面引用计数防止页面被淘汰或变更时还有外部引用存在这就需要引入带代际校验的句柄或类似机制。4. 重写策略如何让 Rust 与 SQLite 兼容共存4.1 分层替换而不是一次性重写Turso 团队并没有把所有模块在第一天就换成 Rust。他们的策略更接近“循序渐进”先实现内核的基础结构和 SQL 执行链路再逐步替换 B-Tree、Pager、OS 抽象层。这样做的好处是每个阶段都能有一个可运行的数据库内核用于验证而不是长年处于“半成品”状态。如果一个项目也考虑用 Rust 重写 C 语言组件比较好的做法是先定义清晰的边界接口用 FFIForeign Function Interface或 IPC 桥接新旧模块从而降低整体风险。4.2 文件格式兼容是底线SQLite 数据库文件是以固定格式存储的包含数据库头、页面、B-Tree 单元等结构。Rust 版必须能够读取和写入原生格式的 SQLite 文件否则现有应用无法通过简单替换文件实现迁移。这需要对 SQLite 的文件格式有精确掌握。例如数据库头部的 100 字节结构包括文件格式版本、页面大小、编码格式、WAL 或 journal 模式信息等。重写时建议直接参考官方文档中的格式说明并编写“金丝雀测试”用 C 版 SQLite 创建数据库Rust 版读取并修改再用 C 版验证。4.3 利用 SQLite 的测试套件进行验证SQLite 官方提供了庞大的测试集虽然这些测试很多是面向 C API 的但其中关于 SQL 行为、CLI 命令、文件格式、损坏恢复的用例仍可作为 Rust 重写的参考依据。Turso 团队在演讲中也提到会利用 SQLite 的 TCL 测试框架做批量回归测试将 Rust 版引擎作为测试接口暴露给测试脚本。这种“复用官方测试资产”的思路非常值得借鉴它能有效减少遗漏。5. Rust 重写 SQLite 的工程实践5.1 推荐环境配置如果你也想动手参与类似的项目或是在自己的环境中试验 Rust 版 SQLite这里给出一个基础环境配置建议操作系统macOS、Linux 或 Windows推荐 WSL2。Rust 工具链建议使用rustup安装开启 stable 工具链。构建工具CargoRust 自带。IDEVS Code rust-analyzer或 CLion Rust 插件。性能分析工具perf、flamegraph、cargo bench。# 安装 rustup curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh # 检查安装结果 rustc --version cargo --version安装完成后可以创建一个新的二进制项目cargo new my_sqlite_playground cd my_sqlite_playground5.2 构建一个最小的 SQL 执行管线为了理解 SQLite 重写的复杂度我们可以自己动手实现一个极简的 SQL 执行管线。这个示例会包含几个部分SQL 词法拆分、简单命令解析、内存表存储。它不追求完整兼容只是为了帮助你建立“从 SQL 到执行”的全链路感知。// src/main.rs use std::collections::HashMap; #[derive(Debug, Clone)] struct Row { id: u64, name: String, } struct MiniDb { next_id: u64, tables: HashMapString, VecRow, } impl MiniDb { fn new() - Self { Self { next_id: 0, tables: HashMap::new(), } } fn create_table(mut self, table_name: str) { self.tables.entry(table_name.to_string()).or_insert_with(Vec::new); } fn insert(mut self, table_name: str, name: str) - Result(), String { let table self.tables.get_mut(table_name).ok_or(table not found.to_string())?; table.push(Row { id: self.next_id, name: name.to_string(), }); self.next_id 1; Ok(()) } fn select(self, table_name: str) - ResultVecRow, String { let table self.tables.get(table_name).ok_or(table not found.to_string())?; Ok(table.clone()) } } fn execute(sql: str, db: mut MiniDb) - ResultVecRow, String { let sql sql.trim(); let tokens: Vecstr sql.split_whitespace().collect(); if tokens.is_empty() { return Err(empty sql.to_string()); } match tokens[0].to_uppercase().as_str() { CREATE { db.create_table(tokens[2]); Ok(vec![]) } INSERT { // 简化解析INSERT INTO users VALUES (name) db.insert(tokens[2], tokens[5])?; Ok(vec![]) } SELECT { // 简化解析SELECT * FROM users db.select(tokens[3]) } _ Err(format!(unsupported sql: {}, sql)), } } fn main() { let mut db MiniDb::new(); execute(CREATE TABLE users, mut db).unwrap(); execute(INSERT INTO users VALUES (Alice), mut db).unwrap(); execute(INSERT INTO users VALUES (Bob), mut db).unwrap(); let rows execute(SELECT * FROM users, mut db).unwrap(); for row in rows { println!(id{}, name{}, row.id, row.name); } }运行结果id0, nameAlice id1, nameBob这个“玩具数据库”虽然简陋但覆盖了 SQL 文本解析、命令分发、数据存储和查询返回的基本流程。真实 SQLite 的实现广度比这大无数倍但整体分层思路是相似的。5.3 Pager 层与 B-Tree 层实现要点Pager 层负责将逻辑页面映射到物理文件的字节偏移。每个 B-Tree 节点通常是一个页面页面大小从 512 字节到 65536 字节不等。在 Rust 中实现时需要关注页面读取的边界检查Rust 的切片和数组访问自带边界检查这比 C 更安全但也要避免不必要的复制。数据序列化与反序列化页面中的单元格Cell由指针和内容组成需要精确控制读写顺序。空闲页面管理删除数据后页面要加入空闲列表以便后续插入复用。这个管理机制容易出错建议用单元测试覆盖。5.4 WAL 模式中的并发控制在 WAL 模式下SQLite 允许多个读事务与一个写事务同时进行但写者之间互斥。实现并发控制时可以利用 Rust 的Mutex、RwLock或crossbeam库但需要注意锁粒度不能过大否则并发性能会下降。更进阶的方案是使用无锁数据结构如DashMap来管理部分索引但无锁结构在复杂事务回滚时很难处理建议初始版本以简单锁为主优先保证正确性。6. 兼容性验证与性能评估6.1 兼容性验证方案为了确保 Rust 版 SQLite 能作为 C 版替代品验证工作至少要覆盖以下维度SQL 语义一致性。文件格式兼容性。错误码和扩展机制。崩溃恢复安全性。扩展 API 的 ABI 兼容性。一个可落地的验证流程是利用 SQLite 官方 CLI 创建一个包含各类表、索引、视图的测试库。用 Rust 版引擎打开并执行查询。对比返回结果集。用 Rust 版引擎写入数据回到 C 版 CLI 查询确认数据一致。6.2 性能评估需要关注哪些指标重写后性能不一定立刻优于 C 版。在评估时建议至少关注这些指标事务吞吐量每秒能提交多少写事务。只读查询延迟尤其是 P99 延迟和尾延迟。内存占用相同数据集下Rust 版与 C 版的内存对比。并发扩展性在多个连接并发操作时吞吐量和锁竞争的变化。数据库文件体积写入后文件大小是否有显著变化。Rust 在优化时可以通过#[inline]、减少堆分配、使用unsafe代码块在确认安全的前提下来提升性能但最初阶段不建议过早优化应先用cargo bench和火焰图找到热点。6.3 一个简单的基准测试思路// benches/bench_sqlite.rs use std::time::Instant; fn main() { let n 100_000; let start Instant::now(); // 这里替换为实际插入逻辑 let elapsed start.elapsed(); println!(insert {} rows: {:?}, n, elapsed); }在正式项目中建议使用 Criterion 库做基准测试它能提供更丰富的统计信息和趋势分析。不过示例中的简易计时方式也能帮助快速感知性能变化。7. 常见问题与理解误区7.1 “Rust 重写 SQLite 等于从零开始”这是最常见的误解。Turso 团队并不是从零开始造全新数据库而是在 SQLite 已有语义、文件格式和测试体系的基础上用 Rust 重新实现内部逻辑。SQLite 二十多年的工程积累——B-Tree 的节点分裂策略、锁的升级降级规则、SQL 优化器的启发式规则——这些都不会消失只是被移植到了新的语言环境。7.2 “Rust 一定比 C 慢”这个结论不成立。Rust 默认会进行很多安全检查确实会带来少量运行时开销但这些开销通常可以通过编译器优化和合理的数据结构设计来降低。在真实的数据库场景中I/O 成本和锁竞争往往比 CPU 指令级别的差异更影响性能。Rust 的零成本抽象意味着只要你用对了语言特性性能可以接近甚至等于手写 C。7.3 “重写完成后SQLite 项目就会消失”不会。SQLite 作为公共领域数据库拥有庞大的使用基础。Turso 的 libSQL 分支更像是扩展 SQLite 生态的一种尝试而不是取代既有项目。很多衍生产品会和 SQLite 长期共存互为补充。7.4 “文件格式兼容意味着直接复制格式规范就可以了”文件格式兼容需要严格的测试和边界情况处理。SQLite 的文件格式包含大量隐含约束比如页面校验、溢出页链、增量 blob I/O这些很难只看文档就完全掌握。最稳妥的方式仍然是进行大量差分化测试。8. 最佳实践与工程启示8.1 从 C 到 Rust迁移时需要准备什么如果你打算维护一个 C 项目并计划逐步迁移到 Rust以下建议值得参考先搭建测试基础设施任何重写都依赖扎实的回归测试。定义清晰的模块接口用契约测试保证新旧模块行为一致。从小型、无状态、边界清晰的模块开始迁移积累经验。不要迷信“一行行翻译”用 Rust 的惯用设计重新表达逻辑。保留 C 版本一段时间方便做对照验证。8.2 Rust 数据库开发中的安全边界数据库引擎对安全要求极高任何失误都可能导致数据丢失或损坏。以下边界值得特别关注文件系统操作写入前要保证数据落盘使用fsync控制持久化时机。内存操作在进入unsafe前必须确保安全不变量被文档化并经过测试覆盖。并发访问锁的获取顺序必须一致避免死锁。错误处理错误必须从底层向上层完整传递不能吞掉错误导致状态不一致。8.3 从演讲中提炼的工程方法论Glauber Costa 的演讲本质上是在讲一个大型系统的重构故事。他的方法论可以总结为明确重写目标不只追求“新语言”本身。用兼容性作为保护层保证用户在迁移过程中不会受损。复用已有测试资产避免为了重写而重造测试轮子。将大项目拆分分期交付每个阶段都有可验证的结果。这套方法论不只适用于 SQLite也能指导你面对遗留系统重构时的决策。8.4 值得关注的 Rust 生态工具在做 Rust 数据库开发时下面这些工具和库非常实用cargo-expand展开宏帮助理解生成代码。valgrind与rr调试内存问题或回放执行轨迹。criterion性能基准测试。proptest基于属性的测试生成大量随机输入验证逻辑。tokio如果要做异步 I/O但要注意 SQLite 本身是同步库异步封装需要谨慎处理线程池。9. 总结与后续学习方向这场演讲让我印象最深的并不是“Rust 很安全”这种老生常谈而是团队在重写一个浸润二十多年工程智慧的数据库时选择了“兼容优先、逐步替换、性能并重”的路线。SQLite 的 C 代码并不是糟糕的技术债它本身质量极高但语言模型的不同决定了它后续能扩展的上限。Rust 的所有权模型、模式匹配和现代工具链为 SQLite 的下一代演进提供了一个更加坚实的地基。如果你对 Rust 本身还比较陌生建议从“Rust 所有权和借用”开始学然后再去读 SQLite 的官方文档和 libSQL 的源码。如果你对 SQLite 内部机制感兴趣那 B-Tree 和 Pager 就是最值得啃的部分。如果想把性能工程做好一定要用真实的负载去压测、去分析火焰图、去观察长尾延迟。无论你是数据库开发者、后端工程师还是正在学习 Rust 的初学者这场关于 SQLite 重写的技术讨论都足够有启发性。未来我们很可能会看到更多“用 Rust 重写关键基础设施”的项目从这个视角看这场演讲不仅仅是分享一个项目的进展更是在描绘系统软件发展的一种新趋势。如果你想上手实践最直接的建议就是安装 Rust、下载 libSQL 源码或尝试基于 Rust 的 SQLite 分支把示例代码跑起来然后尝试修改一个内部函数观察行为变化。只有亲手接触过代码才能真正理解这场重写背后的复杂度与价值。如果这篇文章帮你理清了一些概念欢迎收藏备用。接下来可以继续关注 Flock它是 Turso 团队用 Rust 实现并发原语的开源项目也是理解 SQLite 锁与事务的关键补充。实践出真知动手试试吧。

相关新闻