2022 年 1 月 13 日,全球用户在近两个小时的时间内无法使用 Firefox。此事件打断了许多人的工作流程。本文重点介绍了一系列复杂事件和情况,这些事件和情况共同触发了 Firefox 网络代码深处的错误。
发生了什么?
Firefox 拥有一系列服务器和相关基础设施,用于处理多个内部服务。其中包括更新、遥测、证书管理、崩溃报告和其他类似功能。此基础设施由不同的云服务提供商托管,这些提供商使用负载均衡器将负载均匀分布到服务器之间。对于那些托管在 Google Cloud Platform (GCP) 上的服务,这些负载均衡器具有与它们应宣传的 HTTP 协议相关的设置,其中一项设置是 HTTP/3 支持,它有三种状态:“启用”、“禁用”或“自动(默认)”。我们的负载均衡器设置为“自动(默认)”设置,2022 年 1 月 13 日 UTC 时间 07:28,GCP 部署了一个未经宣布的更改,使 HTTP/3 成为默认设置。由于 Firefox 在支持的情况下使用 HTTP/3,从那时起,Firefox 与服务基础设施建立的一些连接将使用 HTTP/3 而不是以前使用的 HTTP/2 协议。 ¹
不久之后,我们注意到通过我们的崩溃报告器报告的崩溃数量激增,并且还收到了来自 Mozilla 内部和外部的多个报告,描述了浏览器挂起。
作为事件响应流程的一部分,我们很快发现客户端在对 Firefox 内部服务之一的网络请求中挂起。但是,在这一点上,我们既没有找到导致这种情况发生的解释,也没有找到问题的范围。我们继续寻找“触发器”——必须发生的一些变化才能启动问题。我们发现,我们没有发布更新或配置更改,这些更改可能导致此问题。同时,我们牢记 HTTP/3 自 Firefox 88 起就已启用,并且正在被一些流行网站积极使用。
虽然我们看不到,但我们怀疑我们的云提供商之一已经推出了一些“不可见”的更改,这些更改以某种方式修改了负载均衡器行为。经过仔细检查,我们发现没有更改任何设置。然后,我们通过日志发现,由于某种原因,我们的遥测服务的负载均衡器正在提供 HTTP/3 连接,而之前它们没有这样做。我们于 UTC 时间 09:12 在 GCP 上明确禁用了 HTTP/3。这为我们的用户解除了阻塞,但我们仍然不确定根本原因,并且在不知道原因的情况下,我们无法确定这是否会影响其他 HTTP/3 连接。
¹ 一些高度关键的服务,例如更新,使用特殊的 beConservative
标志,以防止在其连接中使用任何实验性技术(例如 HTTP/3)。
特殊成分的组合
我们很快意识到,必须有一些特殊情况的组合才能发生挂起。我们使用各种工具和远程服务执行了许多测试,但无法重现问题,即使是与遥测预发布服务器(仅用于测试部署的服务器,我们已将其保留在原始配置中以进行测试)的常规连接。但是,对于 Firefox 本身,我们能够使用预发布服务器重现该问题。
经过进一步调试,我们找到了发生此错误所需的“特殊成分”。所有 HTTP/3 连接都通过 Necko(我们的网络堆栈)进行。但是,需要直接网络访问的 Rust 组件不会直接使用 Necko,而是通过称为 viaduct
的中间库调用它。
为了理解为什么这很重要,我们首先需要了解 Necko 的内部结构,特别是关于 HTTP/3 上传请求。对于此类请求,更高层的 Necko API² 检查是否存在 Content-Length
标头,如果不存在,它将自动添加。较低层的 HTTP/3 代码稍后将依靠此标头来确定请求大小。这对于我们的代码中的 Web 内容和其他请求来说效果很好。
但是,当请求首先通过 viaduct
时,viaduct
会将每个标头转换为小写,然后将其传递给 Necko。而问题就在这里:Necko 中的 API 检查不区分大小写,而较低层的 HTTP/3 代码区分大小写。因此,如果任何代码要添加 Content-Length
标头并将请求通过 viaduct
传递,它将通过 Necko API 检查,但 HTTP/3 代码将找不到标头。
碰巧的是,遥测目前是 Firefox 桌面中唯一使用网络堆栈并添加 Content-Length
标头的基于 Rust 的组件。这就是为什么禁用遥测的用户会看到此问题得到解决的原因,即使问题本身与遥测功能无关,并且可能以其他方式触发。
² 这些是内部 API,Web 内容无法访问。
无限循环
在负载均衡器更改到位并且新 Rust 服务中的特殊代码路径现在处于活动状态的情况下,触发用户问题的必要最终成分是在 Necko HTTP/3 代码深处。
在处理请求时,代码 以区分大小写的方式查找该字段,并且由于该标头已由 viaduct
转换为小写,因此未能找到该标头。如果没有该标头,Necko 代码将确定该请求已完成,导致真正的请求正文未发送。但是,此代码仅在没有其他内容要发送时才会终止。这 意外状态导致代码无限循环,而不是返回错误。因为所有网络请求都通过一个套接字线程,所以此循环阻止了任何进一步的网络通信,并使 Firefox 无法响应,无法加载 Web 内容。
经验教训
正如经常发生的那样,问题比最初看起来复杂得多,并且有许多相互作用的因素。我们已经确定了一些关键因素,包括
-
GCP 部署 HTTP/3 作为默认设置没有宣布。我们正在积极与他们合作改善这种情况。我们意识到,宣布(通常发送的)可能不会完全降低事件风险,但它可能会触发更受控的实验(例如,在预发布环境中)和部署。
-
我们对负载均衡器使用“自动(默认)”而不是更明确的选择,导致部署自动进行。我们正在审查所有服务配置,以避免将来出现类似错误。
-
我们持续集成系统没有涵盖 Firefox 桌面上的 HTTP/3 和
viaduct
的特定组合。虽然我们无法测试所有可能的配置和组件组合,但 HTTP 版本的选择是一个相当大的更改,应该进行测试,以及使用像viaduct
这样的额外网络层。当前的 HTTP/3 测试涵盖了低级协议行为以及 Web 内容使用的 Necko 层。我们应该使用不同的 HTTP 版本运行更多系统测试,这样做可以揭示此问题。
我们还正在调查行动要点,既要使浏览器对这类问题更具弹性,也要使事件响应更快。尽可能多地从这次事件中学习将有助于我们提高产品质量。我们感谢所有发送崩溃报告、在 Bugzilla 中与我们合作或帮助其他人解决问题的用户。
关于 Christian Holler
Christian 是 Mozilla 的 Firefox 技术主管和首席工程师。
5 评论