Tailscale 用了三年 SQLite,今天才发现这个 16 年前的 bug 一直在偷偷删数据——数据库损坏的完整复盘

Tailscale 用了三年 SQLite,今天才发现这个 16 年前的 bug 一直在偷偷删数据——数据库损坏的完整复盘

Tailscale 是一个用 Go 写的网络工具,背后依赖 SQLite 做状态存储。最近他们在排查一个用户反馈的数据库损坏问题时,顺藤摸瓜发现了一个藏了 16 年的 SQLite 底层 bug——WAL 模式的 Reset 操作在特定时机下会把数据库直接搞坏,而且没有任何报错,等你发现的时候数据已经没了。

这不是 Tailscale 自己的问题。只要你用 SQLite 的 WAL 模式,就有可能踩到这个坑。


第一个问题:为什么 Tailscale 会遇到数据库损坏

Tailscale 用 SQLite 做 DNS 配置、路由表、设备列表这些核心状态存储。它用的是 WAL 模式——简单说就是写操作不锁读,读操作不锁写,比传统 journal 模式性能好很多,尤其是并发场景下。

问题出在 Tailscale 的一个运维操作上:定期把 SQLite 文件从一端复制到另一端做状态同步。这个操作在 Linux 上用的是 sendfile() 系统调用。

sendfile() 的问题是:它只能传文件内容,传不了文件元数据。SQLite 的 WAL 模式依赖一个叫 WAL-index 的共享内存结构,这个结构在文件系统里表现为一个 .shm 文件。sendfile() 传完之后,接收端的 .shm 文件还是空的或者过时的——SQLite 不知道 WAL-index 变了,继续用它,最后数据库就坏了。


第二个问题:16 年前这个 bug 就在了,为什么今天才发现

SQLite 官方其实在 2010 年就加了 PRAGMA wal_checkpointer 来处理这个问题,但 Tailscale 的代码没有用这个机制。更关键的是,SQLite 的损坏检测逻辑有一个漏洞:WAL-mode 数据库在检测到 WAL-index 损坏时,默认行为是「静默重建」,不会报错,开发者根本不知道发生了损坏,直到数据已经丢了。

Tailscale 的复盘里提到,他们查了 SQLite 的 changelog,发现这个问题最早可以追溯到 2010 年的 commit,但从来没有被标记为 bug,也没有 release note。


第三个问题:怎么知道自己有没有踩到这个坑

如果你用 SQLite WAL 模式,有这几个信号要警觉:

  1. 数据库文件突然变大 —— WAL-index 损坏后,SQLite 会尝试恢复,可能产生大量 WAL 日志
  2. 某些查询突然返回空结果 —— 损坏影响的是特定页面,不是整个文件,所以是「部分丢失」
  3. 应用没有报错但状态不对 —— SQLite 的损坏检测默认静默

Tailscale 提供的检测方式是:定期用 PRAGMA integrity_check 做全表扫描,发现问题立即告警。


怎么修

Tailscale 的解法是:在复制数据库之前,先执行 PRAGMA wal_checkpoint(FULL) 把所有 WAL 日志写回主数据库,然后关闭 WAL 连接,再复制。这样 .shm 文件就是干净的。

如果你也需要定期同步 SQLite 文件,这是最小改动、最通用的解法,不需要改 SQLite 源码,也不需要等官方修。

复制前必须这两步:

  • 执行 PRAGMA wal_checkpoint(FULL)
  • 执行 PRAGMA journal_mode=DELETE(强制退出 WAL 模式)

然后再 sendfile()。


总结一下

这个 bug 藏了 16 年,Tailscale 不是唯一踩坑的。只要你用 SQLite WAL 模式 + 需要跨文件同步状态,就有可能遇到。核心原因就一句话:SQLite 的 WAL-index 和数据库文件是两个东西,只复制文件不复制索引,数据库就坏了

修法也很直接:复制前先 checkpoint,强制退出 WAL 模式,再动手。下次遇到数据库莫名损坏,先查 WAL-index 有没有同步。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 192 阅读