给力星

Web Developer

TCP的构成与调优_《Web性能权威指南》读书笔记

《Web性能权威指南(High Performance Browser Networking)》读书笔记,第 1 章和第 2 章。TCP 的一些机制是很有必要掌握清楚的,笔记中记录得会详细一些。

本书官方提供了免费在线阅读 http://chimera.labs.oreilly.com/books/1230000000545/index.html

第1章 延迟与带宽

  • 延迟: 分组从信息源发送到目的地所需的时间
    • 延迟的构成:传播(发送到接受)延迟、传输(信息转移到链路中)延迟、处理延迟、排队延迟。
  • 带宽:逻辑或物理童心路径最大的吞吐量
  • RTT:往返时间。

本地路由器的缓冲区爆满:排队延迟会影响整体性能。路由器通常会配备比较大的入站缓冲区,以避免丢包。但这种做法破坏了 TCP 的拥塞预防机制。因为不删包,就无法触发拥塞预防机制,导致进入排队处理的消息越来越多,从而爆满。

大多数网站性能的瓶颈是延迟,而不是带宽

第2章 TCP的构成

  • IP: 负责联网主机之间的路由选择和寻址。能保证发送的所有字节完整的被接受,并且到达客户端的顺序一致。
  • TCP:负责在不可靠的传输信道上提供可靠的抽象层。

三次握手

  1. 发送端随机生成一个序列号 x ,并发送一个 SYN 分组。
  2. 接收端给 x 加1,并选择自己的一个随机序列号,返回给相应。
  3. 客户端给 x 和 y 都加1,并发送握手期间最后一个ACK分组。此时**客户端可以选择在最后一个ACK分组之后立即发送数据。

TCP的三次握手示意图TCP的三次握手示意图

三次握手完成后,客户端和接收端之间就可以通信了。在每次传输应用数据之前,都必须经历一次完整的往返。因此,**提高 TCP 应用性能的关键,在于想办法重用连接。

TCP快速打开

连接并不是想重用就可以重用。TFO(TCP Fast Open) 是可以减少新建TCP连接带来的性能损失。Linux 3.7 及以后的内核已支持在客户端和服务端支持 TFO,但 TFO 并不能解决所有问题,它虽然有助于减少三次握手的往返时间,但只能在某些情况下有效,比如只能发送某些类型的HTTP请求、只能用于重复的连接等。

拥塞预防机制

流量控制

预防发送端过多的向接收端发送数据的机制。为实现流量控制,TCP 的两端都要通告自己的接收窗口(rwnd)。如果一端跟不上数据传输,就会向发送端通告一个较小的窗口,因此接收的一端更可能成为性能瓶颈。

通告接收窗口大小通告接收窗口大小

窗口缩放

最初的 TCP 规范分给通告窗口大小的字段是16位,相当于设定了发送端和接收端窗口的最大值为 65535 字节,在这个限制内通常无法获得最佳性能。为解决这个问题,RFC 1323 提供了 “TCP窗口缩放”(TCP Window Scaling)选项,可以将窗口大小提升到 1G 字节,这在三次握手期间完成,当中有一个值表示在将来的 ACK 中左移16位窗口字段的位数。

TCP 窗口缩放机制在主要平台上都是默认弃用的,但中间节点和路由器可以重写或去掉这个选项。因此如果服务器或客户端连接不能完全利用现有的带宽,往往应该先查一查窗口大小。Linux 可通过下述命令查询:

$> sysctl net.ipv4.tcp_window_scaling
$> sysctl -w net.ipv4.tcp_window_scaling=1

慢启动

流量控制可以防止发送端向接收端发送过多的数据,但在连接建立之初,无法知道可用宽带是多少,因此需要一个估算机制,且需要根据网络中不断变化的条件而动态改变速度。解决该问题的几种算法:慢启动、拥塞预防、快速重发和快速恢复。

慢启动的设计思路是根据交换数据来估算客户端与服务器之间的可用带宽。TCP 连接时初始化一个拥塞窗口(cwnd)变量,表示发送端从客户端接收确认(ACK)之前可以发送的数据量的限制。同时发送端不会通告 cwnd 变量,发送端只是维护这么一个私有变量。

此时,客户端与服务端之间最大可以传输(未经ACK确认的)数据量取 rwnd 和 cwnd 中的最小值。在分组被确认之后,增大窗口大小(每收到一个 ACK,慢启动算法就告诉服务器可以将它的 cwnd 增加一个 TCP 段),慢慢的启动。因为每接收一个 ACK 就增加一个 TCP 段,所以是指数增长。

TCP慢启动算法示意图TCP慢启动算法示意图

一个 TCP 端的大小是 1460字节

MTU 大小为1500,其中 ip 头为 20,tcp 头 20,所以 maximum segment size 为 1460 字节。1 字节为 8 位二进制。

无论宽带多大,每个 TCP 连接都必须经过慢启动阶段,因此不可能一上来就利用连接的最大带宽。例如接收窗口最大为 65535≈64kb≈45段TCP,则要达到这个窗口经过 4 次往返,这可能会带来上百毫秒的延迟。

拥塞窗口大小增长示意图,经4次往返才能达到最大接收窗口拥塞窗口大小增长示意图,经4次往返才能达到最大接收窗口

慢启动限制了可用了的吞吐量,对于许多 HTTP 连接,特别是一些短暂、突发的连接,例如对小文件的传输来说非常不利

慢启动重启

TCP 还实现了 SSR(Slow-Start Restart 慢启动重启)机制,这种机制会在连接空闲一段时间后重置连接的拥塞窗口。因为在连接空闲的同时,网络状况也可能发生了变化,为避免拥塞,理应将拥塞窗口重置回“安全的”默认值。SSR 对于会出现突发空闲的长周期 TCP 连接(如 HTTP 的 keep-alive 连接)有很大的影响,因此建议在服务器上禁用SSR,Linux平台检查和关闭的命令为:

$> sysctl net.ipv4.tcp_slow_start_after_idle
$> sysctl -w net.ipv4.tcp_slow_start_after_idle=0

为减少增长到拥塞窗口的时间,可以将初始拥塞窗口大小增加至 RFC 9828 规定的10段。以一个 20kb 文件的传输为例:

通过一个新的 TCP 连接请求 20kb 的文件通过一个新的 TCP 连接请求 20kb 的文件

在 RTT 为 56ms 的情况下,这个过程需要 264ms。而如果将初始 cwnd 值设置为 10 个 TCP 段,则图中过程可减少一次 RTT,性能可提升 22%。

假设客户端可以重用同一个 TCP 连接,再发送一次同样的请求:

重用 TCP 连接请求 20kb 的文件重用 TCP 连接请求 20kb 的文件

可以看到,若没有经过三次握手和慢启动,只需要 96ms,性能提升 275%!而在这两种情况下,服务器和客户端的高带宽并不影响 TCP 连接的启动。此时,延迟和拥塞窗口大小才是限制因素

增大 TCP 的初始拥塞窗口大小

把服务器的初始 cwnd 值增大到 RFC 6928 新规定的 10 段(IW10),是提升用户体验以及所有 TCP 应用性能的最简单方式,且很多操作系统已经更新了内核,采用了增大后的值。如 Linux,IW10 是 2.6.39 以上版本内核的新默认值。

拥塞预防

TCP 调节性能主要依赖丢包反馈机制。拥塞预防算法把丢包作为网络拥塞的标志,即路径中某个连接或路由器已经拥塞了,必须采取删包措施,因此必须调整窗口大小,以避免造成更多的包丢失,从而保证网络畅通。

重置拥塞窗口后,拥塞预防机制按照自己的算法(与慢启动不同)来增大窗口以尽量避免丢包。但拥塞控制和预防对于网络性能的影响是存在的。

TCP 比例降速: 确定丢包回复的最优方式并不容易,最初,TCP 使用 AIMD(Multiplicative Decrease and Additive Increase,倍减加增)算法,即发生丢包时,先将拥塞窗口减半,然后每次往返再缓慢地给窗口增加一个固定的值。不过很多时候 AIMD 算法太过保守。

PRR(Proportional Rate Reduction,比例降速)是 RFC 6937 规定的一个新算法,目的就是改进丢包后的恢复速度,据谷歌的测量,实现新算法后,因丢包造成的平均连接延迟减少了 3%-10%。PRR 现在是 Linux 3.2+ 内核默认的拥塞预防算法。

宽带延迟积

发送端和接收端之间在途末确认的最大数据量,取决于拥塞窗口和接收窗口的最小值,无论发送段发送的数据还是接收端接收的数据超过了未确认的最大数据量,都必须停下来等待另一方 ACK 确认某些分组才能继续,等待的时间取决于往返时间。

宽带延迟积(BDP,Bandwidth-delay count): 数据链路的容量与其端到端延迟的乘积,这个结果就是任意时刻处于在途末确认状态的最大数据量。

发送端或接收端无论是谁被迫频繁地停止等待之前发送分组的 ACK,都会造成数据缺口,如下图所示。为解决这个问题,应让窗口足够大,以保证任一端都能在 ACK 返回前持续发送数据。只有传输不中断,才能保证最大吞吐量。

拥塞窗口过小导致数据缺口拥塞窗口过小导致数据缺口

rwnd 和 cwnd 的值多少才合适?假设这两者的最大值为 16kb,往返时间为 100ms,则能达到的最大速率只有 1.31 Mbit/s:

rwnd和cwnd确定的情况下能达到的最大传输速率rwnd和cwnd确定的情况下能达到的最大传输速率

而假设有 10 Mbit/s 带宽可以用,往返时间还是 100ms,则窗口至少需要 122.1 KB:

要充分利用带宽所需的最小窗口值要充分利用带宽所需的最小窗口值

窗口大小的协商与调节由王禄普栈自动控制、调整,但窗口大小有时仍然会是 TCP 性能的限制因素。如果怎么也想不通为什么服务器与客户端之间实际传输速度只有可用带宽的几分之一,那窗口大小很有可能是罪魁祸首。

高速局域网中的带宽延迟积: 往返时间不仅高速传输延迟中是一个常见的瓶颈,在 LAN 中也可能是一个瓶颈。如想在 1ms 的往返时间内达到 1Gbit/s 的传输速度,拥塞窗口也至少需要 122KB。

队首阻塞

TCP 在不可靠的信道上实现了可靠的网络传输,虽然 TCP 很流行,但其并不是唯一的选择,在某些情况下也不是最佳选择,特别是按序交付和可靠交付有时候并不必要,反而会导致额外的延迟,对性能造成负面影响。

为了让分组按照顺序传送到接收端,如果中途有一个分组没能到达接收端,那么后续分组必须保存在接收端的 TCP 缓冲区,等待丢失的分组重发并到达接收端,这一切都发生在 TCP 层,应用程序对 TCP 的重发和分组缓存排队一无所知,需等到分组全部到达才能访问数据,在次之前应用程序只能在套接字读数据时感到延迟交付。这种效应称为 TCP 的队首(HOL,Head of Line)阻塞。

应用程序不必关系分组重排和重组,但分组到达时间会存在无法预知的延迟变化,这种边变化通常被称为抖动

另外有些应用程序可能并不需要可靠的交付或不需要按序交付。如每个分组都是独立的消息,则按序交付就没必要如每个消息都会覆盖之前的消息,则可靠交付同样也没必要了,例子如更新 3D 游戏中角色的状态,收到 T 时刻的包而等待 T-1 时刻的包通常毫无必要。这些情况下可以选择 UDP 等协议。

丢包就丢包

事实上丢包是让 TCP 达到最佳性能的关键,被删除的包正是一种反馈机制,能够让接收端和发送端各自调整速度,以避免网络拥堵,同时保持延迟最短。

针对 TCP 的优化建议

优化 TCP 的最佳途径就是调整它感知当前网络状况的方式,根据它之上或之下的抽象层的类型和需求来改变它的行为。

不同应用程序需求间的复杂关系,以及每个 TCP 算法中的大量因素,使得 TCP 调优成为学术和商业研究的一个“无底洞”。尽管如此,即使具体细节会持续发展,但核心原理和他们的影响是不变的:

  • TCP 三次握手增加了整整一次往返时间;
  • TCP 慢启动将被应用到每个新连接;
  • TCP 流量及拥塞控制会影响所有连接的吞吐量;
  • TCP 的吞吐量由当前拥塞窗口大小控制。

服务器配置调优

TCP 的最佳实践以及影响其性能的底层算法一直在与时俱进,而且大多数变化都只在最新内核中才实现。有了最新内核,推荐遵循如下最佳实践来配置自己的服务器:

  • 增大 TCP 的初始拥塞窗口:对于突发性的短暂连接,这是特别关键的一个优化;
  • 慢启动重启:在连接空闲时禁用慢启动可以改善瞬时发送数据的长 TCP 连接的性能;
  • 窗口缩放:启用窗口缩放可以增大最大接收窗口大小;
  • TCP 快速打开:允许第一个 SYN 分组中发送应用程序数据,这需要客户端和服务器共同支持。

调优更重要的是要分清楚轻重缓急,着力解决真正的瓶颈,而不是眉毛胡子一把抓

应用程序行为调优

  • 能少发就少发,如减少下载不必要的资源或压缩数据;
  • 让传输的距离更短,如采用CDN;
  • 重用 TCP 连接是提升性能的关键,把慢启动和其他拥塞机制的影响降到最低。