Turso Database 深度拆解:用 Rust 完全重写 SQLite,这次不是玩票
SQLite 是这个星球上部署量最大的数据库——超过一万亿个实例跑在手机、浏览器、IoT 设备和你家的路由器里。现在有一家公司说:我们要用 Rust 把它从头重写一遍,不是分叉,不是封装,是逐字节兼容的完全重写。这篇文章带你从架构层面拆解 Turso Database:为什么重写而不是分叉、异步 I/O 与 io_uring 如何落地、MVCC 并发写怎么绕开 SQLite 的单写者限制、确定性模拟测试(DST)如何给数据库上保险,以及生产环境里你真正需要注意的坑。
一、背景:SQLite 的统治,与 C 语言的枷锁
先摆事实:SQLite 大概率是人类历史上部署最广的软件之一。每一台 Android / iOS 设备、每一个 Chrome / Firefox 浏览器、每一架波音客机的某些子系统里,都有它的身影。它的成功建立在三个朴素的设计决策上:
- 进程内嵌入:没有独立服务进程,没有网络协议,数据库就是一个函数库;
- 单文件存储:整个数据库就是磁盘上的一个文件,备份等于
cp; - 零配置:没有 DBA,没有调优参数地狱,打开就能用。
但 SQLite 诞生于 2000 年,它的核心假设也停留在那个年代:
- 同步阻塞 I/O。SQLite 的 VFS(虚拟文件系统)层是完全同步的,一次
read()卡住,整个调用线程就卡住。在 2000 年这没问题,但在今天的 serverless、边缘计算场景里,阻塞 I/O 意味着你必须为每个请求配一个线程,资源模型直接崩坏。 - 单写者模型。SQLite 同一时刻只允许一个写事务。WAL 模式缓解了读写冲突(读不阻塞写、写不阻塞读),但写与写之间依然串行。对于多租户边缘数据库场景,这是硬伤。
- C 语言的内存安全问题。SQLite 的代码质量极高(100% 分支覆盖的 TH3 测试套件),但 C 就是 C——历史上依然出过缓冲区溢出类 CVE(比如 CVE-2022-35737)。测试可以逼近完美,但无法从类型系统层面消灭整类错误。
- 封闭的开发模式。这一点很多人不知道:SQLite 是开源(open source)但不开放贡献(not open contribution)的项目。官方明确不接受外部 PR,核心测试套件 TH3 是专有的、不公开的。你可以读它的代码,但你很难参与它的演进。
这四条里,最后一条是 Turso 这家公司故事的起点。
二、分叉还是重写:从 libSQL 到 Limbo 的路线之争
Turso(前身 ChiselStrike,由 Glauber Costa 和 Pekka Enberg 于 2021 年创立,两人都是 Linux 内核 / ScyllaDB 背景的系统工程师)最早选的路线是分叉:2022 年他们 fork 了 SQLite,创建了 libSQL,目标是做一个"开放贡献版的 SQLite"。
libSQL 相当成功——GitHub 上万星、近百位贡献者,加上了服务端模式、基于 WAL 的复制、原生向量搜索等特性,成为 Turso 云平台的核心引擎。
但分叉的痛点随着时间推移越来越明显:
1. 你继承了 C 代码库的一切负担。 内存安全问题不会因为你 fork 了就消失,每一个大胆的架构改动都要在没有 TH3 测试套件保护的情况下进行——官方测试套件是专有的,fork 拿不到。这意味着 libSQL 团队改核心代码时,心里其实是没底的。
2. 你无法做伤筋动骨的改造。 想把同步 VFS 换成异步?想把单写者模型换成 MVCC?这些改动会让你和上游 SQLite 彻底分道扬镳,再也无法合并上游更新——那分叉的最大好处(跟随上游演进)就没了。
于是 2024 年 12 月,Turso 宣布了一个更激进的实验:Limbo 项目——用 Rust 从零完全重写 SQLite。2025 年这个项目转正,更名为 Turso Database,成为公司的主线产品。
这里有个值得程序员深思的工程决策问题:什么时候该重写,什么时候该分叉?
Turso 团队当年评估过完全重写,结论是"达到生产级标准需要巨大投入",所以选了分叉。两年后他们又推翻了自己——变量是什么?我认为有三个:
- AI 辅助开发改变了重写的成本曲线。Turso 团队公开说过,Limbo 的大量兼容性测试、模糊测试用例是借助 LLM 生成的。重写一个有明确规格(SQLite 的行为就是规格)的系统,恰好是 AI 最擅长的场景。
- 确定性模拟测试(DST)技术成熟了。没有 TH3 没关系,用 DST 可以构建自己的、甚至更强的正确性保障体系(下文细讲)。
- libSQL 趟出了市场。分叉阶段验证了"开发者要一个更现代的 SQLite"这个需求真实存在,重写才有商业逻辑支撑。
这个"先分叉探路、再重写攻坚"的两段式策略,比一上来就喊"Rust 重写万物"要务实得多。
三、架构拆解:Turso Database 到底新在哪
Turso Database 的定位一句话说清:一个进程内(in-process)、兼容 SQLite 的 SQL 数据库,原生支持异步 I/O、并发写和向量搜索。
3.1 兼容性承诺:文件格式 + SQL 方言 + 字节码
Turso 的兼容目标分三层:
- SQL 方言兼容:你的 SQL 语句不用改;
- 文件格式兼容:直接打开现有的
.db/.sqlite文件,页结构、B-Tree 布局、varint 编码全部一致; - 字节码架构对齐:和 SQLite 一样,SQL 先被编译成字节码,再由虚拟机(VDBE 的 Rust 对应实现)解释执行。
EXPLAIN输出的指令序列风格几乎一致。
第三点尤其重要。很多"SQLite 兼容"项目只做到了协议层兼容,而 Turso 选择连内部执行模型都对齐,这让行为级兼容(比如类型亲和性、NULL 排序这些边角语义)更容易逼近原版。
3.2 异步 I/O:从 VFS 到 io_uring
这是整个重写中最核心的架构差异。SQLite 的执行流程是:
SQL → 字节码 → VM 逐条执行 → 需要页数据 → pager 同步 read() → 阻塞等待 → 继续执行
Turso 把这条链路改成了状态机驱动的异步模型:
SQL → 字节码 → VM 逐条执行 → 需要页数据 → 提交异步 I/O 请求 → 返回 I/O pending
→ 调用方决定:等待 / 干别的 → I/O 完成 → 从暂停点恢复执行
关键在于虚拟机本身被设计成可暂停、可恢复的:每条字节码指令执行时如果遇到未就绪的页,不会阻塞线程,而是把当前执行状态挂起,向调用方返回一个"I/O 进行中"的信号。这在 Rust 里天然映射到 Future 的 poll 模型。
在 Linux 上,I/O 后端是 io_uring:
- 读写请求批量提交到内核提交队列(SQ),完成事件从完成队列(CQ)批量收割,系统调用次数大幅下降;
- 配合
O_DIRECT可以绕过 page cache,数据库自己管理缓存(这是 ScyllaDB 流派的经典打法,Turso 两位创始人正是这个流派出身); - 单线程即可打满 NVMe 的队列深度,不需要线程池模拟异步。
对使用者来说,体感是这样的(Rust API):
use turso::Builder;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 打开(或创建)一个本地数据库文件——完全兼容 SQLite 格式
let db = Builder::new_local("app.db").build().await?;
let conn = db.connect()?;
conn.execute(
"CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
)",
(),
).await?;
conn.execute(
"INSERT INTO users (name, email) VALUES (?1, ?2)",
("三哥", "sange@example.com"),
).await?;
let mut rows = conn.query("SELECT id, name FROM users", ()).await?;
while let Some(row) = rows.next().await? {
// 性能提示:get 优于 get_value,可避免一次拷贝
let id: i64 = row.get(0)?;
let name: String = row.get(1)?;
println!("{id}: {name}");
}
Ok(())
}
注意所有数据库操作都是 async 的——这不是在同步 API 外面包了一层线程池假装异步(很多 SQLite 的 async 封装库就是这么干的,比如把操作扔进 spawn_blocking),而是 I/O 路径从上到下真正非阻塞。
对比一下典型的 rusqlite(SQLite 的 Rust 绑定)用法你就能看出范式差异:
// rusqlite:同步阻塞,async 环境下必须自己想办法
let conn = rusqlite::Connection::open("app.db")?;
let mut stmt = conn.prepare("SELECT id, name FROM users")?;
// 在 tokio 里跑这个?要么 spawn_blocking,要么祈祷查询够快
3.3 MVCC 与并发写:干掉单写者
SQLite 的写并发模型是数据库教科书里"简单但受限"的典型:全库一把写锁,WAL 模式下读写互不阻塞,但写与写永远串行。高并发写入场景下,你会频繁撞上 SQLITE_BUSY。
Turso Database 引入了 MVCC(多版本并发控制),支持 BEGIN CONCURRENT 风格的并发写事务:
- 每个写事务在自己的版本空间内工作,不再全局互斥;
- 提交时做冲突检测(乐观并发控制):如果两个事务改了同一行,后提交者失败重试;如果改的是不同行,则都能成功;
- 数据结构上参考了 Hekaton(微软 SQL Server 的内存引擎)的无锁设计思路。
写个模拟场景对比一下。假设 100 个协程并发插入:
use std::sync::Arc;
use turso::Builder;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let db = Arc::new(Builder::new_local("bench.db").build().await?);
let conn0 = db.connect()?;
conn0.execute(
"CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY,
payload TEXT
)", (),
).await?;
let mut handles = Vec::new();
for i in 0..100 {
let db = db.clone();
handles.push(tokio::spawn(async move {
let conn = db.connect()?;
// 每个任务写各自的行,MVCC 下这些写事务可以并发推进
conn.execute(
"INSERT INTO events (payload) VALUES (?1)",
(format!("event-{i}"),),
).await?;
anyhow::Ok(())
}));
}
for h in handles { h.await??; }
Ok(())
}
同样的逻辑用原版 SQLite 写,100 个写事务会在锁上排队;写热点一上来,SQLITE_BUSY 和 busy_timeout 就成了你日志里的常客。
当然要泼一盆冷水:MVCC 不是免费的。版本链需要内存、垃圾回收需要周期、冲突重试需要业务配合。对于单写者完全够用的场景(绝大多数移动 App),MVCC 是屠龙刀切豆腐。它真正的目标场景是 Turso 的老本行——多租户边缘数据库和高并发嵌入式服务端。
3.4 确定性模拟测试(DST):没有 TH3,怎么让人信你?
这是我个人认为整个项目最值得学习的工程实践。
数据库最难的不是写出来,是证明它是对的。SQLite 的护城河很大程度上就是那个专有的 TH3 测试套件——上亿个测试用例,100% 分支覆盖。重写者拿不到它,怎么办?
Turso 的答案是 Deterministic Simulation Testing(确定性模拟测试),思路源自 FoundationDB 和 TigerBeetle:
- 把一切不确定性抽象成可控接口:I/O、时钟、随机数、调度顺序,全部走注入的模拟实现;
- 用伪随机种子驱动整个模拟世界:给定同一个种子,执行序列完全可复现;
- 在模拟世界里注入故障:随机的 I/O 失败、写入中途断电、fsync 谎报成功、页损坏……让数据库在极端恶劣的环境里跑几十亿次操作;
- 一旦发现不变量被破坏(比如提交过的事务丢了),种子就是完整的复现凭证——这解决了分布式/并发 bug "无法复现"这个千古难题。
Turso 为此专门写了模拟器(内部叫 simulator,配合 Antithesis 平台做持续故障注入)。Rust 在这里帮了大忙:所有 I/O 都走 trait 抽象,模拟实现和 io_uring 实现可以无缝替换,编译器保证你没有偷偷绕过抽象层直接 std::fs::read。
// 概念示意:I/O 抽象层
trait IO {
fn read_page(&self, page_no: u32) -> impl Future<Output = Result<Page>>;
fn write_page(&self, page_no: u32, data: &Page) -> impl Future<Output = Result<()>>;
fn sync(&self) -> impl Future<Output = Result<()>>;
}
// 生产实现:io_uring / kqueue / WASM 后端
// 测试实现:SimulatedIO —— 由种子驱动,可注入延迟、失败、断电、数据损坏
这套方法论对普通业务系统同样适用:把不确定性推到边界,核心逻辑保持纯粹,用种子驱动的模拟测试替代碰运气的集成测试。 哪怕你不写数据库,这个思想也值回票价。
3.5 原生向量搜索:不装扩展的 RAG 存储
libSQL 时代 Turso 就把向量搜索做进了引擎(而不是像 sqlite-vss 那样以扩展形式存在),Turso Database 延续了这条路线:向量就是一种列类型,相似度查询就是普通 SQL:
CREATE TABLE docs (
id INTEGER PRIMARY KEY,
content TEXT,
embedding F32_BLOB(768) -- 768 维 float32 向量
);
-- 插入向量
INSERT INTO docs (content, embedding)
VALUES ('Rust 重写 SQLite', vector32('[0.12, 0.87, ...]'));
-- 余弦距离 Top-K 检索
SELECT id, content
FROM docs
ORDER BY vector_distance_cos(embedding, vector32('[0.1, 0.9, ...]'))
LIMIT 5;
对于端侧 RAG(本地知识库、离线 AI 助手)这个正在爆发的场景,"SQLite 兼容 + 原生向量 + 进程内嵌入"是一个非常舒服的组合:关系数据和向量数据在同一个事务域里,不需要再拖一个独立向量数据库。
3.6 多语言绑定与 WASM
得益于 Rust 的 FFI 友好性,Turso Database 的绑定矩阵铺得很快:
- Rust:一等公民,原生 async API(依赖 Tokio);
- JavaScript/TypeScript:提供与 better-sqlite3 兼容的 API,Node / Bun / Deno 通吃;
- Python:兼容
sqlite3标准库风格的 DB-API 接口; - WASM:可以直接跑在浏览器里——这是 C 版 SQLite 要靠 emscripten 折腾半天的事,Rust 生态里
wasm32就是一个普通编译目标; - Go / Java 等绑定也在推进中。
JavaScript 侧示例(风格几乎就是 better-sqlite3):
import { connect } from '@tursodatabase/database';
const db = await connect('local.db');
await db.exec(`CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT)`);
const stmt = db.prepare('INSERT INTO kv (k, v) VALUES (?, ?)');
await stmt.run(['theme', 'dark']);
const row = await db.prepare('SELECT v FROM kv WHERE k = ?').get(['theme']);
console.log(row.v); // dark
四、生产视角:性能、坑与选型
4.1 性能:别只看官方 benchmark
Turso 官方博客给过一些单点数据(比如某些读路径快于 SQLite),但作为老程序员,我建议你用这个框架来理解它的性能画像:
- 顺序读 / 点查:与 SQLite 相当。两者都是 B-Tree + 页缓存,热数据在缓存里时瓶颈根本不在 I/O 模型;
- 高并发混合读写:Turso 优势区。MVCC + 异步 I/O 让它在写竞争场景下吞吐显著更好,不会像 SQLite 那样在写锁上排长队;
- 单线程批量写:SQLite 依然极快(一个事务里
INSERT百万行是它的看家本领),Turso 的 MVCC 开销在这里是纯成本; - 冷启动 / 连接建立:注意!社区实测反馈 Turso 的数据库连接建立比 SQLite 打开文件要慢(异步运行时初始化、状态机构建都有成本)。如果你的使用模式是"频繁打开-关闭数据库",这个开销会被放大——正确姿势是长连接复用。
几个实测层面的调优建议(来自社区经验):
- 用
get而不是get_value取列值,前者可以避免一次值拷贝; - 减少查询次数比优化单条查询收益更大——异步不等于免费,每次 await 都有调度成本,能 JOIN 就别 N+1;
- Rust 项目统一用 Tokio,Turso 的 async API 深度绑定 Tokio 生态,混用其他 runtime 会徒增复杂度;
- 写冲突要有重试逻辑:MVCC 乐观并发下,冲突事务会失败,业务层要准备好指数退避重试。
4.2 现在能上生产吗?
分场景说实话:
- 移动端 / 桌面端主存储:SQLite 依然是默认答案。它有 25 年的实战检验,Turso 还年轻,文件是你用户的数据,稳妥优先;
- 新项目、Rust/TS 技术栈、需要 async:可以认真评估 Turso。SQLite 文件格式兼容意味着退路清晰——不爽了随时把
.db文件拿回去用 SQLite 打开; - 端侧 AI / RAG 应用:Turso 的原生向量搜索 + WASM 支持是差异化优势,值得尝鲜;
- 高并发写入的嵌入式服务端(比如每个租户一个库的 SaaS):这就是 Turso 设计出来要吃的场景,MVCC 并发写是硬需求;
- 强一致性关键业务:再等等。DST 测试体系很硬核,但"跑过几十亿次模拟"和"在千万台设备上跑了二十年"之间还是有信任差距的。
4.3 和 libSQL 什么关系?该选哪个?
很多人搞混这两个,一张表说清:
| 维度 | libSQL | Turso Database |
|---|---|---|
| 本质 | SQLite 的 C 语言分叉 | Rust 完全重写 |
| 成熟度 | 生产可用,Turso 云平台在用 | 快速演进中 |
| 异步 I/O | 无(继承 SQLite 同步 VFS) | 原生支持,io_uring |
| 并发写 | 单写者(同 SQLite) | MVCC 并发写 |
| 向量搜索 | 有 | 有 |
| 长期路线 | 维护模式 | 公司主线,未来会接管云平台引擎 |
简单说:今天求稳选 libSQL,押注未来选 Turso Database。Turso 官方的长期计划就是让 Rust 重写版逐步替代 libSQL 成为其云服务的底层引擎。
五、更大的图景:Rust 重写基础设施的第三阶段
把 Turso 放进 2024-2026 这波浪潮里看会更有意思:
- 第一阶段是工具链重写:ripgrep、fd、uv、Ruff、Oxc、Rolldown——CLI 和构建工具,风险低、收益立竿见影;
- 第二阶段是运行时重写:Deno、Bun(Zig 转 Rust)、各类 WASM 运行时——开始触碰核心执行路径;
- 第三阶段就是存储引擎重写:Turso 重写 SQLite、各家用 Rust 造对象存储和流处理引擎——这是最难啃的骨头,因为数据不能丢,正确性没有商量余地。
Turso 敢碰第三阶段,靠的不是"Rust 信仰",而是两个方法论层面的支撑:确定性模拟测试解决了"重写如何证明正确"的问题,AI 辅助开发压低了"重写百万级行为兼容测试"的成本。这两件事比"又一个 C 项目被 Rust 重写了"的新闻本身重要得多——它们意味着,重写关键基础设施的工程门槛,正在系统性地下降。
SQLite 的作者 D. Richard Hipp 曾说过,SQLite 计划支持到 2050 年。我完全相信它做得到。但 2050 年的新项目还会默认选一个同步阻塞、单写者的 C 库吗?Turso 赌的是"不会"。
这场赌局值得每个工程师搬个板凳看下去——顺便,cargo add turso 也就一行的事。
附:五分钟上手清单
# Rust
cargo add turso tokio --features tokio/full
# JavaScript
npm install @tursodatabase/database
# Python
pip install pyturso
# Python:兼容 sqlite3 标准库的用法
import turso
conn = turso.connect("local.db")
cur = conn.cursor()
cur.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
cur.execute("INSERT INTO notes (body) VALUES (?)", ("hello turso",))
conn.commit()
print(cur.execute("SELECT * FROM notes").fetchall())
老的 .db 文件直接拿来就能开——这就是"兼容 SQLite 文件格式"最实在的意义:**你随时可以来,也随时可以走。**好的开源项目,应该让用户拥有说走就走的自由。