前言
近年来,随着信息技术的不断发展,各行各业也掀起了信息化浪潮,为了留住用户和吸引用户,各个企业力求为用户提供更好的信息服务,这也导致WEB性能优化成为了一个热点。据分析,网站速度越快,用户的黏性、忠诚度、转化率等也越高。对网络通信有决定性影响的因素有延时和带宽,延时有传播延时、传输延时、处理延时和排队延时构成。对于日常网站浏览来说,延时要比带宽对性能影响更大,因为一个网站需要的资源往往是由很多小文件构成,需要多次请求才能完成,其处理延时、排队延时更大。不同的网络协议具有不同的信息传递方式,也就会产生不同的延时,理解这些协议的核心原理,就为优化web体验提供了思路和途径。本文主要探讨网络协议中的TCP协议,分析其内在原理及机制。
TCP
因特网有两个核心协议:IP和TCP。IP(Internet Protocol)负责联网主机之间的路由选择和寻址;TCP(Transmission Control Protocol)负责在不可靠的传输信道之上提供可靠的抽象层。TCP向应用层隐藏了大多数网络通信的复杂细节,比如丢包重发、按序发送、拥塞控制、数据完整等。正如任何一种技术它越完善和强大,其性能损耗也就越大,TCP为了给信息传输提供可靠的方式做了很多工作,也就造成了一定的性能瓶颈,下面就具体的讲解一下TCP的原理及思想。
一个完整的TCP数据包的结构如下所示:
1.连接建立和断开
所有TCP连接的建立都要进行三次握手,因为在交换应用数据之前,客户端和服务器需要约定分组序列号及协商其他相关信息。出于安全考虑,序列号由两端随机生成。三次握手过程如下图:
TCP快速打开:在某些条件下,允许第一个SYN分组中带有应用程序数据。这就使得在传输小文件时,减少了新建TCP连接带来的性能损失,因为它在第一次发送分组时携带了应用数据,可以一定程度上降低HTPP事物网络延时。
虽然建立一个连接需要三次握手,而断开一个连接需要四次握手才可以完成,这是由TCP的半关闭造成的。其大致过程入下所示:
(1) 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
(2) 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。
注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
(3) 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
(4) 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。
2.拥塞预防及控制
TCP在不可靠的网络环境上提供了可靠的信息传输方式,为此它加入了很多机制,如流量控制、拥塞控制和拥塞预防等。
-
1)流量控制
流量控制是一种预防发送过多向接收端发送数据的机制。当接收端收到过多数据时,会因为忙碌、负载重或缓冲区既定而无法处理。为了完成流量控制,TCP连接的每一方都要通告自己的接收窗口(rwnd),其中包含能够保存数据的缓冲区空间大小信息。在第一次建立连接时,客户端和服务端都会使用默认的rwnd值来发送。在数据上传时,服务端的接收窗口会成为制约因素,在数据下载时,客户端的接收窗口会成为制约因素。如果某一端跟不上数据传输,那它可以发送一个更小的rwnd通知对方,如果rwnd为0,则意味着应用层只有清空缓冲区才能继续接收数据。这个流量控制过程贯穿于每个TCP生命周期中,每个ACK分组都会携带相应的rwnd值,以便两端动态调整数据流,使其适应发送端和接收端的容量和处理能力。
窗口缩放:最初TCP规范分配给窗口大小的字段是16位的(可表示0-65535字节),在某些情况下该设置会限制网络达到最优性能。为此,RFC 1323提供了窗口缩放选项,可以把接收窗口大小从65535字节提高到1G字节,其在三次握手期间完成。
-
2)慢启动
流量控制机制可以防止发送端发送过量数据给接收端,但是并不可能有效的防止任何一端向潜在的网络发送过多的数据。也就是说,在TCP连接建立之初,两端都不知道网络中可用带宽大小,因此需要一种机制来评估它,并能动态的调整发送速度以使其适应不断变化的网络条件。
举个例子来说明这种调节机制的价值,想象一下你在家里,正在播放一个在线视频流,服务器以你家里最大的下行带宽下发数据,确保您的最佳体验。这时,家庭网络上的另一个用户打开一个新的连接,下载一些软件更新。突然之间,分配给视频流可用的下行链路带宽下降很多,在这种情况下,视频服务器必须调整其下发数据速率 - 如果它继续以之前的速率,数据将堆积在中间的某个节点上,数据包将被丢弃,网络使用效率极其低下。
在经过三次握手建立TCP连接之后,在此时根据两端的交换数据情况来预测网络带宽是唯一方法,这就是慢启动的设计思路。开始时,服务端为每一个新的TCP连接初始化一个拥塞窗口(cwnd--发送端对从客户端接收ACK之前可发送的最大数据报文段长度)值,cwnd的初始值一般是一个系统的缺省值(在Linux下是 initcwnd变量)。cwnd值是发送端自己保有的,这个值也不会在两端间传递,cwnd值经过专门算法根据数据交换情况进行计算,网络中最大可以传输的数据量取rwnd与cwnd的最小值。
如何通告专门的算法来预测每个连接窗口的大小呢?那就是慢启动。
其核心思想,从一个较小的cwnd开始,每次发送cwnd个数据包,在接收到cwnd个ACK信息后,就翻倍的扩大cwnd值,进而使得cwnd值随信息往返次数指数级增长(cwnd=Initcwnd*2^n),不断靠近网络实际传输带宽。下面是一个计算达到目标吞吐量所需时间的函数:
时间=往返时间*[log2(N/Initcwnd)].
从上公式可得初始cwnd值越大,到达拥塞窗口的时间越小,进而可以增加信息传输速度。由于慢启动使得刚开始信息传输速度较慢,达到最优状态需要经历一个速度爬坡过程,有时候需要几百毫秒甚至数秒才能接近最大速度。这对于大型流式下载服务的影响倒不明显,可以把慢启动时间分摊到整个传输周期,而对于一些短暂的、突发的连接而言,尚未达到最大传输速度就终止了,这就限制了可用的吞吐量,不利于小文件传输。
慢启动重启:除了调节新TCP连接的传输速率,TCP协议还实现了慢启动的重新启动机制(SSR),当一个连接空闲了一段时间后,将重置拥塞窗口(cwnd)。理由很简单:当连接闲置的时候,网络条件可能已经改变,为避免拥塞,拥塞窗口复位到一个“安全”的默认值。由于SSR会对突然空闲的长周期TCP连接进行cwnd重置,而当再次需要使用这些连接传输数据时,又需要重新从初始值开始进行慢启动,增加了信息传输时间。
下面是一个慢启动示例,具体技术参数为:往返时间为56ms,带宽为5Mbit/s,两端rwnd都为65535字节,cwnd初始值为4,服务器响应时间40ms,没有分组丢失、每个分组都要确认、GET请求只占一段。
由上图可知,由于慢启动的存在,一个新的TCP连接传输20KB的数据并不能一下达到最大速度,需要经过一个慢启动,使得信息传输需要更多的时间。如果我们把这20KB的数据重用这个TCP连接传输,就不需要再次经过慢启动过程了,直接一次发送15个TCP段就可以完成传输,可以极大的减小传输时间。
-
3)拥塞预防
TCP设计时将丢包作为一个性能的反馈,并用其来调节网速。换言之,它不是假设要发生丢包,而是当丢包发生时,会去调整。慢启动初始化一个保守的拥塞窗口大小(cwnd),在后面,每一个成功的确认,成倍的数据被发送,直到它超过接收端的流量控制窗口大小(rwnd:系统配置的拥塞阈值窗口),或者当丢失了一个数据包,拥塞预防算法将开始起作用。当发生丢包事件时,需要调整cwnd值来重新保证信息可靠传输。拥塞预防算法将丢包作为网络拥塞的标志,在重置cwnd后,它根据自己的算法来增大cwnd值以尽快恢复传输速度,当网络再次拥塞时以至于丢包时,继续重置cwnd,使用拥塞预防算法调整cwnd值。最后,值得注意的是,改善拥塞控制和拥塞避免的算法在学术研究和商业产品中都是一个活跃的领域,不同的网络类型,不同类型的数据传输,有不同的优化算法。今天,在不同平台上,可能运行着许多变种的算法:TCP Tahoe and Reno(原始实现),TCP Vegas,TCP New Reno,TCP BIC,TCPCUBIC(Linux上的默认实现),或 Compound TCP(Windows的默认实现),还有其他很多种实现方案。然而,无论如何实现,都是为了解决拥塞控制和拥塞问题。一旦发生了丢包,我们需要复位拥塞窗口,但如何以最佳的方式恢复拥塞窗口是一个简单的挑战:如果你是过于激进,那么间歇性的丢包将显著影响整个连接的吞吐量,如果你调整的速度不够快,那么你可能丢失更多的包。
快速恢复算法(PRR)
本来,TCP用“加增乘减”(AIMD)算法:当发生丢包,拥塞窗口大小减半,然后慢慢增加窗口大小。然而,在许多情况下,AIMD算法是过于保守,因此新的算法被开发。
PRR是RFC 6937中指定的一种新的算法,其目标是当一个数据包丢失时,提高恢复速度。他到底优化了多少呢?根据谷歌开发新算法进行测量,它可以减少3-10%的延迟。PRR现在是Linux 3.2 +内核默认的拥塞避免算法 - 这也是一个很好的理由去升级你的服务器!
3.选择性应答
TCP通信时,如果发送序列中间某个数据包丢失,TCP会通过重传最后确认的包开始的后续包,这样原先已经正确传输的包也可能重复发送,急剧降低了TCP性能。为改善这种情况,发展出SACK(Selective Acknowledgment, 选择性确认)技术,使TCP只重新发送丢失的包,不用发送后续所有的包,而且提供相应机制使接收方能告诉发送方哪些数据丢失,哪些数据重发了,哪些数据已经提前收到等。SACK是TCP中的一个可选特性,要想启用需要自己打开设置。
-
1)SACK的产生
SACK通常都是由TCP接收方产生的,在TCP握手时如果接收到对方的SACK允许选项同时自己也支持SACK的话,在接收异常时就可以发送SACK包通知发送方。
如果TCP接收方接收到非期待序列号的数据块时,如果该块的序列号小于期待的序列号,说明是网络复制或重发的包,可以丢弃;如果收到的数据块序列号大于期待的序列号,说明中间包被丢弃或延迟,此时可以发送SACK通知发送方出现了网络丢包。
为反映接收方的接收缓存和网络传输情况,SACK中的第一个块必须描述是那个数据块激发此SACK选项的,接收方应该尽可能地在SACK选项部分中填写尽可能多的块信息,即使空间有限不能全部写完,SACK选项中要报告最近接收的不连续数据块,让发送方能了解当前网络传输情况的最新信息。
-
2)对重发包的SACK(D-SACK)
RFC2883中对SACK进行了扩展,在SACK中描述的是收到的数据段,这些数据段可以是正常的,也可能是重复发送的,SACK字段具有描述重复发送的数据段的能力,在第一块SACK数据中描述重复接收的不连续数据块的序列号参数,其他SACK数据则描述其他正常接收到的不连续数据,因此第一块SACK描述的序列号会比后面的SACK描述的序列号大;而在接收到不完整的数据段的情况下,SACK范围甚至可能小于当前的ACK值。通过这种方法,发送方可以更仔细判断出当前网络的传输情况,可以发现数据段被网络复制、错误重传、ACK丢失引起的重传、重传超时等异常的网络状况。
-
3)发送方对SACK的响应
TCP发送方都应该维护一个未确认的重发送数据队列,数据未被确认前是不能释放的,这个从重发送队列中的每个数据块都有一个标志位“SACKed”标识是否该块被SACK过,对于已经被SACK过的块,在重新发送数据时将被跳过。发送方接收到接收方SACK信息后,根据SACK中数据标志重发送队列中相应的数据块的“SACKed”标志,但如果接收不到接收方数据,超时后,所有重发送队列中数据块的SACKed位都要清除,因为可能接收方已经出现了异常。
通过SACK选项可以使TCP发送方只发送丢失的数据而不用发送后续全部数据,提高了数据的传输效率。
上述只是TCP协议中影响性能的一些典型因素,其他还有延迟应答、快速转发等也会影响网络性能。
4.TCP的不足--队首阻塞
TCP在不可靠的信道上进行可靠地传输,隐藏了很多网络底层的细节,极大的方便了工程人员的使用。任何事物都有其利弊,TCP也不例外,在某些场景下也是有极大限制的,
由于TCP是有序交付的,每个分组都会有一个唯一的序列号,需要按照顺序传递到接收端,接收端也需要接受到所有分组后才能处理所受到的信息。如果中间某一个分组没能被接收到,其他分组需要存储在缓冲区,需要等待接收到重发的分组后才能处理。TCP的按序交付和分组重排使得应用程序不再关心底层细节,使得开发更为方便。但在实时性要求较高的场景下,可能有些应用对有序交付和可靠性要求并不高,使用TCP就会造成很大的延时。而且由于分组到达接收端会存在一定的不确定性,使得传输时间也变得不确定,这个时间变化称之为抖动。
能够容忍和自行处理包的乱序或者丢包的应用,或者对延迟和抖动敏感的应用,可以考虑另外一个选择:UDP。关于UDP将在下一篇文章中探讨。