Fastmail 的数据韧性:一次差点丢数据的备份实战
(或:我如何学会不再担心,爱上备份)
这是一个惊险的故事,用户因一个漏洞丢失了整个邮箱,但备份让我们得以恢复所有信息。
我们已经做这件事很久了
这是一篇关于原则的博客文章。原则很少改变,这也是你能读书的原因这是我十多年前写的一篇博客文章而“三类数据”至今仍和我写作时一样新鲜。
这也是一篇关于具体实施细节的博客文章。我们也不会很快改变这一点,这也是为什么那篇帖子里几乎所有其他内容依然正确!我们还在用git,也在用Debian。
关于Makefile和搜索数据库压缩的一些机制已经改变,但我们仍然会这样做。
更重要的是,我们仍然存储跨多台机器复制的数据,但如今这种保障更加强烈。至少其中一台机器位于不同的物理位置(目前是费城、圣路易斯或阿姆斯特丹)。
默认配置是主地点有两台机器,外加第三台。即使我们最大的站点暂时下线,我们也有足够的计算资源继续运营。
我们还有一项长期运行的系统——为Cyrus IMAP打造内部备份系统,而Cyrus IMAP服务器是我们大部分客户数据的存储地。我们2015年写过关于替代系统的博客最终被搁置,我们继续改进内部系统。
这个系统是现已上市作为开源Cyrus项目的一部分。它将出现在本故事中。
一个讨厌的虫子
2026年4月,Fastmail决定将账户拆分为独立的“商店”(单个Cyrus实例),分别用于欧盟和非欧盟客户,以便设立阿姆斯特丹分店。
作为其中一部分,大多数欧盟客户被迁移到新创建的商店,采用现有基于Cyrus复制的方式迁移用户。该机制经常用于平衡存储与自动移动,以及账户合并时将用户一起移动——但这次在大约一个月内完成的移动量要高得多。
期间,有8个用户账户的部分文件夹被bug完全清除。每次都包含了他们的收件箱,导致整个账户无法访问。和软件错误常见的一样,它是由多个行为相互作用引起的,这些行为单独来看似乎合理,但相互作用却很糟糕。
对于那些对事件完整技术细节感兴趣的人,请继续阅读。否则,可以直接跳到下一部分,看看备份是如何确保数据不丢失的。
直接原因是对无复制复制模式的良好更改,强制清除被同步用户的任何远程状态。这已经是拉取请求 5513.我们只有在将用户迁移到新商店时,Fastmail才使用无复制模式。
目前对用户来说还没有什么特别的选择,所以更激进是合理的。
这个漏洞与另一个变化一起工作,来自拉取请求 4852,确保在同步用户时,我们会遵循其重命名历史。
Cyrus 会为每个邮箱的 uniqueid 保留带有旧名称的墓碑一段时间,以便在常规复制中检测到重命名,并确保如果 IMAP 要求的不变式无法满足,我们绝不会重复使用 UIDVALIDITY。
单独来看,这些都完全合理。把它们合并起来,就出现了大问题——用户名被重复使用。我们很早就决定直接用主邮箱地址作为Cyrus的用户名,这样文件夹共享在IMAP上看起来就很对了。
多年来这也带来了一些痛苦!我们会在自己的域名上保留“已使用”的用户名一段时间,但如果你是在自己的域名创建账户,那么立即可以重复使用。
考虑这样一个情况:用户A被改名为B,然后创建一个新的不同用户A——同样的用户名,同一个商店。我们会将用户A同步到另一台服务器。
这一切正常,完整性检查通过,目前一切正常。
之后,我们尝试同步用户B。B 文件夹的名称历史中包含了用户 A 的文件夹名称。用户B的同步会进入目标服务器上的“新”用户A,清除所有带有该名称的文件夹。
这8个账户正是这样——移动系统清空了目标店三家副本的整个文件夹!
最初的修复尝试已经完成拉取请求 5961,但实际上并没有阻止问题。这是基于错误的假设,认为用户来自另一家商店,墓碑在目的地,而不是相反。
由于修复不完整,有一位在备份初始恢复前未完全迁移的用户再次损坏,直到最后才通过拉取请求 5988.
这种情况只发生在那些创建用户后,将其重命名为其他名称,然后重新创建相同原始用户名的Fastmail用户。而且只有在漏洞存在期间被移动的用户。
备份救了大家
Fastmail 运行 cyrus-imapd,带有插槽和存储布局,详见文档我们的帮助页面.复制协议内置了多种防护措施,所有内容都有校验和,常规滚动复制永远无法立即清除数据,只能标记以便一周左右的后续清理。
这可以防止普通用户造成的损害,以及大多数可能的虫子。不过这个漏洞直接突破了这些保护,使用复制协议立即清除文件夹。Move 系统需要能够删除内容,因为在确认用户成功迁移后,最后一步会清理源仓库上的副本!
Fastmail 还具备备份系统,将所有数据和元数据存储为仅附加的增量备份,偶尔进行重新打包,确保最近备份中的所有数据是安全的。我们每隔几小时定期备份每个用户,也会在他们账户移动服务器前后备份,这正是我们在这里的救命之力!
所有丢失的文件夹都从备份中恢复了,除了一个用户外,所有邮件都恢复了。
那个用户有5封已发送的邮件无法从备份中恢复。幸运的是,这些发送邮件的完全相同副本可以从#jmapsubmissionCyrus 内部的临时文件,使用了一些额外的定制工具。
最终,没有丢失任何邮件。
未来保护
5988 中的解决方案包含一个测试用例,可以重现那些移动过程中实际发生的模式,这意味着如果我们再次写出类似的错误,我们就能检测到。
我们也在考虑将复制协议更基于用户的收件箱唯一ID,而不是mboxname,这样可以完全避免这个问题,但这会是个更大的变化,可能还要几年后才会实现。
尽管不断改进,拯救我们的核心原则正是我在2014年写下的:“主副本——备份。复制它。尽一切可能确保它永远不会丢失。这些东西简直是金子。”通过纵深防御,我们确保即使是这个讨厌的漏洞也不会导致我们负责的数据丢失。