You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

197 lines
17 KiB

---
TCP 的构成
---
#### 目录
1. 前言
2. 三次握手
3. 拥塞预防及控制
* 流量控制
* 慢启动
* 拥塞预防
4. 队首阻塞
5. 针对 TCP 的优化建议
* 服务器配置调优
* 应用程序行为调优
* 性能检查清单
#### 前言
因特网有两个核心协议:TCP 和 IP。IP,即因特网协议,负责联网主机之间的路由选择和寻址;TCP,即传输控制协议,负责在不可靠的传输信道之上提供可靠地抽象层。
TCP 负责在不可靠的传输信道之上提供可靠的抽象层,向应用层隐藏了大多数网络通信的复杂细节,比如丢包重发、按序发送、拥塞控制及避免、数据完整,等等。采用 TCP 数据流可以确保发送的所有字节能够完整的被接收到,而且到达客户端的顺序也一样。也就是说,TCP 专门为精确传送做了优化,但并未过多顾及时间,这一点也给优化浏览器 Web 性能带来了挑战。
HTTP 标准并未规定 TCP 就是唯一的传输协议。如果你愿意,还可以通过 UDP(用户数据报协议)或者其他可用协议来发送 HTTP 消息。但在现实当中,由于 TCP 提供了很多有用的功能,几乎所有 HTTP 流量都是通过 TCP 传送的。
#### 三次握手
所有 TCP 连接一开始都要经过三次握手。客户端与服务器在交换应用数据之前,必须就起始分组序列号,以及其他一些连接相关的细节达成一致。出于安全考虑,序列号由两端随机生成。
![](https://i.loli.net/2019/06/18/5d087dcb28ff059341.png)
* SYN
客户端选择一个随机序列号 x,并发送一个 SYN 分组,其中可能还包括其他 TCP 标志和选项。
* SYN ACK
服务器给 x 加 1,并选择自己的一个随机序列号 y,追加自己的标志和选项,然后返回响应。
* ACK
客户端给 x 和 y 加 1 并发送握手期间的最后一个 ACK 分组。
三次握手完成后,客户端与服务器之间就可以通信了。客户端可以在发送 ACK 分组之后立即发送数据,而服务器必须等接收到 ACK 分组之后才能发送数据。这个启动通信的过程适用于所有 TCP 连接,因此对所有使用 TCP 的应用具有非常大的性能影响,因为每次传输应用数据之前,都必须经历一次完整的往返。
举个例子,如果客户端在纽约,服务器在伦敦,要通过光纤启动一次新的 TCP 连接,光三次握手至少要花 56 ms;向伦敦发送分组需要 28 ms,响应发回纽约又要 28 ms。在此,连接的带宽对时间没有影响,延迟完全取决于客户端和服务器之间的往返时间,这其中主要是纽约到伦敦之间的传输时间。
三次握手带来的延迟使得每创建一个新的 TCP 连接都要付出很大代价。而这也决定了提高 TCP 应用性能的关键,在于想办法重用连接。
> TCP 快速打开
>
> 遗憾的是,连接不是想重用就可以重用的。事实上,由于非常短的 TCP 连接在互联网上随处可见,握手阶段已经成为影响网络总延迟的一个重要因素。为解决这个问题,人们正在积极寻找各种方案,其中 TFO(TCP Fast Open,TCP 快速打开)就是这样一种机制,致力于减少新建 TCP 连接带来的性能损失。
>
> Linux 3.7 及之后的内核已经在客户端和服务器中支持 TFO,因此成为了客户端和服务器操作系统选型的有力候选方案。即使如此,TFO 并不能解决所有问题。它虽然有助于减少三次握手的往返时间,但却只能在某些情况下有效。比如,随同 SYN 分组一起发送的数据净荷有最大尺寸限制、只能发送某些类型的 HTTP 请求,以及由于依赖加密 cookie,只能应用于重复的连接。
#### 阻塞预防及控制
1984 年初,John Nagle 提到了一个被称为 “拥塞崩溃” 的现象,这个现象会影响节点间带宽容量不对称的任何网络:可能是往返时间超过了所有主机的最大中断间隔,于是相应的主机会在网络中制造越来越多的数据报副本,使得整个网络陷入瘫痪。最终,所有交换节点的缓冲区都将被填满,多出来的分组必须删掉。目前的分组往返时间已经设定为最大值。主机会把每个分组都发送好几次,结果每个分组的某个副本会抵达目标,这就是拥塞崩溃。
为了解决这些问题,TCP 加入了很多机制,以便控制双向发送数据的速度,比如流量控制、阻塞控制和拥塞预防机制。
##### 流量控制
流量控制是一种预防发送端过多向接收端发送数据的机制。否则,接收端可能因为忙碌、负载重或缓冲区既定而无法处理。为实现流量控制,TCP 连接的每一方都要通告自己的接收窗口(rwnd),其中包含能够保存数据的缓冲区空间大小信息。
![](https://i.loli.net/2019/06/18/5d0885677bcb877502.png)
第一次建立连接时,两端都会使用自身系统的默认设置来发送 rwnd。浏览网页通过主要是从服务器向客户端下载数据,因此客户端窗口更可能成为瓶颈。然后,如果是在上传图片或视频,即客户端向服务端传送大量数据时,服务器的接收窗口又可能成为制约因素。
不管怎样,如果其中一端跟不上数据传输,那它可以向发送端通告一个较小的窗口。假如窗口为零,则意味着必须由应用层先清空缓冲区,才能再接收剩余数据,这个过程贯穿于每个 TCP 连接的整个生命周期:每个 ACK 分组都会携带相应的最新的 rwnd 值,以便两端动态调整数据流速,使之适应发送端和接收端的容量及处理能力。
##### 慢启动
尽管 TCP 有了流量控制机制,但网络拥塞崩溃仍然在 1980 年代中后期浮出水面。流量控制确实可以防止发送端向接收端过多发送数据,但却没有机制预防任何一端向潜在网络过多发送数据。换句话说,发送端和接收端在连接建立之初,谁也不知道可用带宽是多少,因此需要一个估算机制,然后还要根据网络中不断变化的条件而动态改变速度。
要说明这种动态适应机制的好处,可以想象你在家观看一个大型的流视频。视频服务器会尽最大努力根据你的下行连接提供最高品质信息。而此时,你家里又有人打开一个新连接下载某个软件的升级包。可供视频流使用的下行带宽一下子少了很多,视频服务器必须调整它的发送速度。否则,如果继续保持同样的速度,那么数据很快就会在某个中间的网关越积越多,最终会导致分组被删除,从而降低网络传输效率。
要理解慢启动,最好看一个例子。同样,假如纽约有一个客户端,尝试从位于伦敦的服务器上取得一个文件。首先,三次握手,而且在此期间双方各自通过 ACK 分组通告自己的接收窗口(rwnd)大小。在发送完最后一次 ACK 分组后,就可以交换应用数据了。
此时,根据交换数据来估算客户端与服务器之间的可用带宽是唯一的方法,而且这也是慢启动算法的设计思路。首先,服务器通过 TCP 连接初始化一个新的拥塞窗口(cwnd)变量,将其值设置为一个系统设定的保守值(在 Linux 中就是 initcwnd)。
拥塞窗口大小(cwnd):
发送端对从客户端接收确认(ACK)之前可以发送数据量的限制。
发送端不会通过 cwnd 变量,即发送端和接收端不会交换这个值。此时,位于伦敦的服务器只是维护这么一个私有变量。此时又有了一条新规则,即客户端与服务器之间最大可以传输(未经 ACK 确认的)数据量取 rwnd 和 cwnd 变量中的最小值。那服务器和客户端怎么确定拥塞窗口大小的最优值呢?毕竟,网络状况随时都在变化,即使相同的两个网络节点之间也一样。如果能通过算法来确定每个连接的窗口大小,而不用手工调整就最好了。
解决方案就是慢启动,即在分组被确认后增大窗口大小,慢慢的启动!
新 TCP 连接传输的最大数据量取 rwnd 和 cwnd 中的最小值,而服务器实际上可以向客户端发送 4 个 TCP 段,然后就必须停下来等待确认。此后,每收到一个 ACK,慢启动算法就会告诉服务器可以将它的 cwnd 窗口增加 1 个 TCP 段。每次收到 ACK 后,都可以多发送两个新的分组。TCP 连接的这个阶段通常被称为 “指数增长” 阶段,因为客户端和服务器都在向两者之间网络路径的有效带宽迅速靠拢。
![](https://i.loli.net/2019/06/20/5d0af8e0edfb940685.png)
为什么知道有个慢启动对我们构建浏览器应用这么重要呢?因为包括 HTTP 在内的很多应用层协议都运行在 TCP 之上,无论带宽多大,每个 TCP 连接都必须经过慢启动阶段。换句话说,我们不可能一上来就完全利用连接的最大带宽!
慢启动导致客户端与服务器之间经过几百 ms 才能达到接近最大速度的问题,对于大型流式下载服务的影响倒不显著,因为慢启动的时间可以分摊到整个传输周期内消化掉。
可是,对于很多 HTTP 连接,特别是一些短暂、突发的连接而言,常常会出现还没有达到最大窗口请求就被终止的情况。换句话说,很多 Web 应用的性能经常受到服务器与客户端之间往返时间的制约。因为慢启动限制了可用的吞吐量,而这对于小文件传输非常不利。
> 慢启动重启
>
> 除了调节新连接的传输速度,TCP 还实现了 SSR(Slow-Start Restart,慢启动重启)机制。这种机制会在连接空闲一定时间后重置连接的拥塞窗口。道理很简单,在连接空闲的同时,网络状况也可能发生了变化,为了避免拥塞,理应将拥塞窗口重置回 “安全的” 默认值。
>
> 毫无疑问,SSR 对于那些会出现突发空闲的长周期 TCP 连接(比如 HTTP 的 keep-alive 连接)有很大的影响。因此,我们建议在服务器上禁用 SSR。
##### 拥塞预防
认识到 TCP 调节性能主要依赖丢包反馈机制非常重要。换句话说,这不是一个假设命题,而是一个具体何时发生的命题。慢启动以保守的窗口初始化连接,随后的每次往返都会成倍提高传输的数据量,直到超过接收端的流量控制窗口,即系统配置的拥塞阈值窗口,或者有分组丢失为止,此时拥塞预防算法介入。
拥塞预防算法把丢包作为网络拥塞的标志,即路径中某个连接或路由器已经拥堵了,以至于必须采取删包措施。因此,必须调整窗口大小,以避免造成更多的包丢失,从而保证网络畅通。
重置拥塞窗口后,拥塞预防机制按照自己的算法来增大窗口以尽量避免丢包。某个时刻,可能又会有包丢失,于是这个过程再从头开始。如果你看过 TCP 连接的吞吐量跟踪曲线,发现该曲线呈锯齿状,到现在就该明白为什么了。这是拥塞控制和预防算法在调整拥塞窗口,进而消除网络中的丢包问题。
#### 队首阻塞
TCP 在不可靠的信道上实现了可靠的网络传输。基本的分组错误检测与纠正、按序交互、丢包重发,以及保证网络最高效率的流量控制、拥塞控制和预防机制,让 TCP 成为大多数网络应用中最常见的传输协议。
虽然 TCP 很流行,但它并不是唯一的选择,而且在某些情况下也不是最佳选择。特别是按序交互和可靠交互有时候并不必要,反而会导致额外的延迟,对性能造成负面影响。
要理解为什么,可以想一想,每个 TCP 分组都会带着一个唯一的序列号被发出,而所有分组必须按顺序传送到接收端(如下图)。如果中途有一个分组没能到达接收端,那么后续分组必须保存到接收端的 TCP 缓冲区,等待丢失的分组重发并到达接收端。这一切都发生在 TCP 层,应用程序对 TCP 重发和缓冲区中排队的分组一无所知,必须等待分组全部到达才能访问数据。在此之前,应用程序只能在通过套接字读数据时感觉到延迟交互。这种效应称为 TCP 的队首阻塞。
![](https://i.loli.net/2019/06/20/5d0b1e1f372ee20894.png)
队首阻塞造成的延迟可以让我们的应用程序不用关心重排和重组,从而让代码保持简洁。然后,代码简洁也要付出代价,那就是分组到达时间会存在无法预知的延迟变化。这个时间变化通常被称为抖动,也是影响应用程序性能的一个主要因素。
另外,有些应用程序可能并不需要可靠的交付或者不需要按顺序交付。比如,每个分组都是独立的消息,那么按顺序交付就没有任何必要。而且,如果每个消息都会覆盖之前的消息,那么可靠交付同样也没有必要了。可惜的是,TCP 不支持这种情况,所有分组必须按顺序交付。
无需按序交付数据或能够处理分组丢失的应用程序,以及对延迟或抖动要求很高的应用程序,最好选择 UDP 等协议。
>丢包就丢包
>
>事实上,丢包是让 TCP 达到最佳性能的关键。被删除的包恰恰是一种反馈机制,能够让接收端和发送端各自调整速度,以避免网络拥堵,同时保持延迟最短。另外,有些应用程序可以容忍丢失一定数量的包,比如语言和游戏状态通信,就不需要可靠传输或按序交付。
#### 针对 TCP 的优化建议
TCP 是一个自适应的,对所有网络节点一视同仁的、最大限制利用底层网络的协议。因此,优化 TCP 的最佳途径就是调整它感知当前网络状况的方式,根据它之上或之下的抽象层的类型和需求来改变它的行为。
不同应用程序需求间的复杂关系,以及每个 TCP 算法中的大量因素,使得 TCP 调优成为学术和商业研究的一个无底洞。本章只蜻蜓点水般地介绍了影响 TCP 性能的几个典型因素,而没有探讨的选择性应答、延迟应答、快速转发等,随便一个都能让你领略到 TCP 的复杂性,感受到理解、分析和调优之难。
尽管如此,而且每个算法和反馈机制的具体细节可能会继续发展,但核心原理以及它们的影响是不变的:
* TCP 三次握手增加了整整一次往返时间
* TCP 慢启动将被应用到每个新连接
* TCP 流量及拥塞控制会影响所有连接的吞吐量
* TCP 的吞吐量由当前拥塞窗口大小控制
结果,现代高速网络中 TCP 连接的数据传输速度,往往会受到接收端和发送端之间往返时间的限制。另外,尽管带宽不断增长,但延迟依旧受限于光速,而且已经限定在了其最大值的一个很小的常数因子之内。大多数情况下,TCP 的瓶颈都是延迟,而非带宽。
##### 服务器配置调优
一句话,让你的服务器跟上时代是优化发送端和接收端 TCP 栈的首要措施。
有了最新的内核,我们推荐你遵循如下最佳实践来配置自己的服务器。
1. 增大 TCP 的初始拥塞窗口
加大起始拥塞窗口可以让 TCP 在第一次往返就传输较多数据,而随后的速度提升也会很明显。对于突发性的短暂连接,这也是特别关键的一个优化。
2. 慢启动重启
在连接空闲时禁用慢启动可以改善瞬时发送数据的长 TCP 连接的性能。
3. 窗口缩放
启用窗口缩放可以增大最大接收窗口大小,可以让高延迟的连接达到更好吞吐量。
4. TCP 快速打开
在某些条件下,允许在第一个 SYN 分组中发送应用程序数据。TFO(TCP Fast Open,TCP 快速打开)是一种新的优化选项,需要客户端和服务器共同支持。为此,首先要搞清楚你的应用程序是否可以利用这个特性。
以上几个设置再加上最新的内核,可以确保最佳性能:每个 TCP 连接都会具有较低的延迟和较高的吞吐量。
##### 应用程序行为调优
调优 TCP 性能可以让服务器和客户端之间达到最大吞吐量和最小延迟。而应用程序如何使用新的或已经建立的 TCP 连接同样也有很大的关系。
* 在快也快不过什么也不用发送,能少发就少发
* 我们不能让数据传输的更快,但可以让它们传输的距离更短
* 重用 TCP 连接是提升性能的关键
当然,消除不必要的数据传输本身就是很大的优化。比如,减少下载不必要的资源,或者通过压缩算法把要发送的比特数降到最低。然后,通过在不同的地区部署服务器(比如,使用 CDN),把数据放到接近客户端的地方,可以减少网络往返的延迟,从而显著提升 TCP 性能。最后,尽可能重用已经建立的 TCP 连接,把慢启动和其他拥塞控制机制的影响降到最低。
##### 性能检查清单
优化 TCP 性能的回报是丰厚的,无论什么应用,性能提升可以在服务器的每个连接中体现出来。下面几条请大家务必记在自己的日程表里:
* 把服务器内核升级到最新版本
* 确保 cwnd 大小为 10
* 禁用空闲后的慢启动
* 确保启动窗口缩放
* 减少传输冗余数据
* 压缩要传输的数据
* 把服务器放到离用户近的地方以减少往返时间
* 尽最大可能重用已经建立的 TCP 连接