crates.io 事后复盘:损坏的 crate 下载

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

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

摘要

在世界协调时 (UTC) 2023 年 7 月 20 日 12:17 至 12:30 期间,crates.io 上的所有 crate 下载都出现故障,原因是某次部署在下载 URL 生成中包含一个 bug。

在此期间,crates.io 的平均请求速率为每秒 4.71 千次,导致约 370 万次失败请求,包括 cargo 的重试尝试。

事件最初是由负责触发生产部署的开发者注意到的,他在部署后发现监控面板中的每秒请求数有所升高。此时,请求数升高的根本原因尚不明确,但一位社区成员通过 Zulip 通知了该开发者。

收到通知后,损坏的部署立即回滚到先前的版本,下载功能恢复正常。

事件前情

在世界协调时 (UTC) 2023 年 7 月 19 日 17:41,向 crates.io 提交的一个拉取请求被合并,完成了 crates.io 代码库迁移到使用 object_store crate 进行 AWS S3 访问,取代了我们之前自定义的解决方案。

此拉取请求重构了 crate 和 readme 下载端点生成重定向 URL 的方式。

故障

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

生产环境中的代码路径包含一个 bug,从“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

影响

在世界协调时 (UTC) 2023 年 7 月 20 日 12:17 至 12:30 期间,约 13 分钟内,我们的用户经历了此次事件。

此次事件影响了当时所有试图从 crates.io 下载 crate 文件的用户。

用户在运行 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 issue,其中描述了下载失败的问题。

响应

事件被检测到后,部署开发者立即通过 Heroku 用户界面启动了回滚到先前部署的操作。由于需要登录并确保使用正确的用户界面按钮,此过程耗时约一分钟。

恢复

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

随后开发并合并了对问题拉取请求的修复,其中包括使用更接近实际的值对损坏代码路径的更多测试。该修复首先在 staging 环境中进行测试,然后才部署到生产环境。

时间线

2023-07-19

2023-07-20

根本原因分析:五个为什么

  • 生产环境中 crate 和 readme 下载的重定向 URL 损坏了。

    为什么重定向 URL 会损坏?

    • 在拉取请求 #6834 中引入了一个 bug,并一直影响到了我们的生产环境。

      为什么这个拉取请求会引入 bug?

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

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

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

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

          • 这段代码之前没有进行过单元测试,并且重构停在一个可以至少用硬编码值进行测试的点上。

            为什么重构停在了那个点上?

            • 开发者认为“目前已经足够好”。
      • 这个拉取请求没有经过另一位开发者的评审。

        为什么这个拉取请求没有经过另一位开发者的评审?

        • 创建拉取请求的开发者低估了这个拉取请求中 bug 的潜在影响。他们没有明确请求 crates.io 团队进行评审,并在几个小时后自行合并了它。

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

          • crates.io 团队的活跃成员数量相当少。每月评审由一名全职在 crates.io 工作的开发者提交的几十个拉取请求,对于 crates.io 团队的其他成员来说会导致倦怠。对于那位全职开发者来说,如果他们大部分时间都在等待评审而被阻塞,也会很不顺利。目前的工作方式是只对高影响的拉取请求请求代码评审。

          为什么会低估潜在影响?

          • 开发者忘记考虑这项更改会影响 crates.io 的 crate 下载端点,而这个端点处理了服务器 99% 的流量。

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

            • 目前没有检查清单或指南说明在何种情况下拉取请求应被视为具有高潜在影响,从而需要 crates.io 团队的明确代码评审。

      为什么 bug 会进入生产环境?

      • 在将其提升到生产环境之前,没有在 staging 环境中测试 crate 下载端点。

        为什么没有测试 crate 下载?

        • staging 环境的测试计划只包括发布一个新版本,并查看其在网站和包索引仓库中的反映。

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

          • 鉴于 crates.io 99% 的请求都是 crate 下载,测试计划肯定应该包括这个过程。不过,网页上故意没有下载按钮,因此下载 URL 必须手动构建。

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

            • 因为我们 staging 环境上的冒烟测试流程目前是完全手动的,没有任何自动化。

根本原因

  • 出现故障的代码结构使得测试不同的变体和代码路径变得困难。
  • 没有清单说明哪些拉取请求应被视为高影响的。
  • staging 环境上的冒烟测试流程不包括 crate 下载,并且是手动过程。

待办事项检查

待办事项中没有可能阻止此次事件的特定项。

重复发生

先前的一起事件导致 crate 发布不再起作用。从那起事件中获得的经验是确保冒烟测试流程包含发布过程。不幸的是,这并没有包括 crate 文件下载。

经验教训

  • 如果症状能更早地被识别为由 cargo 重试行为引起,从部署到事件通知的检测时间可能会更快。然而,负责部署的开发者因 Grafana 数据变化而提高了警觉性,这有助于更快地解决此问题。
  • 从事件通知到回滚并解决问题的响应时间很快。
  • 所有代码的结构都应便于测试不同的代码路径。
  • 我们需要更明确的规则来确定哪些拉取请求需要进行代码评审。
  • 冒烟测试流程应包括 crate 下载。
  • 冒烟测试流程应尽可能自动化。

纠正措施

  • 将 crate 下载纳入 staging 环境的冒烟测试计划
  • 自动化 staging 环境的冒烟测试
  • 制定关于哪些拉取请求需要明确代码评审的规则