TCP 协议可以说是今天互联网的基石,作为可靠的传输协议,在今天几乎所有的数据都会通过 TCP 协议传输,然而 TCP 在设计之初没有考虑到现今复杂的网络环境,当你在地铁上或者火车上被断断续续的网络折磨时,你可能都不知道这一切可能都是 TCP 协议造成的。本文会分析 TCP 协议为什么在弱网环境下有严重的性能问题[^1]。

底层的数据传输协议在设计时必须要对带宽的利用率和通信延迟进行权衡和取舍,所以想要解决实际生产中的全部问题是不可能的,TCP 选择了充分利用带宽,为流量而设计,期望在尽可能短的时间内传输更多的数据[^2]。

在网络通信中,从发送方发出数据开始到收到来自接收方的确认的时间被叫做往返时延(Round-Trip Time,RTT)。

弱网环境是丢包率较高的特殊场景,TCP 在类似场景中的表现很差,当 RTT 为 30ms 时,一旦丢包率达到了 2%,TCP 的吞吐量就会下降 89.9%[^3],从下面的表中我们可以看出丢包对 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/IP网络模型

计算机与网络设备要相互通信,双方就必须基于相同的方法。比如,如何探测到通信目标、由哪一边先发起通信、使用哪种语言进行通信、怎样结束通信等规则都需要事先确定。不同的硬件、操作系统之间的通信,所有的这一切都需要一种规则。而我们就把这种规则称为协议(protocol)。

TCP/IP 是互联网相关的各类协议组的总称,比如:TCP,UDP,IP,FTP,HTTP,ICMP,SMTP 等都属于 TCP/IP 族内的协议。

TCP/IP模型是互联网的基础,它是一系列网络协议的总称。这些协议可以划分为四层,分别为链路层、网络层、传输层和应用层。

链路层:负责封装和解封装IP报文,发送和接收ARP/RARP报文等。网络层:负责路由以及把分组报文发送给目标网络或主机。传输层:负责对报文进行分组和重组,并以TCP或UDP协议格式封装报文。应用层:负责向用户提供应用程序,比如HTTP、FTP、Telnet、DNS、SMTP等。

在网络体系结构中网络通信的建立必须是在通信双方的对等层进行,不能交错。 在整个数据传输过程中,数据在发送端时经过各层时都要附加上相应层的协议头和协议尾(仅数据链路层需要封装协议尾)部分,也就是要对数据进行协议封装,以标识对应层所用的通信协议。接下去介绍TCP/IP 中有两个具有代表性的传输层协议----TCP 和 UDP。

二、UDP

UDP协议全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在第四层——传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。

它有以下几个特点:

1.面向无连接

首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。

具体来说就是:

在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作

2.有单播,多播,广播的功能

UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

3.UDP是面向报文的

发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文

4.不可靠性

首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。

并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。

再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。

从上面的动态图可以得知,UDP只会把想发的数据报文一股脑的丢给对方,并不在意数据有无安全完整到达。

5.头部开销小,传输数据报文时是很高效的。

UDP 头部包含了以下几个数据:

两个十六位的端口号,分别为源端口(可选字段)和目标端口整个数据报文的长度整个数据报文的检验和(IPv4 可选 字段),该字段用于发现头部信息和数据中的错误

因此 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的

三、TCP

当一台计算机想要与另一台计算机通讯时,两台计算机之间的通信需要畅通且可靠,这样才能保证正确收发数据。例如,当你想查看网页或查看电子邮件时,希望完整且按顺序查看网页,而不丢失任何内容。当你下载文件时,希望获得的是完整的文件,而不仅仅是文件的一部分,因为如果数据丢失或乱序,都不是你希望得到的结果,于是就用到了TCP。

TCP协议全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的RFC 793定义。TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,你可以把它想象成排水管中的水流。

1.TCP连接过程

如下图所示,可以看到建立一个TCP连接的过程为(三次握手的过程):

第一次握手

客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。

第二次握手

服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

第三次握手

当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

这里可能大家会有个疑惑:为什么 TCP 建立连接需要三次握手,而不是两次?这是因为这是为了防止出现失效的连接请求报文段被服务端接收的情况,从而产生错误。

2.TCP断开链接

TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

第一次握手

若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。

第二次握手

B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明 A 到 B 的连接已经释放,不再接收 A 发的数据了。但是因为 TCP 连接是双向的,所以 B 仍旧可以发送数据给 A。

第三次握手

B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。

第四次握手

A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

3.TCP协议的特点

面向连接

面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。

仅支持单播传输

每条TCP传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。

面向字节流

TCP不像UDP一样那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。

可靠传输

对于可靠传输,判断丢包,误码靠的是TCP的段编号以及确认号。TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。

提供拥塞控制

当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞

TCP提供全双工通信

TCP允许通信双方的应用程序在任何时候都能发送数据,因为TCP连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于MSS)

四、TCP和UDP的比较

1.对比

UDP

TCP

是否连接

无连接

面向连接

是否可靠

不可靠传输,不使用流量控制和拥塞控制

可靠传输,使用流量控制和拥塞控制

连接对象个数

支持一对一,一对多,多对一和多对多交互通信

只能是一对一通信

传输方式

面向报文

面向字节流

首部开销

首部开销小,仅8字节

首部最小20字节,最大60字节

适用场景

适用于实时应用(IP电话、视频会议、直播等)

适用于要求可靠传输的应用,例如文件传输

2.总结

TCP向上层提供面向连接的可靠服务 ,UDP向上层提供无连接不可靠服务。虽然 UDP 并没有 TCP 传输来的准确,但是也能在很多实时性要求高的地方有所作为对数据准确性要求高,速度可以相对较慢的,可以选用TCP

拥塞控制

TCP 拥塞控制算法是互联网上主要的拥塞控制措施,它使用一套基于线増积减(Additive increase/multiplicative decrease,AIMD)的网络拥塞控制方法来控制拥塞[^4],也是造成 TCP 性能问题的主要原因。

第一次发现的互联网拥塞崩溃是在 1986 年,NSFnet 阶段一的骨干网的处理能力从 32,000bit/s 降到了 40bit/s,该骨干网的处理能力直到 1987 和 1988 年,TCP 协议实现了拥塞控制之后才得到解决[^5]。正是因为发生过网络阻塞造成的崩溃,所以 TCP 的拥塞控制算法就认为只要发生了丢包当前网络就发生了拥堵,从这一假设出发,TCP 就使用了慢启动和线增积减[^6]的机制实现拥塞控制。

tcp-congestion-control

图 1 - TCP 的拥塞控制机制

每一个 TCP 连接都会维护一个拥塞控制窗口(Congestion Window),拥塞控制窗口的作用有两个:

防止发送方向接收方发送了太多数据,导致接收方无法处理;防止 TCP 连接的任意一方向网络中发送大量数据,导致网络拥塞崩溃;

除了拥塞窗口大小(cwnd)之外,TCP 连接的双方都有接收窗口大小(rwnd),在 TCP 连接建立之初,发送方和接收方都不清楚对方的接收窗口大小,所以通信双方需要一套动态的估算机制改变数据传输的速度,在 TCP 三次握手期间,通信双方会通过 ACK 消息通知对方自己的接收窗口大小,接收窗口大小一般是带宽延迟乘积(Bandwidth-delay product, BDP)决定的[^7],不过在这里我们就不展开介绍了。

客户端能够同时传输的最大数据段的数量是接收窗口大小和拥塞窗口大小的最小值,即 min(rwnd, cwnd)。TCP 连接的初始拥塞窗口大小是一个比较小的值,在 Linux 中是由 TCP_INIT_CWND 定义的[^8]:

/* TCP initial congestion window as per rfc6928 */

#define TCP_INIT_CWND10

初始拥塞控制窗口的大小从出现之后被多次修改,几个名为 Increasing TCP's Initial Window 的 RFC 文档:RFC2414[^9]、RFC3390[^10] 和 RFC6928[^11] 分别增加了 initcwnd 的值以适应不断提高的网络传输速度和带宽。

tcp-congestion-window

图 2 - TCP 拥塞控制算法的线増积减

如上图所示,TCP 连接发送方的拥塞控制窗口大小会根据接收方的响应而变化:

线性增长:经过 1 个 RTT ,拥塞窗口大小会加一;积式减少:当发送方发送的数据包丢包时,拥塞控制阈值会减半;

如果 TCP 连接刚刚建立,由于 Linux 系统的默认设置,客户端能够同时发送 10 个数据段,假设我们网络的带宽是 10M,RTT 是 40ms,每个数据段的大小是 1460 字节,那么使用 BDP 计算的通信双方窗口大小上限应该是 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 协议的设计者认为丢包意味着网络出现拥塞,一旦发生丢包,客户端疯狂重试就可能导致互联网的拥塞崩溃,所以发明了拥塞控制算法来解决该问题。

但是如今的网络环境更加复杂,无线网络的引入导致部分场景下的网络不稳定成了常态,所以丢包并不一定意味着网络拥堵,如果使用更加激进的策略传输数据,在一些场景下会得到更好的效果。

三次握手

TCP 使用三次握手建立连接应该是全世界所有工程师都十分了解的知识点,三次握手的主要目的是避免历史错误连接的建立并让通信的双方确定初始序列号[^12],然而三次握手的成本相当高,在不丢包的情况下,它需要建立 TCP 连接的双方进行三次通信。

basic-3-way-handshake

图 3 - 常见的 TCP 三次握手

如果我们要从北京访问上海的服务器,由于北京到上海的直线距离约为 1000 多公里,而光速是目前通信速度的极限,所以 RTT 一定会大于 6.7ms:

然而因为光在光纤中不是直线传播的,真正的传输速度会比光速慢 ~31%[^13],而且数据需要在各种网络设备之间来回跳转,所以很难达到理论的极限值。在生产环境中从北京到上海的 RTT 大概在 40ms 左右,所以 TCP 建立连接所需要最短时间也需要 60ms(1.5RTT)。

在网络环境较差的地铁、车站等场景中,因为丢包率较高,客户端很难与服务端快速完成三次通信并建立 TCP 连接。当客户端长时间没有收到服务端的响应时,只能不断发起重试,随着请求次数逐渐增加,访问的延迟也会越来越高。

由于大多数的 HTTP 请求都不会携带大量的数据,未被压缩的请求和响应头大小在 ~200B 到 2KB 左右,而 TCP 三次握手带来的额外开销是 222 字节,其中以太网数据帧占 3 * 14 = 42 字节,IP 数据帧占 3 * 20 = 60 字节,TCP 数据帧占 120 字节:

tcp-three-way-handshake-overhead

图 4 - TCP 三次握手的额外开销

虽然 TCP 不会为每一个发出的数据段建立连接,但是三次握手建立连接需要的成本还是相当高,不仅需要额外增加 1.5RTT 的网络延时,还需要增加 222 字节的额外开销,所以在弱网环境下,通过三次握手建立连接会加剧 TCP 的性能问题。

重传机制

TCP 传输的可靠性是通过序列号和接收方的 ACK 来保证的,当 TCP 传输一个数据段时,它会将该数据段的副本放到重传队列上并开启计时器[^14]:

如果发送方收到了该数据段对应的 ACK 响应,当前数据段就会从重传队列中删除;如果发送方在计时器到期之间都没有收到该数据段对应的 ACK,就会重新发送当前数据段;

TCP 的 ACK 机制可能会导致发送方重新传输接收方已经收到了数据段。TCP 中的 ACK 消息表示该消息之前的全部消息都已经被成功接收和处理,例如:

发送方向接收方发送了序号为 1-10 的消息;接收方向发送方发送 ACK 8 响应;发送方认为序号为 1-8 的消息已经被成功接收;

这种 ACK 的方式在实现上比较简单,更容易保证消息的顺序性,但是在以下情况可能会导致发送方重传已经接收的数据:

tcp-retransmission-al

图 5 - TCP 的重传策略

如上图所示,接收方已经收到了序号为 2-5 的数据,但是由于 TCP ACK 的语义是当前数据段前的全部数据段都已经被接收和处理,所以接收方无法发送 ACK 消息,由于发送方没有收到 ACK,所有数据段对应的计时器就会超时并重新传输数据。在丢包较为严重的网络下,这种重传机制会造成大量的带宽浪费。

总结

TCP 协议的一些设计在今天来看虽然仍然具有巨大的价值,但是并不能适用于所有场景。为了解决 TCP 的性能问题,目前业界有两种解决方案:

使用 UDP 构建性能更加优异、更灵活的传输协议,例如:QUIC[^15] 等;通过不同的手段优化 TCP 协议的性能,例如:选择性 ACK(Selective ACK, SACK)[^16],TCP 快开启(TCP Fast Open, TFO)[^17];

由于 TCP 协议在操作系统内核中,不利于协议的更新,所以第一种方案目前发展的更好,HTTP/3 就使用了 QUIC 作为传输协议[^18]。我们在这里重新回顾一下导致 TCP 性能问题的三个重要原因:

TCP 的拥塞控制在发生丢包时会进行退让,减少能够发送的数据段数量,但是丢包并不一定意味着网络拥塞,更多的可能是网络状况较差;TCP 的三次握手带来了额外开销,这些开销不只包括需要传输更多的数据,还增加了首次传输数据的网络延迟;TCP 的重传机制在数据包丢失时可能会重新传输已经成功接收的数据段,造成带宽的浪费;

TCP 协议作为互联网数据传输的基石可以说是当之无愧,虽然它确实在应对特殊场景时有些问题,但是它的设计思想有着非常多的借鉴意义并值得我们学习。

到最后,我们还是来看一些比较开放的相关问题,有兴趣的读者可以仔细思考一下下面的问题:

QUIC 协议是能否保证丢包率较高时的传输性能?除了 SACK 和 TFO 之外还有哪些手段可以优化 TCP 的性能?