TCP 的构建模块
网络基础 101,第二章
互联网的核心由两个协议构成:IP 与 TCP。IP(Internet Protocol,互联网协议)提供主机到主机的路由与寻址功能;TCP(Transmission Control Protocol,传输控制协议)则在不可靠的通道之上,抽象出一个可靠的网络。TCP/IP 也常被称为”互联网协议套件”,最早由 Vint Cerf 和 Bob Kahn 在 1974 年的论文《A Protocol for Packet Network Intercommunication》中提出。
最初的提案(RFC 675)经过多次修订,1981 年,TCP/IP 的 v4 规范以两份独立的 RFC 文档发布:
- RFC 791 — 互联网协议(Internet Protocol)
- RFC 793 — 传输控制协议(Transmission Control Protocol)
此后,TCP 经历了诸多增强提案与实现,但其核心机制并未发生根本性改变。TCP 迅速取代了此前的协议,如今已成为众多主流应用的首选协议:万维网、电子邮件、文件传输,以及其他无数应用。
TCP 成功地将可靠的网络抽象构建于不可靠的通道之上,向应用程序隐藏了网络通信的大部分复杂性:丢包重传、按序交付、拥塞控制与避免、数据完整性校验等。当你使用 TCP 流时,可以确信发送的每一个字节都与接收到的字节完全一致,且会以相同的顺序到达客户端。因此,TCP 优化的是准确交付,而非及时交付。事实证明,这也给浏览器端的 Web 性能优化带来了一些挑战。
HTTP 标准并未强制要求 TCP 作为唯一的传输协议。理论上,我们可以通过数据报套接字(User Datagram Protocol,UDP)或任何其他传输协议来承载 HTTP,但在实践中,当今互联网上的所有 HTTP 流量都通过 TCP 传输,这得益于 TCP 开箱即用的众多优秀特性。
正因如此,理解 TCP 的一些核心机制,对于构建优化的 Web 体验至关重要。你可能不会在应用中直接操作 TCP 套接字,但你在应用层的设计选择,将决定 TCP 的性能以及应用所依赖的底层网络表现。
TCP 与 IP 协议的交织历史
我们都熟悉 IPv4 和 IPv6,但 IPv{1,2,3,5} 去哪了?IPv4 中的”4”指的是 TCP/IP 协议的第 4 版,发布于 1981 年 9 月。最初的 TCP/IP 提案将两个协议耦合在一起,正是 v4 草案正式将它们拆分为独立的 RFC。因此,IPv4 中的”4”源于它与 TCP 的历史渊源:并不存在独立的 IPv1、IPv2 或 IPv3 协议。
1994 年,当工作组开始着手”下一代互联网协议”(IPng)时,需要一个新的版本号,但 v5 已被分配给另一个实验性协议:Internet Stream Protocol(ST)。事实证明,ST 并未得到广泛应用,因此鲜为人知。这就是 IPv6 中”6”的由来。
三次握手
所有 TCP 连接都以三次握手开始(图 2-1)。在客户端或服务器交换任何应用数据之前,它们必须就起始数据包序列号以及双方的其他连接特定变量达成一致。出于安全考虑,序列号由双方随机选取。
SYN
客户端选取一个随机序列号 x,发送 SYN 数据包,其中可能包含额外的 TCP 标志和选项。
SYN-ACK
服务器将 x 加 1,选取自己的随机序列号 y,附加自己的标志和选项集,然后发送响应。
ACK
客户端将 x 和 y 都加 1,通过发送握手中的最后一个 ACK 数据包完成握手。
图 2-1. 三次握手
三次握手完成后,应用数据即可在客户端与服务器之间流动。客户端可以在 ACK 数据包后立即发送数据,而服务器必须等待 ACK 后才能发送任何数据。这个启动过程适用于每一个 TCP 连接,并对所有使用 TCP 的网络应用性能产生重要影响:每个新连接都需要经历一个完整的往返时延(RTT),才能开始传输应用数据。
例如,如果客户端位于纽约,服务器位于伦敦,我们通过光纤链路建立新的 TCP 连接,那么三次握手至少需要 56 毫秒(表 1-1):数据包单向传播需要 28 毫秒,然后必须返回纽约。注意,连接带宽在这里不起作用。延迟由客户端与服务器之间的往返时延决定,而这主要取决于纽约与伦敦之间的传播时间。
三次握手带来的延迟使得创建新的 TCP 连接代价高昂,这也是连接复用成为所有基于 TCP 的应用的关键优化手段的主要原因之一。
TCP 快速打开(TFO)
加载一个网页通常需要从数十个不同主机获取数百个资源。这可能要求浏览器建立数十个新的 TCP 连接,每个连接都必须承担 TCP 握手的开销。毋庸置疑,这可能是 Web 浏览延迟的重要来源,尤其是在较慢的移动网络上。
TCP Fast Open(TFO)是一种旨在消除新连接延迟惩罚的机制,允许在 SYN 数据包中传输数据。然而,它也有自身的限制:SYN 数据包中的数据负载大小有限制,只能发送特定类型的 HTTP 请求,且由于需要加密 cookie,它仅适用于重复连接。关于 TFO 能力与限制的详细讨论,请参阅 IETF 草案《TCP Fast Open》的最新版本。
启用 TFO 需要客户端、服务器的显式支持,以及应用的主动选择。为获得最佳效果,服务器应使用 Linux 内核 4.1+,客户端需兼容(如 Linux、iOS 9+ / macOS 10.11+),并在应用中启用相应的套接字标志。
根据 Google 进行的流量分析和网络仿真研究,TFO 可将 HTTP 事务的网络延迟降低 15%,整页加载时间平均减少 10% 以上,在高延迟场景下甚至可达 40%!
拥塞避免与控制
1984 年初,John Nagle 记录了一种被称为”拥塞崩溃”(congestion collapse)的状况,这可能影响任何网络节点间带宽容量不对称的网络:
“拥塞控制是复杂网络中的一个公认问题。我们发现,国防部的互联网协议(IP,一种纯粹的数据报协议)与传输控制协议(TCP,一种传输层协议)一起使用时,会因传输层与数据报层之间的交互而产生异常的拥塞问题。特别是,IP 网关容易受到我们称之为’拥塞崩溃’的现象影响,尤其是当这些网关连接带宽差异很大的网络时……
如果往返时间超过任何主机的最大重传间隔,该主机将开始向网络中引入越来越多相同数据报的副本。网络现在陷入了严重困境。最终,交换节点的所有可用缓冲区都将填满,数据包必须被丢弃。成功交付的数据包的往返时间现在达到了最大值。主机正在多次发送每个数据包,最终每个数据包的某个副本会到达目的地。这就是拥塞崩溃。
这种状况是稳定的。一旦达到饱和点,如果数据包丢弃选择算法是公平的,网络将继续在降级状态下运行。”
— John Nagle, RFC 896
报告指出,拥塞崩溃尚未成为 ARPANET 的问题,因为大多数节点具有统一的带宽,且骨干网拥有充足的过剩容量。然而,这两个前提并未持续太久。1986 年,随着网络节点数量(5000+)和多样性的增长,一系列拥塞崩溃事件席卷整个网络——在某些情况下,容量下降了 1000 倍,网络变得无法使用。
为解决这些问题,TCP 中实现了多种机制来管理双向数据传输速率:流量控制、拥塞控制和拥塞避免。
高级研究计划局网络(ARPANET)是现代互联网的前身,也是世界上第一个运营的分组交换网络。该项目于 1969 年正式启动,1983 年 TCP/IP 协议取代了早期的 NCP(网络控制程序)成为主要通信协议。剩下的,正如人们所说,就是历史了。
流量控制
流量控制是一种防止发送方以接收方可能无法处理的数据量将其淹没的机制——接收方可能正忙、负载过重,或只愿分配固定数量的缓冲区空间。为解决此问题,TCP 连接的每一方都会通告(图 2-2)自己的接收窗口(rwnd),用于传达可用于保存传入数据的缓冲区空间大小。
连接建立之初,双方使用系统默认设置初始化各自的 rwnd 值。典型的网页主要从服务器向客户端传输数据,因此客户端的窗口可能成为瓶颈。然而,如果客户端向服务器流式传输大量数据(如图片或视频上传),则服务器的接收窗口可能成为限制因素。
如果任何一方无法跟上,它可以向发送方通告更小的窗口。如果窗口降至零,则视为信号,表示在缓冲区中的现有数据被应用层清空之前,不应再发送数据。这一工作流程贯穿每个 TCP 连接的整个生命周期:每个 ACK 数据包都携带双方最新的 rwnd 值,允许双方根据发送方和接收方的容量及处理速度动态调整数据流速。
图 2-2. 接收窗口(rwnd)大小通告
窗口缩放(RFC 1323)
最初的 TCP 规范为接收窗口大小分配了 16 位,这给发送方和接收方通告的最大值(2^16,即 65,535 字节)设置了硬性上限。事实证明,这个上限通常不足以获得最佳性能,尤其是在表现出高带宽延迟积的网络中;更多内容请参阅”带宽延迟积”部分。
为解决此问题,RFC 1323 起草了”TCP 窗口缩放”选项,允许我们将最大接收窗口大小从 65,535 字节提升到 1 GB!窗口缩放选项在三次握手期间协商,携带的值表示在后续 ACK 中要将 16 位窗口大小字段左移的位数。
如今,TCP 窗口缩放已在所有主流平台默认启用。然而,中间节点、路由器和防火墙可能会重写甚至完全剥离此选项。如果你的连接无法充分利用可用带宽,检查窗口大小的交互总是一个好的起点。在 Linux 平台上,可以通过以下命令检查和启用窗口缩放设置:
$> sysctl net.ipv4.tcp_window_scaling
$> sysctl -w net.ipv4.tcp_window_scaling=1
慢启动
尽管 TCP 中存在流量控制,但在 1980 年代中后期,网络拥塞崩溃仍成为现实问题。问题在于,流量控制防止了发送方压垮接收方,但没有机制防止任何一方压垮底层网络:发送方和接收方在连接开始时都不知道可用带宽,因此需要一种机制来估计它,并适应网络中不断变化的条件。
为说明这种自适应的益处,想象你正在家中从远程服务器流式传输大型视频,该服务器已设法饱和你的下行链路以提供最佳质量体验。然后你家庭网络中的另一个用户打开新连接下载软件更新。突然,视频流可用的下行带宽大幅减少,视频服务器必须调整其数据速率——否则,如果继续以相同速率传输,数据将堆积在某个中间网关,导致数据包被丢弃,造成网络资源使用效率低下。
1988 年,Van Jacobson 和 Michael J. Karels 记录了几种解决这些问题的算法:慢启动、拥塞避免、快速重传和快速恢复。这四种算法很快成为 TCP 规范的强制性部分。事实上,人们普遍认为,正是这些对 TCP 的更新,在 80 年代和 90 年代初流量持续指数级增长时,防止了互联网的崩溃。
理解慢启动的最佳方式是观察它的实际运作。让我们回到位于纽约的客户端,尝试从伦敦的服务器获取文件。首先执行三次握手,期间双方在 ACK 数据包中通告各自的接收窗口(rwnd)大小(图 2-2)。一旦最终的 ACK 数据包发出,我们就可以开始交换应用数据。
估计客户端与服务器之间可用容量的唯一方法是通过数据交换来测量,这正是慢启动的设计目的。首先,服务器为每个 TCP 连接初始化一个新的拥塞窗口(cwnd)变量,并将其初始值设置为保守的系统指定值(Linux 上的 initcwnd)。
拥塞窗口大小(cwnd)
发送方限制,规定在收到客户端确认(ACK)之前可以处于传输中的数据量。
cwnd 变量不在发送方和接收方之间通告或交换——在本例中,它将是伦敦服务器维护的私有变量。此外,引入了一条新规则:客户端与服务器之间处于传输中(未确认)的最大数据量是 rwnd 和 cwnd 变量的最小值。到目前为止一切顺利,但服务器和客户端如何确定其拥塞窗口大小的最优值呢?毕竟,网络条件时刻在变化,即使是相同的两个网络节点之间,如前面的例子所示,如果我们能使用算法而不必为每个连接手动调整窗口大小,那就太好了。
解决方案是从慢开始,随着数据包被确认而逐步增大窗口大小:这就是慢启动!最初,cwnd 起始值设置为 1 个网络段;RFC 2581 在 1999 年 4 月将此值更新为 4 个段;最近,RFC 6928 在 2013 年 4 月再次将其增加到 10 个段。
新 TCP 连接的最大传输中数据量是 rwnd 和 cwnd 值的最小值;因此,现代服务器可以向客户端发送最多十个网络段,然后必须停止并等待确认。随后,对于每个收到的 ACK,慢启动算法指示服务器可以将其 cwnd 窗口大小增加一个段——每确认一个数据包,就可以发送两个新数据包。TCP 连接的这一阶段通常被称为**“指数增长”算法**(图 2-3),因为客户端和服务器正试图快速收敛到它们之间网络路径上的可用带宽。
图 2-3. 拥塞控制与拥塞避免
那么,为什么在为浏览器构建应用时,慢启动是一个需要牢记的重要因素呢?HTTP 和许多其他应用协议运行在 TCP 之上,无论可用带宽如何,每个 TCP 连接都必须经历慢启动阶段——我们无法立即使用链路的全部容量!
相反,我们从一个小的拥塞窗口开始,每往返一次将其翻倍——即指数增长。因此,达到特定吞吐量目标所需的时间是客户端与服务器之间往返时延和初始拥塞窗口大小的函数。
达到大小为 N 的 cwnd 所需时间
为直观展示慢启动的影响,让我们假设以下场景:
- 客户端和服务器接收窗口:65,535 字节(64 KB)
- 初始拥塞窗口:10 个段(RFC 6928)
- 往返时延:56 毫秒(伦敦到纽约)
尽管接收窗口大小为 64 KB,新 TCP 连接的吞吐量最初受限于拥塞窗口大小。事实上,要达到 64 KB 的接收窗口限制,我们首先需要将拥塞窗口大小增长到 45 个段,这将耗时 168 毫秒:
这需要三次往返(图 2-4)才能达到客户端与服务器之间 64 KB 的吞吐量!客户端和服务器可能具备 Mbps+ 的数据传输能力,但在建立新连接时这没有影响——这就是慢启动。
上述示例使用了新的(RFC 6928)初始拥塞窗口值,即十个网络段。作为练习,用旧的四个段大小重复相同计算——你会发现这将额外增加 56 毫秒的往返时间!
图 2-4. 拥塞窗口大小增长
要减少拥塞窗口增长所需的时间,我们可以减少客户端与服务器之间的往返时延——例如,将服务器地理位置移近客户端。或者我们可以将初始拥塞窗口大小增加到新的 RFC 6928 值,即 10 个段。
对于大型、持续的流式下载,慢启动不是什么大问题,因为客户端和服务器将在几百毫秒后达到最大窗口大小,并继续以接近最大速度传输——慢启动阶段的成本在较长传输的生命周期中被摊薄。
然而,对于许多通常短促而突发的 HTTP 连接,数据传输在达到最大窗口大小之前完成并不罕见。因此,许多 Web 应用的性能往往受限于服务器与客户端之间的往返时延:慢启动限制了可用带宽吞吐量,对小数据传输的性能产生不利影响。
慢启动重启(SSR)
除了调节新连接的传输速率外,TCP 还实现了慢启动重启(SSR)机制,在连接空闲定义时间段后重置其拥塞窗口。其原理很简单:连接空闲期间网络条件可能已改变,为避免拥塞,窗口被重置为”安全”默认值。
毫不奇怪,SSR 可能对空闲突发长时间的长连接 TCP 连接性能产生重大影响——例如,由于用户不活动。因此,通常建议在服务器上禁用 SSR,以帮助改善长连接 HTTP 连接的性能。在 Linux 平台上,可以通过以下命令检查和禁用 SSR 设置:
$> sysctl net.ipv4.tcp_slow_start_after_idle
$> sysctl -w net.ipv4.tcp_slow_start_after_idle=0
为说明三次握手和慢启动阶段对简单 HTTP 传输的影响,假设我们纽约的客户端通过新 TCP 连接向伦敦的服务器请求一个 64 KB 的文件(图 2-5),并设定以下连接参数:
- 往返时延:56 毫秒
- 客户端和服务器带宽:5 Mbps
- 客户端和服务器接收窗口:65,535 字节
- 初始拥塞窗口:10 个段(~14 KB)
- 服务器生成响应的处理时间:40 毫秒
- 无丢包,每数据包一个 ACK,GET 请求适合单个段
图 2-5. 通过新 TCP 连接获取文件
| 时间 | 事件 |
|---|---|
| 0 ms | 客户端以 SYN 数据包开始 TCP 握手 |
| 28 ms | 服务器以 SYN-ACK 回复,指定其 rwnd 大小 |
| 56 ms | 客户端确认 SYN-ACK,指定其 rwnd 大小,并立即发送 HTTP GET 请求 |
| 84 ms | 服务器收到 HTTP 请求 |
| 124 ms | 服务器完成生成 64 KB 响应,在暂停等待 ACK 前发送 10 个 TCP 段(初始 cwnd 大小为 10) |
| 152 ms | 客户端收到 10 个 TCP 段并确认每个段 |
| 180 ms | 服务器为每个 ACK 增加其 cwnd,发送 20 个 TCP 段 |
| 208 ms | 客户端收到 20 个 TCP 段并确认每个段 |
| 236 ms | 服务器为每个 ACK 增加其 cwnd,发送剩余的 15 个 TCP 段 |
| 264 ms | 客户端收到 15 个 TCP 段,确认每个段 |
在新 TCP 连接上,客户端与服务器之间往返时延为 56 毫秒,传输 64 KB 文件耗时 264 毫秒!
相比之下,假设客户端能够复用同一 TCP 连接(图 2-6),再次发出相同请求。
图 2-6. 通过现有 TCP 连接获取文件
| 时间 | 事件 |
|---|---|
| 0 ms | 客户端发送 HTTP 请求 |
| 28 ms | 服务器收到 HTTP 请求 |
| 68 ms | 服务器完成生成 64 KB 响应,但 cwnd 值已大于发送文件所需的 45 个段;因此它一次性发送所有段 |
| 96 ms | 客户端收到所有 45 个段,确认每个段 |
在相同连接上发出的相同请求,无需三次握手的成本和慢启动阶段的惩罚,现在仅需 96 毫秒,性能提升了 275%!
在这两种情况下,服务器和客户端拥有 5 Mbps 上行带宽的事实,在 TCP 连接启动阶段都没有产生影响。相反,延迟和拥塞窗口大小是限制因素。
事实上,如果我们增加往返时延,通过现有连接发送的第一次和第二次请求之间的性能差距只会扩大;作为练习,尝试用几个不同的值计算。一旦你对 TCP 拥塞控制的机制有了直观理解,诸如 keepalive、管道化和多路复用等数十种优化就几乎不需要进一步说明了。
增加 TCP 初始拥塞窗口
将服务器上的初始 cwnd 大小增加到新的 RFC 6928 值 10 个段(IW10),是改善所有用户和所有基于 TCP 应用性能的最简单方法之一。好消息是,许多操作系统已在其最新内核中更新为使用增加后的值——请查阅相应的文档和发布说明。
对于 Linux,IW10 是 2.6.39 以上所有内核的新默认值。然而,不要止步于此:升级到 3.2+ 还能获得其他重要更新的好处;请参阅”TCP 的比例速率降低”。
拥塞避免
重要的是要认识到,TCP 专门设计为使用丢包作为反馈机制来帮助调节其性能。换句话说,问题不是是否会发生丢包,而是何时会发生。慢启动以保守窗口初始化连接,每往返一次将传输中的数据量翻倍,直到超过接收方的流量控制窗口、系统配置的拥塞阈值(ssthresh)窗口,或直到发生丢包,此时拥塞避免算法(图 2-3)接管。
拥塞避免的隐含假设是,丢包表明网络拥塞:在路径某处我们遇到了拥塞链路或路由器,被迫丢弃数据包,因此我们需要调整窗口以避免诱发更多丢包,防止压垮网络。
一旦拥塞窗口被重置,拥塞避免指定了自己的窗口增长算法,以最小化进一步丢包。在某个时刻,会发生另一次丢包事件,过程再次重复。如果你曾查看过 TCP 连接的吞吐量跟踪,并观察到其中的锯齿模式,现在你知道为什么了:这是拥塞控制和避免算法在调整拥塞窗口大小以应对网络中的丢包。
最后,值得注意的是,改进拥塞控制和避免是学术研究和商业产品的活跃领域:针对不同网络类型、不同数据传输类型等有各种适配。如今,根据你的平台,你可能会运行众多变体之一:TCP Tahoe 和 Reno(原始实现)、TCP Vegas、TCP New Reno、TCP BIC、TCP CUBIC(Linux 默认)、Compound TCP(Windows 默认)等。然而,无论哪种风格,拥塞控制和避免的核心性能影响对所有协议都适用。
TCP 的比例速率降低(PRR)
确定从丢包中恢复的最佳方式并非易事:如果你过于激进,间歇性丢包将对整个连接的吞吐量产生重大影响;如果你调整得不够快,则会诱发更多丢包!
最初,TCP 使用乘性减少加性增加(AIMD)算法:发生丢包时,将拥塞窗口大小减半,然后每往返缓慢增加固定量。然而,在许多情况下,AIMD 过于保守,因此开发了新的算法。
比例速率降低(PRR)是 RFC 6937 规定的新算法,其目标是在丢包时提高恢复速度。它好多少?根据 Google 的测量(该算法在此开发),它可将丢包连接的平均延迟降低 3-10%。
PRR 现在是 Linux 3.2+ 内核中的默认拥塞避免算法——这是升级服务器的另一个好理由!
带宽延迟积
TCP 内置的拥塞控制和拥塞避免机制带来了另一个重要的性能影响:最优的发送方和接收方窗口大小必须根据它们之间的往返时延和目标数据速率而变化。
要理解为何如此,首先回想一下,发送方与接收方之间最大未确认传输中数据量定义为接收窗口(rwnd)和拥塞窗口(cwnd)大小的最小值:当前接收窗口在每个 ACK 中传达,拥塞窗口由发送方根据拥塞控制和避免算法动态调整。
如果发送方或接收方超过最大未确认数据量,则必须停止并等待另一端确认一些数据包后才能继续。需要等待多久?这由两者之间的往返时延决定!
带宽延迟积(BDP)
数据链路容量与其端到端延迟的乘积。结果是在任何时间点可以处于传输中的最大未确认数据量。
如果发送方或接收方经常被迫停止并等待先前数据包的 ACK,这将在数据流中产生间隙(图 2-7),从而限制连接的最大吞吐量。为解决此问题,窗口大小应设置得足够大,使得任何一方都可以继续发送数据,直到收到客户端对较早数据包的 ACK——无间隙,最大吞吐量。因此,最优窗口大小取决于往返时延!选择较低的窗口大小,无论对等方之间可用或通告的带宽如何,你都会限制连接吞吐量。
图 2-7. 由于拥塞窗口大小过低导致的传输间隙
那么,流量控制(rwnd)和拥塞控制(cwnd)窗口值需要多大?实际计算很简单。首先,假设 cwnd 和 rwnd 窗口大小的最小值为 16 KB,往返时延为 100 毫秒:
无论发送方与接收方之间的可用带宽如何,此 TCP 连接的数据速率不会超过 1.31 Mbps!要实现更高吞吐量,我们需要增加窗口大小或降低往返时延。
类似地,如果我们知道往返时延和两端的可用带宽,我们可以计算最优窗口大小。在此场景中,假设往返时延保持不变(100 毫秒),但发送方有 10 Mbps 的可用带宽,接收方处于高吞吐量的 100 Mbps+ 链路上。假设它们之间没有网络拥塞,我们的目标是饱和客户端可用的 10 Mbps 链路:
窗口大小至少需要 122.1 KB 才能饱和 10 Mbps 链路。回想一下,除非存在窗口缩放(参见”窗口缩放(RFC 1323)”),TCP 中的最大接收窗口大小为 64 KB:仔细检查你的客户端和服务器设置!
好消息是,窗口大小协商和调整由网络栈自动管理,应会相应调整。坏消息是,有时它仍将是 TCP 性能的限制因素。如果你曾疑惑为什么你的连接以可用带宽的一小部分传输,即使你知道客户端和服务器都具备更高速率的能力,那很可能是由于窗口大小过小:饱和的对等方通告低接收窗口、恶劣的网络状况和高丢包率重置拥塞窗口,或可能应用于限制连接吞吐量的显式流量整形。
高速局域网中的带宽延迟积
BDP 是往返时延和目标数据速率的函数。因此,虽然往返时延是高传播延迟情况下的常见瓶颈,但在本地局域网(LAN)上它也可能成为瓶颈!
要在 1 毫秒往返时延下实现 1 Gbit/s,我们也需要至少 122 KB 的拥塞窗口。计算与我们之前看到的完全相同;我们只需在目标数据率上增加几个零,并从往返延迟中移除相同数量的零。
队头阻塞
TCP 提供了在不可靠通道上运行可靠网络的抽象,包括基本的数据包错误检查与纠正、按序交付、丢包重传,以及旨在使网络运行在最高效率点的流量控制、拥塞控制和拥塞避免。综合这些特性,TCP 成为大多数应用的首选传输协议。
然而,虽然 TCP 是受欢迎的选择,但它并非唯一选择,也不一定适用于所有场合。具体来说,某些特性(如按序和可靠的数据包交付)并非总是必需的,可能引入不必要的延迟和负面性能影响。
要理解为何如此,回想一下每个 TCP 数据包在发出时都携带唯一的序列号,数据必须按序传递给接收方(图 2-8)。如果其中一个数据包在前往接收方的途中丢失,则所有后续数据包必须保存在接收方的 TCP 缓冲区中,直到丢失的数据包被重传并到达接收方。由于这项工作在 TCP 层完成,我们的应用对 TCP 重传或排队的数据包缓冲区没有可见性,必须等待完整序列才能访问数据。相反,当它尝试从套接字读取数据时,只看到交付延迟。这种效应被称为 TCP 队头(HOL)阻塞。
队头阻塞带来的延迟使我们的应用无需处理数据包重排序和重组,这使我们的应用代码更简单。然而,这是以在数据包到达时间中引入不可预测的延迟变化(通常称为抖动)为代价的,这可能对应用性能产生负面影响。
图 2-8. TCP 队头阻塞
此外,某些应用可能甚至不需要可靠交付或按序交付:如果每个数据包都是独立消息,则按序交付严格不必要;如果每条消息都覆盖所有先前消息,则可以完全移除可靠交付的要求。不幸的是,TCP 不提供此类配置——所有数据包都被排序并按序交付。
能够处理乱序交付或丢包、且对延迟或抖动敏感的应用,可能更适合使用替代传输协议,如 UDP。
丢包是可以接受的
事实上,丢包对于从 TCP 获得最佳性能是必要的!丢弃的数据包充当反馈机制,允许接收方和发送方调整其发送速率以避免压垮网络,并最小化延迟;参见”本地路由器中的缓冲区膨胀”。此外,某些应用可以在无不利影响的情况下容忍丢包:音频、视频和游戏状态更新是常见应用数据示例,不需要可靠或按序交付——顺便说一下,这也是 WebRTC 使用 UDP 作为其基础传输的原因。
如果数据包丢失,音频编解码器可以简单地在音频中插入一个小间隙并继续处理传入的数据包。如果间隙很小,用户甚至可能不会注意到,而等待丢失的数据包会冒着在音频输出中引入可变暂停的风险,这将导致用户更差的体验。
类似地,如果我们正在交付游戏状态更新,那么等待描述时间 T-1 状态的数据包,当我们已经拥有时间 T 的数据包时,通常根本没必要——理想情况下,我们会收到每个更新,但为避免游戏延迟,我们可以接受间歇性丢包以换取更低延迟。
TCP 优化实践
TCP 是一种自适应协议,旨在对所有网络对等方公平,并最有效地利用底层网络。因此,优化 TCP 的最佳方式是调整 TCP 如何感知当前网络条件,并根据其下层和上层的类型和需求调整其行为:无线网络可能需要不同的拥塞算法,某些应用可能需要自定义的服务质量(QoS)语义以提供最佳体验。
不断变化的应用需求与每个 TCP 算法中的众多旋钮之间的紧密相互作用,使 TCP 调优和优化成为学术和商业研究中取之不尽的领域。在本章中,我们只触及了影响 TCP 性能众多因素的皮毛。其他机制,如选择性确认(SACK)、延迟确认、快速重传等,使每个 TCP 会话的理解、分析和调优更加复杂(或有趣,取决于你的视角)。
话虽如此,虽然每个算法和反馈机制的具体细节将继续演进,但核心原则及其影响保持不变:
- TCP 三次握手引入了一个完整的往返时延。
- TCP 慢启动应用于每个新连接。
- TCP 流量和拥塞控制调节所有连接的吞吐量。
- TCP 吞吐量受当前拥塞窗口大小调节。
因此,在现代高速网络中,TCP 连接传输数据的速率往往受限于接收方与发送方之间的往返时延。此外,虽然带宽持续增加,但延迟受光速限制,已接近其最大值的较小常数因子。在大多数情况下,延迟而非带宽是 TCP 的瓶颈——例如,参见图 2-5。
服务器配置调优
作为起点,在调优 TCP 中每个缓冲区和超时变量的具体值(有数十个)之前,你最好简单地将主机升级到最新的系统版本。TCP 最佳实践和决定其性能的底层算法持续演进,大多数这些更改仅在最新内核中可用。简而言之,保持服务器更新,以确保发送方和接收方 TCP 栈之间的最优交互。
表面上,升级服务器内核版本似乎是微不足道的建议。然而,在实践中,它经常遇到重大阻力:许多现有服务器针对特定内核版本进行了调优,系统管理员不愿执行升级。
公平地说,每次升级都带来风险,但要获得最佳 TCP 性能,这也可能是你能做的最佳单一投资。
有了最新的内核,确保服务器配置遵循以下最佳实践是良好做法:
增加 TCP 初始拥塞窗口
更大的起始拥塞窗口允许 TCP 在第一次往返中传输更多数据,显著加速窗口增长。
慢启动重启
禁用空闲后的慢启动将改善以周期性突发传输数据的长连接 TCP 连接性能。
窗口缩放(RFC 1323)
启用窗口缩放增加最大接收窗口大小,允许高延迟连接实现更好吞吐量。
TCP 快速打开
允许在某些情况下在初始 SYN 数据包中发送应用数据。TFO 是一种新的优化,需要客户端和服务器都支持;调查你的应用是否可以利用它。
上述设置与最新内核的组合,将为单个 TCP 连接实现最佳性能——更低延迟和更高吞吐量。
根据你的应用,你可能还需要调优服务器上的其他 TCP 设置,以优化高连接率、内存消耗或类似标准。查阅你的平台文档,并阅读 HTTP 工作组维护的《TCP Tuning for HTTP》文档以获取额外建议。
对于 Linux 用户,ss 是一个有用的强大工具,用于检查开放套接字的各种统计信息。从命令行运行 ss --options --extended --memory --processes --info,查看当前对等方及其各自的连接设置。
应用层行为优化
调优 TCP 性能允许服务器和客户端为单个连接提供最佳吞吐量和延迟。然而,应用如何使用每个新的或已建立的 TCP 连接,可能产生更大影响:
没有比不发送的比特更快的比特;发送更少的比特。
我们无法让比特传播得更快,但我们可以将比特移得更近。
TCP 连接复用对于提升性能至关重要。
消除不必要的数据传输当然是最佳优化——例如,消除不必要的资源,或通过应用适当的压缩算法确保传输最少数量的比特。其次,通过在全球地理分布服务器——例如使用 CDN——将比特移近客户端,将有助于减少网络往返延迟,显著提升 TCP 性能。最后,在可能的情况下,应复用现有 TCP 连接,以最小化慢启动和其他拥塞机制带来的开销。
性能检查清单
优化 TCP 性能会带来高回报,无论应用类型如何,对于到你的服务器的每个新连接都是如此。列入议程的简短清单:
- 将服务器内核升级到最新版本。
- 确保 cwnd 大小设置为 10。
- 确保启用窗口缩放。
- 禁用空闲后的慢启动。
- 调查启用 TCP 快速打开。
- 消除冗余数据传输。
- 压缩传输数据。
- 将服务器部署得更靠近用户以减少往返时延。
- 尽可能复用 TCP 连接。
- 研究《TCP Tuning for HTTP》建议。