这是 Napkin Math 时事通讯的一个版本,是关于使用餐巾式数学和第一性原理思维来估计系统性能的时事通讯。您可以通过电子邮件订阅。您是否知道,如果您的网站低于 12kb,第一页的加载速度会明显加快?当 TCP 正在预热(称为 TCP 慢启动)时,服务器在初始往返中仅发送几个数据包(通常为 10 个)。发送完第一组数据包后,需要等待客户端确认收到所有这些数据包。使用 10 与 30 的初始 TCP 慢启动窗口(也称为初始拥塞窗口或 initcwnd)传输约 15kb 的快速说明:初始窗口越大,我们可以在第一次往返中传输的越多,您的站点越快在初始页面加载时。对于较长的往返时间(例如穿越海洋),这将开始变得很重要。以下是一些常见托管服务提供商的初始窗口的大致大小: 为了生成这个,我编写了一个脚本,您可以使用 sirupsen/initcwnd 来分析您自己的站点。根据报告,您可以尝试调整页面大小,或调整服务器的初始慢启动窗口大小 (initcwnd)(参见文章底部)。请继续阅读,我们将详细介绍!亲爱的餐巾马瑟斯,太久了。自上次以来,我在令人惊叹的 8 年之后离开了 Shopify。一生的骑行。就目前而言,我正在通过耐力冲浪(在我离开后的一周进行 125K 为期 3 天的旅行)、休闲编程(其中餐巾纸数学肯定是其中的一部分)和学习一些非计算机的东西。在本期中,我们将深入了解当我们通过 HTTP 对网站进行初始页面加载时,在线路上究竟发生了什么。正如我已经暗示的那样,我们将展示在优化短期、突发 TCP 传输时需要注意的一个神奇的字节阈值。如果你低于这个阈值,或者增加它,它可能会使客户端免于多次往返。特别是对于经常从遥远的地方(即高往返时间)请求的具有单一位置的站点,例如美国 - > 澳大利亚,这可以产生巨大的差异。如果您正在运营 SaaS 风格的服务,这很可能就是您所处的情况。虽然我们将重点关注公共互联网上的 HTTP,但 TCP 慢启动对数据中心内的 RPC 也很重要,尤其是在它们之间。
与往常一样,我们将首先展示关于我们在第 4 层思考加载网站如何工作的朴素心理模型。然后我们将对预期性能进行餐巾纸数学计算,并将我们脆弱、朴素的模型与现实面对面,看看它是否符合要求.那么当我们请求一个站点时,我们认为在 TCP 级别会发生什么?为简单起见,我们将排除压缩、DOM 渲染、Javascript 等,并仅限于下载 HTML。换句话说: curl --http1.1 https://sirupsen.com > /dev/null (注意 sirupsen/initcwnduses -- 用 curl 压缩以反映现实)。为了让事情变得更有趣,我们将选择一个在地理上离我更远但没有过度优化的站点:information.dk,一家丹麦报纸。通过从不同地理区域的服务器进行的一些 DNS 查找并使用观察镜,我可以确定它们的所有 HTML 流量始终路由到哥本哈根的数据中心。如今,许多站点都通过 Cloudflare POP 路由,这些站点附近有一个数据中心,为了简化我们的分析,我们希望确保情况并非如此。我目前坐在魁北克西南部,使用 LTE 连接。我可以通过 traceroute(1) 确定我的流量正在通过路径蒙特利尔 -> 纽约 -> 阿姆斯特丹 -> 哥本哈根前往哥本哈根。往返时间约为 140 毫秒。如果我们将上述餐巾纸模型的往返次数相加(不包括 DNS),我们预计加载丹麦站点需要 4 * 140 毫秒 = 560 毫秒。因为我使用的是 LTE 连接,但我没有得到远高于 15 mbit/s,我们必须考虑到除了 4 次往返之外,还需要大约 100 毫秒来传输数据。因此,根据我们的餐巾纸数学,我们期望我们应该能够在大约 660 毫秒的时间内从哥本哈根的服务器下载 160kb 的 HTML。然而,现实还有其他计划。当我运行时 curl --http1.1 https://www.information.dk 需要 1.3 秒!通常我们会说,如果餐巾数学在 ~10 倍以内,餐巾数学可能与现实相符,但这通常是我们处理纳秒和微秒的情况。未关闭~ 640ms!那么这里发生了什么?当餐巾纸数学和现实之间存在差异时,这是因为(1)世界的餐巾纸模型不正确,或者(2)系统中有优化空间。在这种情况下,它有点两者兼而有之。让我们追捕那 640 毫秒。 👀
为此,我们必须使用 Wireshark 分析原始网络流量。 Wireshark 带回了许多回忆……有些喜欢,但主要是……在试图找出间歇性网络问题的原因时感到沮丧。在这种情况下,这一次是为了好玩和游戏!我们将在 Wireshark 中输入 host www.information.dk 以使其捕获到站点的流量。在我们的终端中,我们运行上面的 curl 命令让 Wireshark 有一些东西要捕获。 Wireshark 然后会给我们一个很好的 GUI 来帮助我们追捕我们没有考虑的大约半秒。需要注意的一件事是,为了让 Wireshark 了解会话的 TLS/SSL 内容,它需要知道与服务器协商的秘密。这里有一个完整的指南,但简而言之,您将 SSLKEYLOGFILE=log.log 传递给 curl 命令,然后在 TLS 配置中的 Wireshark 中指向该文件。我们看到了预期的 TCP 往返,来自客户端的 SYN,然后来自服务器的 SYN+ACK。布埃诺。但在那之后它看起来很可疑。我们看到了 3 次 TLS/SSL 往返,而不是上面图中预期的 2 次!为了确保我没有误会,我再次检查了 sirupsen.com,果然,它显示 Wireshark 中的两次往返都符合预期:如果我们仔细研究上面为丹麦报纸带注释的 Wireshark 转储,我们可以看到问题是,无论出于何种原因,服务器在传输证书(数据包 9)的过程中都在等待 TCP 确认。为什么服务器在传输约 4398 字节的证书后等待来自客户端的 TCP ACK?为什么服务器不一次发送整个证书?在 TCP 中,服务器会仔细监控它在传输中的数据包/字节数。通常,每个数据包是大约 1460 字节的应用程序数据。服务器不必一次发送它拥有的所有数据,因为服务器不知道管道对客户端的“胖”程度。如果客户端只能接收 64 kbit/scurrent,那么发送例如 100 个数据包可能会完全阻塞网络。网络很可能会丢弃一些随机数据包,与以更可持续的速度为客户端发送数据包相比,这些数据包的补偿速度甚至更慢。
TCP 协议的一个主要部分是在任何给定时间尝试发送尽可能多的数据的平衡行为,同时确保服务器不会使到客户端的路径过度饱和并丢失数据包。丢包对 TCP 中的带宽非常不利。服务器在任何给定时间只保留一定数量的数据包。 TCP 术语中的“传输中”意味着“未确认”的数据包,即服务器已发送给客户端但客户端尚未向客户端发送确认的数据包它收到的服务器。通常对于每个成功确认的数据包,服务器的 TCP 实现将决定将允许的传输中数据包的数量增加 1。您可能听说过这种简单的算法,称为“TCP 慢启动”。另一方面,如果数据包已被丢弃,则服务器将决定传输中的字节数略少。在 TCP 连接的整个生命周期中,这种舞蹈将不知疲倦地进行。在 TCP 术语中,我们所说的“飞行中”被称为“拥塞窗口”(或简称 cwnd)。通常在第一个数据包丢失后,TCP 实现从简单的 TCP 慢启动算法切换到更复杂的“拥塞控制算法”,其中有几十个。他们的工作是:根据我们对网络的观察,我们应该有多少流量才能最大化带宽?现在我们可以回过头来理解为什么 TLS 握手需要 3 次往返而不是 2 次。在客户端开始与 TLS HELLO 的 TLS 握手后,丹麦服务器真的、真的想要传输这个 ~6908 字节的证书。不幸的是,尽管服务器的拥塞窗口(允许飞行中的数据包)当时只是不够大,无法容纳整个证书!换句话说,服务器的 TCP 实现已经决定它不相信可怜的客户端可以一次接收那么多美味的字节——所以它发送了一个 4398 字节的证书。当然,63% 的证书不足以继续进行 TLS 握手……所以客户端叹了口气,将 TCP ACK 发送回服务器,然后服务器发送证书剩余的 2510,以便客户端可以继续执行其TLShandshake 的一部分。当然,这一切看起来有点傻……首先,为什么证书是6908字节?!为了比较,我的网站是 2635。虽然这对我来说不是太有趣。更有趣的是为什么服务器只发送6908个字节?对于现代 Web 服务器来说,这似乎很少!在 TCP 中,在我们对客户端一无所知之前,我们可以在全新连接上发送多少数据包称为“初始拥塞窗口”。在配置上下文中,这称为 initcwnd。如果您在飞行中的数据包中引用了上面的黄色图表,那么这就是第一次往返时的值。
现在,Linux 服务器的默认值是 10 个数据包,或 10 * 1460 = 14600 字节,其中 1460 大致是每个数据包的数据负载。那将符合丹麦报纸的怪物证书。很明显,这不是他们的 initcwd,从那时起服务器就不会耐心等待我的 ACK。通过一些挖掘,似乎在 Linux 3.0.0 之前 initcwnd 是 3,或 ~ 3 * 1460 = 4380 字节!大致对齐,所以丹麦报纸的 initcwnd 似乎是 3。我们不确定它是 Linux,但我们知道 initcwnd 是 3。由于飞行中的数据包呈指数增长,因此 initcwnd 的重要性我们可以在前几次宝贵的往返中发送很多数据:正如我们在介绍中看到的,在 CDN 中将值从默认值提高到例如 32 (~46kb) 是很常见的。这是有道理的,因为您可能正在传输许多兆字节的图像。等待 TCP 慢启动到这一点可能需要几次往返。另一个其他原因,这也是 HTTP2/HTTP3 朝着通过相同连接移动更多数据的方向发展的原因,因为它已经有一个“热”的 TCP 会话。 “温暖”意味着传输中的拥塞窗口/字节已经从最初被服务器慷慨地增加了。 TCP 慢启动窗口也是存在点 (POP) 有用的部分原因。如果您连接到距离您网站前 10 毫秒的 POP,请与 POP 协商 TLS,并且 POP 已经与 100 毫秒之外的后端服务器建立了热连接——这会显着提高性能,没有其他变化。从 4 * 100ms = 400ms 到 3 * 10ms + 100ms = 130ms。现在我们已经了解了为什么我们有 3 次 TLS 往返而不是预期的 2 次:初始拥塞窗口很小。拥塞窗口(服务器允许传输的字节数)同样适用于服务器发回给我们的 HTTP 有效负载。如果它不适合拥塞窗口,那么我们需要多次往返来接收所有 HTML。在 Wireshark 中,我们可以拉出一个 TCP 视图,它可以让我们了解完成请求需要多少次往返(sirupsen/initcwnd 尝试用一个非常简单的算法为你猜测):
我们看到 TCP 往返、3 次 TLS 往返,然后是 5-6 个 HTTP 往返以获取 ~160kb 页面!图中的每个小点都表示一个数据包,因此您会注意到拥塞窗口(允许传输的字节数)在每次往返时大致加倍。服务器为每次成功的往返增加窗口的大小。 “成功的往返”是指没有丢包的往返,在一些较新的算法中,是指不需要太多时间的往返。通常,服务器将继续为每次成功的往返将数据包数量加倍(每个约 1460 字节),直到发生不成功的往返(慢速或丢失数据包),或者传输中的字节将超过客户端的接收窗口。当 TCP 会话开始时,客户端将通告它允许传输的字节数。这通常比服务器愿意立即发送的要大得多。我们可以在来自客户端的初始 SYN 包中提取它,并看到它大约为 65kb:如果会话更长并且我们将其推向该窗口,客户端将发送一个 TCP 包更新接收的大小窗户。所以有两个窗口在起作用:服务器管理传输中的数据包数量:拥塞窗口。拥塞窗口由服务器的拥塞算法控制,该算法根据成功的往返次数进行调整,但始终以客户端的接收窗口为上限。我们来看看服务器每次往返传输的数据包数量: HTTP 往返 5: 48 (~69kb,这在理论上会超过 64kb 当前接收窗口,因为客户端出于某种原因没有放大它。服务器只传输~ 64kb) 拥塞窗口的增长是教科书的三次函数,它有一个完美的拟合:
我不完全确定为什么它遵循三次函数。我预计 TCP 慢启动每次往返都会加倍。 🤷 据我所知,在现代 TCP 实现中,拥塞窗口每次往返都会加倍,直到数据包丢失(就像我分析过的大多数其他站点一样,例如下面屏幕截图中的会话)。在那之后,我们可能会转向三次增长。这可能后来改变了?这完全取决于 TCP 实现。这就是为什么我写了 sirupsen/initcwnd 来吐出 windows 的大小的部分原因,所以你不必做任何数学或猜测,这里有一个 Github 存储库(未压缩):所以现在我们可以解释我们的简单化之间的差异餐巾纸数学模型和现实。我们假设有 2 次 TLS 往返,但实际上有 3 次,因为服务器的初始拥塞窗口较低。我们还假设了 1 个 HTTProundtrip,但实际上有 6 个,因为服务器的拥塞窗口和客户端的接收窗口不允许一次发送所有内容。这使我们的总往返次数达到 1 + 3 + 6 = 10 次往返。我们的往返时间为 130 毫秒,这与我们在帖子顶部观察到的 1.3 秒的总时间完全一致!这表明我们新的、更新的系统心智模型很好地反映了现实。现在我们已经一起分析了这个网站,您可以使用它来分析您自己的网站并对其进行优化。您可以通过对您的网站运行 sirupsen/initcwnd 来完成此操作。它使用一些非常简单的启发式方法来猜测窗口及其大小。它们并不总是有效,特别是如果您的连接速度较慢或网站将响应流式传输回客户端,而不是一次发送所有响应。需要注意的另一件事是 Linux 内核(可能还有其他内核)通过路由缓存与客户端缓存拥塞窗口大小(除其他外)。这很棒,因为这意味着当客户端重新连接时,我们不必从头开始重新协商。但这可能意味着针对同一个网站的后续运行会给你一个更大的 initcwnd。最低的youencounter将是正确的。还要注意,一个站点可能有一个队列,其中的服务器具有不同的 initcwnd 值!在这里我们可以看到 TCP 窗口的大小。 Github.com 的初始窗口是 10 个数据包,然后每次往返增加一倍。最后一个窗口不是一个完整的 80 个数据包,因为服务器没有足够的字节数。有了这个结果,我们可以决定将 initcwnd 更改为更高的值,以尝试在更少的往返中将其发回。但是,这对于连接速度较慢的客户端可能有缺点,应谨慎操作。 CDN 在 30 年代就具有价值,这确实表明了一些承诺。不幸的是,我没有足够的流量来亲自查看来研究这一点,就像谷歌支持将默认值从 3 更改为 10 时所做的那样。该文档还更详细地解释了潜在的缺点。
最实用的日常外卖可能是例如 base64 内联图像和 CSS,如果它使您的站点超过拥塞窗口阈值,则它可能会带来严重的缺点。您可以在 Linux 上使用 ip(1) 命令更改 initcwnd,从这里到默认的 10 到 32: simon@netherlands:~$ ip route showdefault via 10.164.0.1 dev ens4 proto dhcp src 10.164.0.2 metric 10010.164.0. dhcp scope link src 10.164.0.2 metric 100simon@netherlands:~$ sudo ip route change default via 10.164.0.1 dev ens4 proto dhcp src 10.164.0.2 metric 100 initcwnd 32 initrwnd 32 initrwnd 32 initrwnd.1032 initrwnd@10332 [email protected] dev ens4 proto dhcp src ens4 proto dhcp src 10.164.0.2 metric 100 initcwnd 32 initrwnd 3210.164.0.1 dev ens4 proto dhcp scope link src 10.164.0.2 metric 100 另一个重要的 TCP 设置,它值得在 .tcpslow 之后开始调整 TCP _ cpslow 是 .tcpslow 的开始这是一个好名字:默认情况下,当设置为 1 时,它会在几秒钟没有活动(当您在网站上阅读时)后重新协商拥塞窗口。您可能希望在 /proc/sys/net/ipv4/tcp_slow_start_after_idle 中将其设置为 0,以便它记住下一个页面加载的拥塞窗口。