分布式 SQLite:十二年间的五个误区实测
十二年分布式 SQLite 实践(Raft、Litestream、Turso、D1),实测五个关于一致性、复制以及何时该用 Postgres 的常见误区。

分布式 SQLite 从一个人的周末项目,发展成了一个获得风险投资的产品品类,而大多数工程师甚至没注意到中间这十年。2014 年 9 月,Philip O'Toole 在 SQLite 前面加了一层 Raft 日志,把每次写入都复制到整个集群,并把代码以 rqlite 的名字推到了 GitHub 上。十二年后,这里已经形成了一整套生态——dqlite、Litestream、LiteFS、cr-sqlite、libSQL/Turso、Cloudflare D1——而人们对它的大多数认知,都以各种有意思的方式错了。
我在生产环境中跑过集群数据库,也收拾过它们的各种故障,所以先帮你省点麻烦:关于分布式 SQLite 的争论,几乎从来不是在讨论 SQLite 本身,而是在讨论你实际要解决的是哪个问题——机器挂了怎么办?机器挂了而我又等不起怎么办?或者,根本就没有“机器”怎么办?下面就用这十二年的历史,来检验五个误区。
误区一:分布式 SQLite 是个新思路
关于复制 SQLite 的这波热潮大约从 2021 年开始,那时 Litestream 和“SQLite 就够了”的运动声势浩大,Turso 和 Cloudflare D1 也带着融资和发布文章登场。于是大家形成了一种印象:好像是有人最近才聪明地发现 SQLite 可以复制。实际上,第一个基于 Raft 的 SQLite 系统早在 2014 年就发布了,而且它甚至不是第一个 SQLite 复制方案——Expensify 早在 2016 年发布 Bedrock 之前,就已经在内部用类 Paxos 的协议做这件事多年;ActorDB 则在差不多同一时期,给每个 Erlang actor 配了一个 Raft 复制的 SQLite 数据库。
除了署名问题,这为什么重要?因为一个设计如果能经受十二年的独立重复发明,它本身就说明了点什么。至少有八个团队,需求和协议各不相同,却都得出了同一种架构:SQLite 作为存储引擎,复制能力则在外部搭建。没人找到理由停下来。当你看到这种十年间不断收敛的重新实现模式,你面对的就是一个真正好的想法,而不是炒作周期。拿那些昙花一现的概念来对比,这个方向一直在持续积累。
早期的设计决策至今仍然是承重墙。rqlite 在 2014 年就选择了 CP 系统的定位——写入经过 leader,必须得到多数派确认,被分区的少数派拒绝写入——而且从未改变过主意。这一个选择决定了它的 HTTP API、读一致性级别,以及快照行为。那些把一致性当成日后再决定的配置开关的团队,通常都会后悔;你心里设想的规格与实际实现之间的差距,正是分布式 bug 滋生的温床。
误区二:在 SQLite 上跑 Raft 就能得到廉价版的分布式 Postgres
这是清单上代价最高的误区。这种说法听起来很诱人:拿全世界部署最广的数据库,加上共识协议,又能省掉客户端-服务端系统那套沉重的运维负担。但你实际得到的是一个单写入者数据库:每次写入都要付出一次与多数节点之间的网络往返开销,而且 SQLite 的 SQL 能力——没有存储过程、类型系统简单、并发原语有限——依然还是 SQLite 的那套 SQL。
写入路径才是最诚实的成本账。本地 SQLite 提交只是几次 fsync。而 Raft 复制的提交过程是:leader 把日志追加到本地,发送给 follower,等待多数派持久化,应用到 SQLite,然后才返回。在健康的局域网里,这大约是一到两毫秒,而不是几十微秒。跨地域的话,那就是光速加上磁盘的时间了。对于读多写少、写入速率不高的负载——比如配置存储、服务发现元数据、小型控制平面——这完全够用,而这也正是 rqlite 的目标场景(它的起源是一个需要少量关系存储的集群系统,而且没有好的办法决定该由哪个节点来持有它)。但对于高频 OLTP 负载,这就是选错了工具。
还有那些 README 里不会写的运维现实:leader 选举是你必须自己负责的一种故障模式。多数派投票确实防止了脑裂,但这也意味着一个三节点集群只要丢了两个节点,就会拒绝所有写入。你把“服务器挂了所以数据库不可用”换成了“法定人数没了所以数据库不可用”,而后者更难跟凌晨三点被叫醒的值班同事解释清楚。如果你还没考虑过对所选系统做 Jepsen 式的测试,就该考虑一下——Hacker News 上有位评论者直接问 rqlite 是否经过了 Jepsen 测试,因为没经过测试的系统通常都藏着意外。与此同时,同一讨论串里另一位经验丰富的工程师指出,借助现代工具,部署真正的 Postgres 已经变得足够简单,“直接用数据库服务器”重新成了一个合理的默认选择。他说得没错。

误区三:复制就等于共识
十二年的历史真正能给出的唯一一个洞见是:“复制型 SQLite”并不是一种设计,而是设计空间里的三个角,而且它们出现的顺序很有说明性。把它们混为一谈,就会导致团队为了解决一个备份问题,结果去跑一个 Raft 集群。
- 共识(CP)。rqlite、dqlite。一个 leader,强一致性,一个真正的集群。它回答的是:“机器挂了,而我等不起恢复怎么办?”代价:写入延迟和法定人数相关的操作。
- 流式复制(单写入者)。Litestream,后来的 LiteFS。完全不用共识——SQLite 的 WAL 被流式传输到对象存储,为单节点提供近乎连续的灾难恢复。它回答的是:“机器挂了怎么办?”代价:可能丢失的数据窗口虽小但存在,恢复时间以秒到分钟计。
- 合并(AP)。cr-sqlite 以及本地优先(local-first)运动。以 SQLite 扩展的形式提供 CRDT,多个写入者各自分叉写入,再无需 leader 地合并。它回答的是:“如果根本没有中心机器怎么办?”代价:最终一致性,以及你必须自己设计的合并语义。
Ben Johnson 为 Litestream 提出的论点值得尊重,因为它对大多数应用来说是对的:这些应用跑在一台机器上,它们需要的是机器故障后能恢复的方法,而不是一个集群。第一个问题——“机器挂了怎么办”——远比第二个问题——“而我等不起恢复”——常见得多。我见过团队为了保护一个 RTO 要求是“午饭前恢复就行”的数据库,却去部署三节点的 Raft 集群。这不是可靠性工程,而是盲目模仿。有意思的是,Litestream 的 WAL 流式方案后来反过来启发了 rqlite 自己在 v8 版本中处理 WAL 的方式——流式复制这一支反哺了共识这一支,形成了闭环。
所以在选工具之前,把问题写下来。真的写下来。“当节点 X 挂掉时,我们需要在 N 秒内恢复写入,并且我们接受丢失最后 M 秒已提交的数据。”如果 M 是几秒,N 是一分钟,那么 Litestream 加 S3 就是你的答案,它永远不会因为法定人数而半夜叫醒你。如果 M 是零、N 是秒级,那你就进入了共识的领域,并且每次写入都要为此付出代价。这和任何威胁建模的原则一样:在购买防御之前先明确故障是什么,就像应对抵御灾难的系统一样,先决定你要抵御的是哪一种灾难。
误区四:托管的“边缘 SQLite”消除了所有权衡
这个品类里的钱大多流向了平台——Turso 拿到了风险投资,后来被 Supabase 收购;Cloudflare 则把 D1 放进了自家的边缘网络。宣传的说法是,现在有人替你处理了那些困难的分布式部分。有时确实如此。但有时这只是营销,而不光彩的内幕会慢慢露出来:一位 Cloudflare 开发者公开承认,D1 本质上就是把一个单例 Durable Object 暴露出的 SQLite 数据库包了一层,而且你直接使用 Durable Objects 的存储往往会更好。这并不是共识意义上的分布式数据库,它只是一个外面包着平台的单个 SQLite 文件。在生产环境中使用 D1 的用户反馈的可靠性也与此相称。
这并不意味着托管方案不好,只是说权衡并没有消失,而是被藏到了你无法检视的 API 后面。当你自己运行 rqlite 时,你清楚地知道 CP 意味着什么:三个节点、一个 leader、多数派确认、被分区的少数派拒绝写入。而当你购买“边缘 SQLite”时,你需要对一个黑盒提出同样的问题:写入者在哪里?分区时会发生什么?我的实际持久化窗口是多长?如果供应商的回答含糊其辞,那么你的事故复盘也不会清楚。
复制即服务并不能消除一致性问题,它只是把答案交给了别人的基础设施,而你无法审计。
历史规律值得内化:开源系统(rqlite、dqlite、Litestream)在有人卖之前,已经把这个想法打磨了八年,而资本则流向了那些把想法变成按账单计费服务的人。这通常是基础设施的命运。购买往往是正确的选择,但购买时要清楚自己实际得到的是三个角中的哪一个。

误区五:强一致性是备份之上的免费升级
人们常把选择框定为备份(弱、便宜)与共识(强、昂贵但严格更优)。这并不对。通过 Raft 实现的强一致性是一个不同的产品,而不是同一产品的更好版本,它会带来持续的成本,而备份从来不会有这些成本。
成本之一:吞吐量。一位同时用过两者的工程师指出,rqlite 能提供“存放在不止一个地方”的保证,但比单节点数据库慢得多——这就是共识的往返开销,而且它永远不会消失。成本之二:微妙的语义问题。如果通过传输 SQL 语句来复制,你就会继承确定性问题:像 random() 或 datetime('now') 这样的函数,在各节点上可能产生不同结果,除非复制层重写它们,这也是为什么一些实现只支持 SQLite SQL 的一个子集。如果改在 WAL 层复制,就能避开这个问题,但你会与 SQLite 的磁盘格式和页面布局耦合。无论把复制层放在哪里,都要做出妥协。成本之三:分布式事务确实很难。有位工程师描述了他们的设计演进:最初事务中的每条语句都是一个独立的 Raft 命令——基本正确但很慢;后来改为由发起事务的节点在本地执行整个事务,回滚,然后只提交一条包含整个批次的单一提交命令。这种经验教训只有在真正运维这些系统之后才能学到。
下面是一个最简示例,用来说明两种写入路径的差异,使用的是 rqlite 的 HTTP API:
# Local SQLite: one process, one fsync, microseconds.
sqlite3 app.db "INSERT INTO kv VALUES ('flag', 'on')"
# rqlite: this HTTP call hides a Raft round trip —
# leader log append, replication to a majority, then apply.
curl -XPOST 'localhost:4001/db/execute?pretty' \
-H 'Content-Type: application/json' \
-d '["INSERT INTO kv VALUES (?, ?)", "flag", "on"]'
# The guarantee changed: the write now survives the loss
# of any single node. So did the latency budget.
还要注意你没有得到什么:读扩展能力。Raft 复制日志是为了让 follower 能够接管;它并不会默认让 follower 提供强一致性的读取(rqlite 提供读一致性级别,正是因为这是一种权衡)。如果你是冲着“分布式”来期待更多的读取能力,那你需要的是只读副本,而不是共识。
十二年的 Raft on SQLite 实践究竟教会了我们什么
这个品类的历史为基础设施理念如何走向成熟提供了异常清晰的证据。首先,有人为一个具体需求构建了原则性很强的方案(rqlite,来自一家真实公司中的真实集群问题)。然后,其他人为了适应自己的打包和部署约束重新实现它——Canonical 的 dqlite 是一个你可以嵌入的库,因为 Canonical 需要在其产品内部使用数据库,而 rqlite 则是一个可以与任何产品并排运行的服务器。接着,有人指出常见场景比原则性方案所假设的要小(Litestream),并被证明是对的,于是整个领域重新校准。最后,平台将其包装起来并出售。如果你在基础设施领域待得够久,就会再次看到这部电影——LLM 智能体正在以同样的方式重新学习分布式系统的教训。
我的直白建议是:综合以上所有内容,先从单节点 SQLite 加上 WAL 流式备份到对象存储开始。它能走得多远,会让你惊讶,而且你的故障应对方案就是一次恢复,而不是一份共识事故复盘。只有当你能用一句话说清楚“流式复制无法满足的可用性要求”时,才考虑迁移到基于 Raft 的系统,并接受随之而来的写入延迟和法定人数运维成本。一旦你需要 Postgres 的 SQL 能力或多写入者的吞吐量,就该直接用 Postgres。如果这些都不合适,更广阔的嵌入式数据库世界也有其他选择,从可嵌入的图数据库到本地优先的 CRDT 领域都有。
打破误区的总结:分布式 SQLite 并不新鲜,不是廉价版的 Postgres,不只有一种设计,不会因为套上托管外壳就变得安全,也不是备份之上的免费升级。它是一个已有十二年历史、脉络清晰的设计空间,有三个角,各自适用于不同的问题。那些吃过亏的工程师,往往是从来没有把问题写下来的人。


