此内容已使用自动机器翻译服务进行翻译,仅供您参考及阅读便利。其中可能包含错误、遗漏,或与原始英文版本存在理解方面的细微差别。如有疑问,请参考原始英文版本。
Cloudflare 的重点关注领域之一是为最终用户提供更快的互联网速度。其中一个方法是查看可能减慢速度的“大石块”或瓶颈,特别是关键路径上的流程。最近,当我们将注意力转向我们的隐私代理产品时,我们发现了一个巨大的改进机会。
我们的隐私代理产品是什么?这些代理让用户可以浏览 Web,但不会将个人信息暴露给他们所访问的网站。Cloudflare 运行针对隐私代理的基础设施,例如 Apple 的 Private Relay 和 Microsoft 的 Edge Secure Network 。
与任何安全的基础设施一样,在我们打开与用户正在访问的网站的连接之前,请确保通过这些隐私代理的身份验证。为了以保护隐私的方式做到这一点(以便 Cloudflare 收集尽可能少的最终用户信息),我们使用开放的互联网标准 Privacy Pass 来颁发对我们的代理服务进行身份验证的令牌。
每次用户通过我们的 Privacy Proxy 访问网站时,我们都会检查其请求的 Proxy-Authorization 标头中包含的 Privacy Pass 令牌的有效性。在对用户的令牌进行加密验证之前,我们会检查该令牌是否已经被使用。如果令牌未使用,我们让用户请求通过。否则,就会造成“双花”。从访问控制的角度来看,双花就是问题所在。从隐私角度来看,双花会降低匿名集和隐私特征。从性能角度来看,我们的隐私代理每秒处理数百万个请求——任何花费时间进行身份验证都会延迟人们访问站点——因此检查需要快速。让我们看看我们如何将这些双花检查的延迟从大约 40 毫秒减少到 1 毫秒以下。
我们使用一个跟踪平台Jaeger。它让我们能够了解代码执行了哪些路径,以及函数运行了多长时间。查看这些踪迹后,我们发现延迟约为 40 毫秒。这是一个很好的线索,但仅凭这一点并不足以断定这是一个问题。原因是我们只对一小部分踪迹进行了采样,所以我们看到的不是全貌。我们需要查看更多数据。我们本可以增加采样的踪迹数量,但踪迹数量庞大且繁重,对于我们的系统处理而言意义重大。Metrics 是一个更轻量的解决方案。我们添加了指标以获取所有双花检查的数据。
图中的线条是我们在全球最慢隐私代理上观察到的延迟中位数。指标数据让我们相信,这是一个影响大部分请求的问题……假设 ~ 45 ms 比预期的时间长。但这是否在意料之中呢?我们的预期数字是多少?
要了解合适的等待时间,我们来详细了解一下“双花支票”的构成要素。在进行双花检查时,我们会询问后备数据存储是否存在 Privacy Pass 令牌。我们使用的数据存储是 内存缓存
。我们有许多 Memcached
实例,在世界各地的服务器上运行,那么我们会问,是哪一个服务器呢?为此,我们使用 mcrouter
。我们无需考虑要询问哪个 Memcached
服务器,而是将请求发送给 mcrouter
,而后者将选择合适的 memcached
服务器。我们查看了 mcrouter
处理我们请求的中位数时间。此图显示每台服务器的平均延迟随时间的变化。会有一些峰值,但大多数时候延迟 < 1 毫秒。
此时,我们确信所有地方的双花检查延迟都比预期的要长,于是我们开始寻找根本原因。
我们从科学方法中获得了灵感。我们分析了我们的代码,创造了关于代码段导致延迟原因的理论,并用数据来反证这些理论。对于理论中遗留的问题,我们实施了修复措施,并测试其是否有效。
我们来看看代码。总体而言,双花检查的逻辑是:
获得一个连接,这个过程可以分解为:
发送 memcached version
命令。这是一次运行状况检查,检查连接是否仍适合发送数据。
如果连接仍然良好,则断开连接。否则,建立一个新的连接。
在该连接上发送 memcached get
命令。
我们来回顾一下上述每一步的理论。
我们主要将运行状况检查作为健全性检查来衡量。version 命令简单且处理快速,因此应该不会花费很长时间。但我们保持着理智。中值延迟 < 1 ms。
要了解为什么我们可能需要等待才能建立连接,让我们更详细地了解如何获得连接。在我们的代码中,我们使用连接池。池 是到 mcrouter
的一组现成的连接。拥有连接池的好处是,我们不必在每次发出请求时都建立连接。但池是有大小限制的。我们的限制是每台服务器 20 个,这是一个潜在问题所在。假设我们有一台服务器每秒处理 5000 个请求,并且请求停留 45 毫秒。我们可以使用利特定律来估计我们系统中的平均请求数量: 5000 x 0.045 = 225
。由于我们的池大小限制,我们一次只能有 20 个连接,因此我们在任何时间点都只能处理 20 个请求。这意味着 205 请求正在等待!当我们进行双花检查时,也许我们需要等待大约 40 毫秒才能建立连接?
我们研究了许多不同服务器的指标。无论每秒请求数是多少,延迟都始终在 40 毫秒左右,证明了这一理论是正确的。例如,本图显示来自某个服务器每秒最多请求 20 个的数据。它显示了随时间变化的直方图,并且大部分请求落在 40 - 50 ms 的存储桶中。
我们决定与 Gemini 对话,提供我们目前为止的观察结果。它提出了很多建议,但最有趣的是检查 TCP_NODELAY
是否已设置。如果我们在代码中设置了这个选项,就会禁用一种叫做 Nagle 算法 的东西。Nagle 的算法本身没有问题,但是当与另一个功能(延迟的 ACK)一起启用时,延迟可能就会出现。为了解释其中的原因,让我们打一个比方。
假设我们运行一个群聊应用。通常情况下,人们会输入完整的想法,并在一条消息中发送。但是,我们有一个朋友,他每次只发送一个字:“嗨”。发送。”如何“ 。发送。"are"。发送。“你”。发送。通知数量很多。Nagle 算法旨在防止这种情况发生。Nagle 说,如果朋友想发送一条短信,那也可以,但每个轮只允许他们发送一次。当他们此后尝试发送更多的单个单词时,Nagle 会将这些单词保存在草稿消息中。一旦草稿消息达到一定的长度,Nagle 就会发送出去。但是,如果草稿邮件永远没有达到那个长度怎么办?为了应对此问题,每当朋友发送消息时,Delayed ACKs 就会启动一个 40 ms 的定时器。如果应用在定时器结束前没有得到进一步的输入,消息就会被发送到群组中。
我仔细查看了代码,包括 Cloudflare 编写的代码和我们依赖的依赖中的代码。我们依赖 memcache-async
crate 来实施让我们发送 内存缓存
命令的代码。发送 memcached version
命令的代码如下:
self.io.write_all(b"version\r\n").await?;
self.io.flush().await?;
没有任何异常。然后,我们查看 get 函数的内部。
let writer = self.io.get_mut();
writer.write_all(b"get ").await?;
writer.write_all(key.as_ref()).await?;
writer.write_all(b"\r\n").await?;
writer.flush().await?;
在我们的代码中,我们将 io
设置为 TcpStream
,这意味着每个 write_all
调用都会发送一条消息。启用 Nagle 算法后,数据流如下:
哎呀。我们试图发送所有三个小消息,但是在我们发送“get”之后,内核将令牌和\r\n
放入缓冲区并开始等待。当 mcrouter
收到 “get “时,它不能做任何事情,因为它没有完整的命令。所以,它等待了 40 毫秒。然后发送 ACK 作为响应。我们收到了 ACK,并发送了缓冲区中命令的其余部分。mcrouter
获取命令的其余部分,对其进行处理,然后返回一个响应,告诉我们该令牌是否存在。禁用 Nagle 算法的情况下,数据流会怎么样?
这三个小信息我们都会发送。 mcrouter
将获取完整的命令,并立即返回响应无需等待。
我们的 Linux 服务器对延迟有一个下限。下面的一段 Linux 源代码定义了这些边界。
#if HZ >= 100
#define TCP_DELACK_MIN ((unsigned)(HZ/25)) /* minimal time to delay before sending an ACK */
#define TCP_ATO_MIN ((unsigned)(HZ/25))
#else
#define TCP_DELACK_MIN 4U
#define TCP_ATO_MIN 4U
#endif
注释告诉我们,TCP_DELACK_MIN
是延迟 ACK 在发送 ACK 之前将等待的最短时间。我们花了一些时间来深入研究 Cloudflare 的自定义内核设置,发现了这一点:
CONFIG_HZ=1000
CONFIG_HZ
最终传播到 HZ
并导致 40 ms 的延迟。这就是数字的来源!
为一个命令发送三个单独的消息,而我们需要发送一条消息。我们捕获了 Wireshark 中 get
命令的样子,以验证我们发送的是三个单独的消息。(我们在 MacOS 上本地捕获了这一点。有趣的是,每条消息我们都收到了 ACK。)
修复方法是使用 BufWriter<TcpStream>
,以便 write_all
会将小消息缓冲在用户空间内存缓冲区中,而 flush
将在一条消息中发送整个 memcached
命令。Wireshark 的捕获看起来干净得多。
在将修复部署到生产环境后,我们发现所有地方的双花检查延迟中位数均降至预期值。
我们的调查采用了系统的、数据驱动的方法。我们首先使用可观察性工具来确认问题的规模。然后,我们提出可检验的假设,并使用数据系统地反证这些假设。这个过程最终导致我们发现了 Nagle 算法和延迟的 ACK 之间的微妙相互作用,这种交互作用是由我们使用第三方依赖关系造成的。
我们的最终使命是帮助建设更加美好的互联网。节省的每一毫秒都有助于为最终用户提供更快、更无缝、私密的浏览体验。我们很高兴推出这一功能,并很高兴继续追求进一步的性能改进!