Tailscale 将数据库损坏追溯到 16 年前的 SQLite WAL 重置错误
去年年底,我们的正常运行时间非常不稳定。你可以在我们的状态页面上看到这个趋势,这种不稳定性持续到了新的一年。这些停机事件中的许多都是由一个单一的错误引起的,这个错误深埋在 SQLite 中。追踪这个错误花费了几个月的 intense 取证工作。现在夏天来了,我们有信心我们已经找到了这个错误,理解了它——更重要的是,我们修复了它。我们知道我们的客户期望 Tailscale 是一个可靠的服务,而几个月来我们没有做到这一点。这是扰动性的,我们对此感到抱歉。我们发布这篇博客文章是为了解释出了什么问题,我们是如何回应的,以及我们如何最终帮助发现了 SQLite 数据库核心的一个长期存在的错误。Tailscale 的数据库架构虽然我们的客户端作为一个单一的公共端点 (controlplane.tailscale.com) 与我们的控制平面进行交互,但我们内部的控制平面实际上被分成了一系列协调服务器(或“分片”)。每个 tailnet 同时只存在于一个内部分片上,但可以无缝迁移到另一个分片上。这些分片是内部实现细节:你不知道你的 tailnet 在哪个分片上,也永远不需要知道。每个分片都有一个 SQLite 数据库,其中保存了该分片上 tailnet 的所有信息。一个单一的 Go 进程专门访问该数据库,并服务于那些 tailnet 的控制平面。这个单写入设计正是 SQLite 的使用方式。自 2022 年以来,我们一直将 SQLite 作为我们的主要数据库,我们选择它是因为它是众所周知的、可靠的、广泛使用的。SQLite 是“无聊的技术”——以一种好的方式。许多公司在更大规模的部署中使用 SQLite 而没有问题,我们也期望能够无忧使用。在我们当前的备份管道中,我们每几分钟进行一次数据库的完整快照,然后将整个 SQLite 文件上传到 S3 存储桶。自 2023 年初以来,我们一直在没有事件的情况下运行这个设置。快进到去年八月,当一个读取那些 S3 备份的数据管道报告我们的一个数据库出现错误。我们用 SQLite 的 PRAGMA integrity_check 命令检查了备份,并发现它确实已损坏。SQLite 损坏是可能的,但这是非常不寻常的,在正常操作中你不应该碰到。我们修复了受影响的数据库,并调查原因,但没有成功。当以规模运营时,即使是稀有事件也可能频繁发生,因此,当它再次发生时,我们应该感到并不惊讶——而且是一次又一次。总的来说,在我们最终解决根本错误之前,我们在六个月内遇到了 19 次单独的数据库损坏实例。当你听到“数据库损坏”这个短语时,自然会担心数据丢失。因为我们的控制平面仅处理配置数据,这些数据库只包含关于你的 tailnet 和设备的元数据,但绝不会包含你的私钥或网络流量。在最早的事件中,恢复过程意味着一些新添加的设备或配置更改未能持久化,并且必须重新输入少量元数据。每当出现损坏时,我们都必须停止该分片上的控制平面进程,同时修复或恢复数据库。这对那条分片上的 tailnet 是痛苦的,因为它们的整个控制平面在那个恢复窗口期间消失。在早期事件中,那段停机时间超过一个小时,但我们逐渐加快了后续事件的恢复过程。每个 tailnet 都是一个网状网络,其中设备彼此通过点对点 WireGuard® 连接。当设备加入 tailnet 时,它必须从控制平面获取其他设备的列表,然后才能建立新连接——因此,如果设备在 SQLite 停机期间上线,它无法连接。当数据库正在修复时,已经在线的设备仍然保持相互连接,但它们无法了解网络的变化。这些 tailnet 也暂时失去了对基于 Web 的管理控制台和 Tailscale API 的访问。信任也受到了更广泛的影响。即使只有少数 tailnet 受到影响,我们仍会在状态页面上发布全球事件。许多人看到状态页面事件是一个不影响他们的事件。实际上,大多数分片和 tailnet 从未遇到过数据库损坏事件!尽管如此,反复的停机也侵蚀了信任,无论你是否受到直接影响。自第一次腐败事件开始,我们就知道这是对我们可靠性的严重威胁,我们投入了大量工程时间来解决这个问题——但修复并非易事。尝试找到错误这个错误抵抗了我们所有最初的尝试。我们查看了最近的更改,但没有一个似乎相关。没有人对我们与 SQLite 交互的底层代码进行过工作,因为这些代码都是多年以前编写的,直到那时也没有出现问题。我们重新审核了所有这些代码,仔细检查。
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡