编程 Turso Database 深度拆解:用 Rust 完全重写 SQLite,这次不是玩票

2026-07-27 03:16:01 +0800 CST views 8

Turso Database 深度拆解:用 Rust 完全重写 SQLite,这次不是玩票

SQLite 是这个星球上部署量最大的数据库——超过一万亿个实例跑在手机、浏览器、IoT 设备和你家的路由器里。现在有一家公司说:我们要用 Rust 把它从头重写一遍,不是分叉,不是封装,是逐字节兼容的完全重写。这篇文章带你从架构层面拆解 Turso Database:为什么重写而不是分叉、异步 I/O 与 io_uring 如何落地、MVCC 并发写怎么绕开 SQLite 的单写者限制、确定性模拟测试(DST)如何给数据库上保险,以及生产环境里你真正需要注意的坑。

一、背景:SQLite 的统治,与 C 语言的枷锁

先摆事实:SQLite 大概率是人类历史上部署最广的软件之一。每一台 Android / iOS 设备、每一个 Chrome / Firefox 浏览器、每一架波音客机的某些子系统里,都有它的身影。它的成功建立在三个朴素的设计决策上:

  1. 进程内嵌入:没有独立服务进程,没有网络协议,数据库就是一个函数库;
  2. 单文件存储:整个数据库就是磁盘上的一个文件,备份等于 cp
  3. 零配置:没有 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:

  1. 把一切不确定性抽象成可控接口:I/O、时钟、随机数、调度顺序,全部走注入的模拟实现;
  2. 用伪随机种子驱动整个模拟世界:给定同一个种子,执行序列完全可复现;
  3. 在模拟世界里注入故障:随机的 I/O 失败、写入中途断电、fsync 谎报成功、页损坏……让数据库在极端恶劣的环境里跑几十亿次操作;
  4. 一旦发现不变量被破坏(比如提交过的事务丢了),种子就是完整的复现凭证——这解决了分布式/并发 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 打开文件要慢(异步运行时初始化、状态机构建都有成本)。如果你的使用模式是"频繁打开-关闭数据库",这个开销会被放大——正确姿势是长连接复用

几个实测层面的调优建议(来自社区经验):

  1. get 而不是 get_value 取列值,前者可以避免一次值拷贝;
  2. 减少查询次数比优化单条查询收益更大——异步不等于免费,每次 await 都有调度成本,能 JOIN 就别 N+1;
  3. Rust 项目统一用 Tokio,Turso 的 async API 深度绑定 Tokio 生态,混用其他 runtime 会徒增复杂度;
  4. 写冲突要有重试逻辑:MVCC 乐观并发下,冲突事务会失败,业务层要准备好指数退避重试。

4.2 现在能上生产吗?

分场景说实话:

  • 移动端 / 桌面端主存储:SQLite 依然是默认答案。它有 25 年的实战检验,Turso 还年轻,文件是你用户的数据,稳妥优先;
  • 新项目、Rust/TS 技术栈、需要 async:可以认真评估 Turso。SQLite 文件格式兼容意味着退路清晰——不爽了随时把 .db 文件拿回去用 SQLite 打开;
  • 端侧 AI / RAG 应用:Turso 的原生向量搜索 + WASM 支持是差异化优势,值得尝鲜;
  • 高并发写入的嵌入式服务端(比如每个租户一个库的 SaaS):这就是 Turso 设计出来要吃的场景,MVCC 并发写是硬需求;
  • 强一致性关键业务:再等等。DST 测试体系很硬核,但"跑过几十亿次模拟"和"在千万台设备上跑了二十年"之间还是有信任差距的。

4.3 和 libSQL 什么关系?该选哪个?

很多人搞混这两个,一张表说清:

维度libSQLTurso 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 文件格式"最实在的意义:**你随时可以来,也随时可以走。**好的开源项目,应该让用户拥有说走就走的自由。

推荐文章

Python 微软邮箱 OAuth2 认证 Demo
2024-11-20 15:42:09 +0800 CST
程序员茄子在线接单