Tailscale 官方博客本周公布了一次长达半年、跨越 19 次事故的数据库损坏排查历程:最终在 SQLite 核心开发团队的帮助下,揪出了一个潜伏至少 16 年的罕见竞态 bug——”WAL-Reset bug”。这是一个关于”无聊技术”如何藏下深雷、以及专业支持如何物有所值的绝佳案例。
背景:控制面全部押注 SQLite
Tailscale 的控制平面由一系列协调服务器(分片)组成,每个 tailnet 在同一时间只归属于一个分片。每个分片都有一个 SQLite 数据库保存该分片上所有 tailnet 的元数据,且由一个 Go 进程独占访问——这是 SQLite 官方推荐的单写者用法。自 2022 年起,SQLite 就是 Tailscale 的主数据库,理由是它”众所周知、可靠、被广泛使用”,是典型的”无聊技术”。
事故:6 个月 19 次损坏
尽管设计正确,2025 年某段时间起数据库损坏开始反复出现:六个月里累计 19 次。损坏意味着部分新加入的设备或配置变更没有持久化,少量元数据需要重录。恢复期间该分片的整个控制面下线,早期单次停机超过一小时,期间新上线的设备无法加入 tailnet,管理后台和 API 也暂时不可用。即使只影响少数 tailnet,团队也会在状态页发布全局事故公告——反复的停机在侵蚀信任。
排查:常规手段全部失效
最初的排查一无所获:近期变更无关(底层 SQLite 交互代码是多年前写的)、逐行复查没发现 bug、损坏事件找不到任何共同因素(不绑定分片、客户、功能、时段或负载),也无法合成复现。团队只能在生产环境部署被动的取证遥测,等待下一次损坏”抓现行”。事故间隔从几小时到几周不等,最长的空窗期达六周。
转机:专业支持 + 事务日志管线
团队决定购买 SQLite 开发者的专业支持,随后做了两件关键的事。第一,建立事务日志管线:把每个修改数据库的 SQL 语句流式记录到独立日志,由于 SQLite 是单写者数据库,事务历史完全线性且确定,回放日志即可安全绕过损坏恢复数据。第二,这套管线意外给出了线索——有两次回放失败,显示一个事务已提交的写入对后续事务”不可见”,写入凭空消失且不报错。
根因:checkpoint 与写入之间的罕见竞态
SQLite 开发者为诊断专门开发了 tmstmpvfs shim——一个包装虚拟文件系统层的追踪工具,记录数据库变更的额外日志。部署到生产环境后,下一次损坏立刻暴露了真相:checkpoint(把 WAL 日志页复制回主库文件的过程)与写事务之间存在一个极罕见的数据竞争。若写入恰好在 checkpoint 的特定时刻发生,checkpoint 会误以为某些页已从 WAL 复制到主库,但实际上没有——这些页上的数据被永久丢失,引用它们的索引等页面却已写入,数据库就此损坏。
这个 bug 被命名为 “WAL-Reset bug”,估计已在 SQLite 中存在至少 16 年(WAL 机制自 2010 年引入)。它罕见到了什么程度?SQLite 开发者不得不在测试环境中专门加代码才能触发它。修复方式是在 checkpoint 函数中增加一个检查,检测 WAL 是否已被其他线程重置。Tailscale 之所以更容易踩中,是因为他们手动控制 checkpoint 过程并且频率非常激进——罕见条件也架不住高频触发。
结局:修复 + 一次假警报
修复随 SQLite 3.52.0 发布,Tailscale 先金丝雀分片后全量滚动部署。然而备份监控随即全红:13 个数据库报”损坏”。虚惊一场——这是 3.52.0 中另一个 bug:表达式索引陈旧问题(文本转浮点的舍入行为被优化改动),误报为损坏。与 SQLite 团队确认后同样解决。
社区反响
HN 上 133 条评论中,开发者 simonw 提到 Tailscale 资助了开源的 SQLite VFS shim,这帮助隔离了竞态,未来也能用于追踪类似 bug。多位用户赞赏 Tailscale 公开透明的故障处理方式,也有人感慨:连 SQLite 这种被全球亿级设备使用的代码,都会藏 16 年才暴露的竞态——”无聊技术”从不无聊。
原文链接: How Tailscale helped find the SQLite WAL-Reset bug | HN 讨论: news.ycombinator.com/item?id=49272832