Tailscale 追查 SQLite 16 年 WAL 重置 bug 始末
Tailscale Traces Database Corruption to 16y/o SQLite WAL-Reset Bug
做数据库或基础设施的工程师必看,这是一份完整的 SQLite 深度故障排查实录,从现象到取证再到根因定位的路径都值得借鉴,建议收藏复盘。
Blog|insightsAugust 12, 2026
博客|见解2026年8月12日
How we tracked down a 16-year-old SQLite bug
我们如何追踪到一个存在16年的SQLite bug
At the end of last year, our uptime was pretty shaky. You can see this trend on our status page, and that instability continued into the new year. Many of these outages were caused by a single bug, deep in SQLite. It took months of intense forensics to track it down.
去年年底,我们的正常运行时间相当不稳定。您可以在我们的状态页面上看到这一趋势,而且这种不稳定一直持续到新的一年。许多中断是由SQLite深处的一个bug引起的。我们花了数月时间进行密集的取证调查才找到它。
Now we’re in summer, we’re confident that we’ve found the bug, that we understand it—and more importantly, that we’ve fixed it.
现在已是夏季,我们确信已经找到了这个bug,理解了它——更重要的是,已经修复了它。
We know our customers expect Tailscale to be a reliable service, and for several months we didn’t live up to that promise. That’s disruptive, and we’re sorry. We’re publishing this blog post to explain what went wrong, how we responded, and how we ultimately helped to uncover a long-standing bug in the heart of the SQLite database.
我们知道客户期望Tailscale是一个可靠的服务,而几个月来我们未能兑现这一承诺。这造成了干扰,我们深表歉意。我们发布这篇博客是为了解释出了什么问题,我们如何应对,以及我们最终如何帮助揭示了SQLite数据库核心中一个长期存在的bug。
Tailscale’s database architecture
Tailscale的数据库架构
While our clients interact with our control plane as a single public endpoint (controlplane.tailscale.com), internally, our control plane is split into a series of coordination servers (or “shards”). Each tailnet lives on one internal shard at a time, but can migrate seamlessly from one to another. These shards are an internal implementation detail: you don’t know what shard your tailnet is on, and you never need to.
虽然我们的客户端将我们的控制平面视为一个单一的公共端点(controlplane.tailscale.com),但在内部,我们的控制平面被划分为一系列协调服务器(或“分片”)。每个tailnet一次只位于一个内部分片上,但可以在分片之间无缝迁移。这些分片是内部实现细节:您不知道您的tailnet在哪个分片上,也无需知道。
Each shard has an SQLite database that holds all the information about the tailnets on that shard. A single Go process exclusively accesses that database, and serves the control plane for those tailnets. This single-writer design is exactly how SQLite is meant to be used.
每个分片都有一个SQLite数据库,保存该分片上所有tailnet的信息。一个单独的Go进程独占访问该数据库,并为这些tailnet提供控制平面服务。这种单写者设计正是SQLite的预期使用方式。
We’ve used SQLite as our primary database since 2022, and we chose it because it's well-known, reliable, and widely used. SQLite is “boring technology”—in a good way. Many companies use SQLite in much larger deployments without issue, and we expected the same stress-free usage.
自2022年以来,我们一直使用SQLite作为我们的主要数据库,我们选择它是因为它知名、可靠且被广泛使用。SQLite是“无聊的技术”——这是褒义。许多公司在更大的部署中使用SQLite而没有任何问题,我们也期望同样无忧的使用体验。
In our current backup pipeline, we take a complete snapshot of the database every few minutes, then upload the entire SQLite file to an S3 bucket. We’d been running this setup without incident since early 2023.
在我们当前的备份管道中,我们每隔几分钟对数据库进行一次完整快照,然后将整个SQLite文件上传到S3存储桶。自2023年初以来,我们一直运行此设置,没有发生任何事故。
Fast forward to August last year, when a data pipeline that reads those S3 backups reported an error in one of our databases. We ran SQLite’s PRAGMA integrity_check command against the backup, and found it was indeed corrupted. SQLite corruption is possible, but it’s highly unusual and not something you should encounter in normal operation. We repaired the affected database, and investigated the cause, but to no avail.
时间快进到去年八月,一个读取这些S3备份的数据管道报告我们其中一个数据库出现错误。我们对备份运行了SQLite的PRAGMA integrity_check命令,发现它确实已损坏。SQLite损坏是可能的,但极不寻常,在正常操作中不应遇到。我们修复了受影响的数据库,并调查了原因,但无济于事。
When operating at scale, even rare events can occur with some frequency, so we should have been unsurprised when it happened again—and again, and again, and again. In total, we faced 19 separate instances of database corruption over six months before we finally resolved the underlying bug.
在规模运营时,即使是罕见的事件也可能频繁发生,所以当它一次又一次地发生时,我们本应不感到惊讶。总共,在最终解决根本错误之前,我们在六个月内遇到了19次独立的数据库损坏事件。
When you hear the phrase “database corruption”, it’s natural to worry about data loss. Because our control plane only handles configuration data, these databases contain metadata about your tailnet and devices, but never your private encryption keys or network traffic. In the earliest incidents, the recovery process meant a handful of newly added devices or configuration changes didn’t persist, and a small amount of metadata had to be re-entered.
当你听到“数据库损坏”这个短语时,自然会担心数据丢失。因为我们的控制平面只处理配置数据,这些数据库包含关于你的tailnet和设备元数据,但绝不包含你的私密加密密钥或网络流量。在最早的事件中,恢复过程意味着少数新添加的设备或配置更改没有持久化,少量元数据必须重新输入。
Whenever corruption occurred, we had to stop the control plane process on the shard while we repaired or restored the database. This was painful for tailnets on that shard, because their entire control plane disappeared during that recovery window. In the early incidents, that downtime was over an hour, but we gradually sped up the recovery process over subsequent incidents.
每当发生损坏时,我们必须在修复或恢复数据库的同时停止该分片上的控制平面进程。这对该分片上的tailnet来说是痛苦的,因为在恢复窗口期间,它们的整个控制平面消失了。在早期事件中,停机时间超过一个小时,但在后续事件中我们逐渐加快了恢复过程。
Each tailnet is a mesh network, where devices make peer-to-peer WireGuard® connections to each other. When a device joins the tailnet, it has to get a list of other devices from the control plane before it can establish new connections—so if a device came online during the SQLite downtime, it couldn’t connect. While the database was being repaired, devices already online remained connected to each other, but they couldn’t learn about changes to the network. Those tailnets also temporarily lost access to the web-based admin console and the Tailscale API.
每个tailnet都是一个网状网络,设备之间建立点对点的WireGuard®连接。当设备加入tailnet时,它必须先从控制平面获取其他设备的列表,然后才能建立新连接——因此,如果设备在SQLite停机期间上线,它将无法连接。在数据库修复期间,已经在线设备之间保持连接,但它们无法了解网络的变化。这些tailnet还暂时失去了基于Web的管理控制台和Tailscale API的访问权限。
There’s also a broader impact on trust. We post a global incident on our status page even when only a small number of tailnets are affected. Many people saw a status page event for an incident that didn’t affect them. Indeed, the majority of shards and tailnets were never involved in a database corruption incident! Nonetheless, repeated downtime erodes trust, whether or not you’re directly affected.
还有更广泛的信任影响。即使只有少数tailnet受到影响,我们也会在状态页面上发布全球事件。许多人看到了一个不影响他们的事件状态页面。事实上,大多数分片和tailnet从未涉及数据库损坏事件!尽管如此,重复的停机侵蚀信任,无论你是否直接受到影响。
From the very first instance of corruption, we knew this was a serious threat to our reliability, and we threw a lot of engineering time at the problem—but the fix wasn’t easy.
从第一次损坏开始,我们就知道这是对我们可靠性的严重威胁,我们投入了大量工程时间来解决这个问题——但修复并不容易。
Trying to find the fault
试图找到故障
This bug resisted all our initial attempts to find it.
这个错误抵抗了我们所有最初的寻找尝试。
We looked at recent changes, but there weren’t any that seemed relevant. Nobody had been working on our low-level code that interacts with SQLite, because it had all been written years ago and presented no issues up until that point. We re-reviewed all of that code with a fine-toothed comb to look for previously missed bugs, but we didn’t find anything that would cause the corruption we were seeing.
我们查看了最近的更改,但没有发现任何相关的更改。没有人修改过我们与 SQLite 交互的底层代码,因为这些代码都是多年前编写的,直到那时都没有出现过问题。我们仔细重新审查了所有这些代码,寻找之前遗漏的 bug,但没有发现任何会导致我们看到的损坏的问题。
We looked for common factors between corruption incidents, but we couldn’t find any. It wasn’t tied to a single shard, or customer, or tailnet feature, or time of day, or load level. We were at a loss for what might be triggering the behaviour.
我们寻找损坏事件之间的共同因素,但没有找到任何共同点。它不限于某个分片、客户、tailnet 功能、一天中的某个时间或负载水平。我们不知道是什么触发了这种行为。
This lack of reliable trigger conditions meant we couldn’t reproduce the bug synthetically. Instead, we had to rely on deploying passive, forensic telemetry in our live environment to catch the corruption red-handed. Gathering live diagnostics for a database issue is the last thing we wanted to do, but we had no choice.
由于缺乏可靠的触发条件,我们无法在合成环境中重现该 bug。相反,我们不得不依赖在实时环境中部署被动的取证遥测来当场捕获损坏。为数据库问题收集实时诊断是我们最不想做的事情,但我们别无选择。
As an additional complication, the corruption didn’t occur on a regular schedule. Sometimes incidents would be hours apart, other times weeks. This made it difficult to predict progress or plan further work, because we were never sure when we’d get our next diagnostic dump. We had a six-week period between October and December when there were no corruption incidents, before they returned as an unwelcome Christmas present.
另一个复杂因素是,损坏并非定期发生。有时事件间隔数小时,有时则相隔数周。这使得我们难以预测进展或规划后续工作,因为我们永远不确定何时会获得下一个诊断转储。在 10 月到 12 月之间,我们有六周没有发生损坏事件,然后它们作为不受欢迎的圣诞礼物又回来了。
Because this wouldn’t be a quick or easy fix, we reached out to the SQLite developers for a professional support contract. This was a great decision. It gave us direct access to their deep expertise and experience, and we had many detailed technical conversations about our architecture and our incidents.
由于这不是一个快速或简单的修复,我们联系了 SQLite 开发者,签订了专业支持合同。这是一个很棒的决定。它让我们能够直接接触他们深厚的专业知识和经验,我们就我们的架构和事件进行了许多详细的技术讨论。
Between Tailscale engineering and the SQLite core developers, we mapped out several theories for what might be causing the corruption—including broken POSIX locks on close(), mismanaging memory owned by SQLite, or accidentally using SQLite from multiple threads while disabling thread safety. After every incident, we gathered more data, added more diagnostics, and systematically ruled out these theories. We were gradually converging on the true bug.
在 Tailscale 工程团队和 SQLite 核心开发者之间,我们提出了几种可能导致损坏的理论——包括 close() 时 POSIX 锁被破坏、错误管理 SQLite 拥有的内存,或者在禁用线程安全的情况下意外地从多个线程使用 SQLite。每次事件后,我们收集更多数据,添加更多诊断,并系统地排除了这些理论。我们逐渐接近真正的 bug。
The transactions that didn’t bark
没有发出警告的事务
While we were investigating the root cause, we still had a live platform to run. We took aggressive steps to automate recovery and minimize downtime:
在调查根本原因的同时,我们仍然有一个实时平台需要运行。我们采取了积极的措施来自动化恢复并最小化停机时间:
- Configuring our control plane shards to hard-stop immediately upon encountering corruption
- Deploying an automated backup monitor that continuously ran PRAGMA integrity_check over our backups
- Improving our runbooks and on-call training
- 配置我们的控制平面分片,在遇到损坏时立即硬停止
- 部署一个自动化备份监控器,持续对我们的备份运行PRAGMA完整性检查
- 改进我们的运行手册和值班培训
These efforts cut our response time to under an hour—and then we discovered an unexpected clue.
这些努力将我们的响应时间缩短到一小时以内——然后我们发现了一个意想不到的线索。
We wanted a way to restore service that didn’t involve rolling back to the last known-good backup (which would lose a lot of data) or repairing the known-corrupted database (which was potentially risky).
我们想要一种恢复服务的方式,既不需要回滚到最后一个已知良好的备份(那会丢失大量数据),也不需要修复已知损坏的数据库(那可能有风险)。
更进一步:量化金融体系
看懂新闻只是起点——沿量化金融路径,把它变成能交付的工程能力