为什么这么设计(Why’s THE Design)是一系列关于计算机领域中程序设计决策的文章,我们在这个系列的每一篇文章中都会提出一个具体的问题并从不同的角度讨论这种设计的优缺点、对具体实现造成的影响。如果你有想要了解的问题,可以在文章下面留言。
TCP 协议可以说是今天互联网的基石,作为可靠的传输协议,在今天几乎所有的数据都会通过 TCP 协议传输,然而 TCP 在设计之初没有考虑到现今复杂的网络环境,当你在地铁上或者火车上被断断续续的网络折磨时,你可能都不知道这一切可能都是 TCP 协议造成的。本文会分析 TCP 协议为什么在弱网环境下有严重的性能问题1。
注:本文的分析基于 RFC 7932 中定义的 TCP 协议,从 RFC 793 发布至今已经过了将近 40 年,期间多个状态为 Proposed Standard 的非强制性 RFC 都对 TCP 协议进行了修订,尝试优化 TCP 协议的性能,例如:选择性 ACK(Selective ACK, SACK)3、虚假超时剖析(Forward RTO, F-RTO)4 和 TCP 快开启(TCP Fast Open, TFO)5,最新版本的 Linux 中已经包含了这些 RFC 的实现。
底层的数据传输协议在设计时必须要对带宽的利用率和通信延迟进行权衡和取舍,所以想要解决实际生产中的全部问题是不可能的,TCP 选择了充分利用带宽,为流量而设计,期望在尽可能短的时间内传输更多的数据6。
在网络通信中,从发送方发出数据开始到收到来自接收方的确认的时间被叫做往返时延(Round-Trip Time,RTT)。
弱网环境是丢包率较高的特殊场景,TCP 在类似场景中的表现很差,当 RTT 为 30ms 时,一旦丢包率达到了 2%,TCP 的吞吐量就会下降 89.9%7,从下面的表中我们可以看出丢包对 TCP 的吞吐量极其显著的影响:
RTT | TCP 吞吐量 | TCP 吞吐量(2% 丢包率) |
---|---|---|
0 ms | 93.5 Mbps | 3.72 Mbps |
30 ms | 16.2 Mbps | 1.63 Mbps |
60 ms | 8.7 Mbps | 1.33 Mbps |
90 ms | 5.32 Mbps | 0.85 Mbps |
本文将分析在弱网环境下(丢包率高)影响 TCP 性能的三个原因:
TCP 的拥塞控制算法会在丢包时主动降低吞吐量;
TCP 的三次握手增加了数据传输的延迟和额外开销;
TCP 的累计应答机制导致了数据段的传输;
在上述的三个原因中,拥塞控制算法是导致 TCP 在弱网环境下有着较差表现的首要原因,三次握手和累计应答两者的影响依次递减,但是也加剧了 TCP 的性能问题。
拥塞控制
TCP 拥塞控制算法是互联网上主要的拥塞控制措施,它使用一套基于线増积减(Additive increase/multiplicative decrease,AIMD)的网络拥塞控制方法来控制拥塞8,也是造成 TCP 性能问题的主要原因。
第一次发现的互联网拥塞崩溃是在 1986 年,NSFnet 阶段一的骨干网的处理能力从 32,000bit/s 降到了 40bit/s,该骨干网的处理能力直到 1987 和 1988 年,TCP 协议实现了拥塞控制之后才得到解决9。正是因为发生过网络阻塞造成的崩溃,所以 TCP 的拥塞控制算法就认为只要发生了丢包当前网络就发生了拥堵,从这一假设出发,TCP 最初的实现 Tahoe 和 Reno 就使用了慢启动和拥塞避免两个机制10实现拥塞控制,本节中对拥塞控制的分析就是基于这个版本的实现。
图 1 - TCP 拥塞控制
每一个 TCP 连接都会维护一个拥塞控制窗口(Congestion Window),它决定了发送方同时能向接收方发送多少数据,其作用主要有两个:
防止发送方向接收方发送了太多数据,导致接收方无法处理;
防止 TCP 连接的任意一方向网络中发送大量数据,导致网络拥塞崩溃;
除了拥塞窗口大小(cwnd)之外,TCP 连接的双方都有接收窗口大小(rwnd),在 TCP 连接建立之初,发送方和接收方都不清楚对方的接收窗口大小,所以通信双方需要一套动态的估算机制改变数据传输的速度,在 TCP 三次握手期间,通信双方会通过 ACK 消息通知对方自己的接收窗口大小,接收窗口大小一般是带宽延迟乘积(Bandwidth-delay product, BDP)决定的11,不过在这里我们就不展开介绍了。
客户端能够同时传输的最大数据段的数量是接收窗口大小和拥塞窗口大小的最小值,即 min(rwnd, cwnd)
。TCP 连接的初始拥塞窗口大小是一个比较小的值,在 Linux 中是由 TCP_INIT_CWND
定义的12:
/* TCP initial congestion window as per rfc6928 */ #define TCP_INIT_CWND 10
C
初始拥塞控制窗口的大小从出现之后被多次修改,几个名为 Increasing TCP’s Initial Window 的 RFC 文档:RFC241413、RFC339014 和 RFC692815 分别增加了 initcwnd
的值以适应不断提高的网络传输速度和带宽。
TCP 协议使用慢启动阈值(Slow start threshold, ssthresh)来决定使用慢启动或者拥塞避免算法:
当拥塞窗口大小小于慢启动阈值时,使用慢启动;
当拥塞窗口大小大于慢启动阈值时,使用拥塞避免算法;
当拥塞窗口大小等于慢启动阈值时,使用慢启动或者拥塞避免算法;
图 2 - TCP 的慢启动
如上图所示,使用 TCP 慢启动时,发送方每收到一个响应方的 ACK 消息,拥塞窗口大小就会加一。当拥塞窗口大小大于慢启动阈值时,就会使用拥塞避免算法:
线性增长:每收到一个 ACK,拥塞窗口大小会加一;
积式减少:当发送方发送的数据包丢包时,慢启动阈值会设置为拥塞窗口大小的一半;
TCP 的早期实现 Tahoe 和 Reno 在遇到丢包时会将拥塞控制大小重置为初始值16,由于拥塞窗口大小小于慢启动阈值,所以重新进入慢启动阶段。
如果 TCP 连接刚刚建立,由于 Linux 系统的默认设置,客户端能够同时发送 10 个数据段,假设我们网络的带宽是 10M,RTT 是 40ms,每个数据段的大小是 1460 字节,那么使用 BDP 计算的通信双方窗口大小上限应该是 35,这样才能充分利用网络的带宽:
rwndmax=10×106bit/s⋅40×10−3s1460∗8bit=35rwndmax=10×106bit/s⋅40×10−3s1460∗8bit=35
然而拥塞控制窗口的大小从 10 涨到 35 需要 2RTT 的时间,具体的过程如下:
发送方向接收方发送
initcwnd = 10
个数据段(消耗 0.5RTT);接收方接收到 10 个数据段后向发送方发送 ACK(消耗 0.5RTT);
发送方接收到发送方的 ACK,拥塞控制窗口大小由于 10 个数据段的成功发送 +10,当前拥塞控制窗口大小达到 20;
发送方向接收方发送 20 个数据段(消耗 0.5RTT);
接收方接收到 20 个数据段后向发送方发送 ACK(消耗 0.5RTT);
发送方接收到发送方的 ACK,拥塞控制窗口大小由于 20 个数据段的成功发送 +20,当前拥塞控制窗口大小达到 40;
从 TCP 三次握手建立连接到拥塞控制窗口大小达到假定网络状况的最大值 35 需要 3.5RTT 的时间,即 140ms,这是一个比较长的时间了。
早期互联网的大多数计算设备都通过有线网络连接,出现网络不稳定的可能性也比较低,所以 TCP 协议的设计者认为丢包意味着网络出现拥塞,一旦发生丢包,客户端疯狂重试就可能导致互联网的拥塞崩溃,所以发明了拥塞控制算法来解决该问题。
但是如今的网络环境更加复杂,无线网络的引入导致部分场景下的网络不稳定成了常态,所以丢包并不一定意味着网络拥堵,如果使用更加激进的策略传输数据,在一些场景下会得到更好的效果
作者公众号:一起写程序
原文链接:https://draveness.me/whys-the-design-tcp-performance/
共同学习,写下你的评论
评论加载中...
作者其他优质文章