crates.io 事后分析:损坏的包下载

2023年7月21日 · Tobias Bieniek 代表 crates.io 团队

(基于 https://www.atlassian.com/incident-management/postmortem/templates)

摘要

在 2023-07-20 12:17 到 12:30 UTC 之间,由于部署中下载 URL 生成的错误,crates.io 的所有包下载都已损坏。

在此期间,crates.io 的平均每秒请求数为 4.71K,导致约 370 万个请求失败,包括来自 cargo 的重试尝试。

该事件是由触发生产部署的开发人员在部署后在我们的监控仪表板中看到每秒请求数升高而发现的。此时,请求数升高的根本原因尚不清楚,但一位社区成员通过 Zulip 通知了开发人员。

收到通知后,立即将损坏的部署回滚到之前的部署,再次修复了下载问题。

前导

在 2023-07-19 17:41 UTC,合并了一个 拉取请求 到 crates.io,完成了 crates.io 代码库的迁移,使用 object_store crate 进行 AWS S3 访问,而不是我们之前的自定义解决方案。

此拉取请求重构了包和自述文件下载端点生成重定向 URL 的方式。

故障

该拉取请求为之前未经测试的功能引入了一些测试,但不幸的是,它使用了与 crates.io 在生产环境中使用的环境变量内容不同的值。这导致生产代码路径未被正确测试。

生产代码路径包含一个错误,其中从“CDN 前缀”和“路径”组件生成的 URL 缺少斜杠 (/) 分隔符。

这导致 https://crates.io/api/v1/crates/smallvec/1.10.0/download 重定向到 https://static.crates.iocrates/smallvec/smallvec-1.10.0.crate,而不是 https://static.crates.io/crates/smallvec/smallvec-1.10.0.crate

影响

在 2023-07-20 12:17 到 12:30 UTC 之间的大约 13 分钟内,我们的用户遇到了此事件。

此事件影响了在此期间尝试从 crates.io 下载包文件的所有用户。

该问题在我们的用户运行 cargo 时看到如下错误体现出来

warning: spurious network error (3 tries remaining): [6] Couldn't resolve host name (Could not resolve host: static.crates.iocrates)
warning: spurious network error (2 tries remaining): [6] Couldn't resolve host name (Could not resolve host: static.crates.iocrates)
warning: spurious network error (1 tries remaining): [6] Couldn't resolve host name (Could not resolve host: static.crates.iocrates)
error: failed to download from `https://crates.io/api/v1/crates/serde_derive/1.0.173/download`

https://github.com/rust-lang/crates.io/issues/6850 被提交并获得了 12 次赞。

检测

触发生产部署的开发人员在部署期间监控了 crates.io Grafana 仪表板,并注意到下载端点的每秒请求数水平升高。这是 cargo 在放弃之前多次重试下载的症状。

部署后约 11 分钟,一位社区成员通过 Zulip 通知了 crates.io 团队有关已打开的 GitHub 问题,其中描述了失败的下载。

响应

检测到事件后,部署开发人员立即通过 Heroku 用户界面启动了回滚到之前部署的操作。由于登录过程和确保使用了用户界面中正确的按钮,此过程花费了大约一分钟。

恢复

回滚到之前的部署后,系统立即自行恢复,并再次生成了正确的重定向 URL。

随后开发并合并了对损坏的拉取请求的修复,包括对具有更多真实值的损坏代码路径的更多测试。然后在部署到生产环境之前,在暂存环境中测试了该修复程序。

时间线

2023-07-19

2023-07-20

根本原因识别:五问法

  • 包和自述文件下载的重定向 URL 在生产环境中损坏。

    为什么重定向 URL 损坏?

    • 拉取请求 #6834 中引入了一个错误,该错误一直进入了我们的生产环境。

      为什么此拉取请求中引入了错误?

      • 该拉取请求引入了测试,但没有测试所有代码路径。

        为什么拉取请求没有测试所有代码路径?

        • 代码的结构使得使用不同的“CDN 前缀”值进行测试变得复杂。

          为什么代码的结构使得使用不同的值进行测试变得复杂?

          • 该代码之前没有进行单元测试,重构停在了可以使用硬编码值至少测试代码的点上。

            为什么重构停在该点?

            • 开发人员认为“现在足够好”。
      • 该拉取请求没有经过另一位开发人员的审查。

        为什么该拉取请求没有经过另一位开发人员的审查?

        • 创建拉取请求的开发人员错误地判断了拉取请求中错误的潜在影响。他们在几个小时后没有明确要求 crates.io 团队进行审查,就自己合并了它。

          为什么没有向 crates.io 团队请求代码审查?

          • crates.io 团队中活跃的团队成员数量很少。审查每月由一位全职受雇于 crates.io 的开发人员提出的数十个拉取请求,对于 crates.io 团队的其他成员来说将是精疲力竭。对于全职受雇的开发人员来说,如果他们的大部分时间都因等待审查而被阻止,那也不会很好。当前的工作方式是仅对高影响力的拉取请求请求代码审查。

          为什么潜在的影响被误判了?

          • 开发人员忘记考虑此更改影响了 crates.io 的包下载端点这一事实,该端点处理了服务器 99% 的流量。

            为什么开发人员忘记检查是否影响了高优先级端点?

            • 没有清单或指南描述在哪些情况下应将拉取请求视为具有高潜在影响,因此需要 crates.io 团队的明确代码审查。

      为什么该错误进入了生产环境?

      • 在将其提升到生产环境之前,未在暂存环境中测试包下载端点。

        为什么未测试包下载?

        • 暂存环境的测试计划仅包括发布新版本并查看其反映在网站和包索引存储库中。

          为什么测试计划不包括包下载?

          • 由于对 crates.io 的所有请求中有 99% 是用于包下载,因此测试计划绝对应该包括此过程。尽管如此,网页上故意没有下载按钮,因此必须手动构建下载 URL。

            为什么需要手动构建下载 URL?

            • 因为我们暂存环境中的冒烟测试过程目前是一个完全手动的过程,没有任何自动化。

根本原因

  • 失败代码的结构使其难以测试不同的变体和代码路径。
  • 没有清单描述哪些拉取请求应被视为高影响力。
  • 暂存环境上的冒烟测试过程不包括包下载,并且是一个手动过程。

待办事项检查

待办事项中没有可能导致此事件的特定条目。

复发情况

之前的事件导致 crate 发布功能失效。从该事件中吸取的教训是,要确保冒烟测试程序包含发布过程。不幸的是,这并没有包括 crate 文件下载。

经验教训

  • 如果能够更早地识别出该症状是由 cargo 重试行为引起的,那么从部署到事件通知的检测时间可能会更快。 然而,由于 Grafana 数字的变化,部署开发人员的高度警觉性反而有助于更快地解决此问题。
  • 从事件通知到回滚并解决问题的响应时间很快。
  • 所有代码的结构都应易于测试不同的代码路径。
  • 我们需要更清晰的规则来规定哪些拉取请求需要代码审查。
  • 冒烟测试程序应包括 crate 下载。
  • 冒烟测试程序应尽可能自动化。

纠正措施

  • 在预发布环境的冒烟测试计划中包含 crate 下载
  • 自动化预发布环境的冒烟测试
  • 制定关于哪些拉取请求需要明确的代码审查的规则