42
TCP拥塞控制机制之慢启动

尽管TCP有了流量控制机制,但网络拥塞崩溃仍然在1980年代中后期浮出水面。流量控制确实可以防止发送端向接收端过多发送数据,但却没有机制预防任何一端向潜在网络过多发送数据。换句话说,发送端和接收端在连接建立之初,谁也不知道可用带宽是多少,因此需要一个估算机制,然后还要根据网络中不断变化的条件 而动态改变速度。

要说明这种动态适应机制的好处,可以想象你在家里观看一个大型的流视频。视频服务器会尽最大努力根据你的下行连接提供最高品质信息。而此时,你家里又有人打开一个新连接下载某个软件的升级包。可供视频流使用的下行带宽一下子少了很多,视频服务器必须调整它的发送速度。否则,如果继续保持同样的速度,那么数据很快就会在某个中间的网关越积越多,最终会导致分组被删除,从而降低网络传输效率。

1988年,Van Jacobson和Michael J.Karels撰文描述了解决这个问题的几种算法:慢启动、拥塞预防、快速重发和快速恢复。这4种算法很快被写进了TCP规范。事实上,正是由于这几种算法加入TCP,才让因特网在20世纪80年代末到90年代初流量暴增时免于大崩溃。要理解慢启动,最好看一个例子。同样,假设纽约有一个客户端,尝试从位于伦敦的服务器上取得一个文件。

首先,三次握手,而且在此期间双方各自通过ACK分组通告自己的接收窗口(rwnd)大小。在发送完最后一次ACK分组后,就可以交换应用数据了。此时,根据交换数据来估算客户端与服务器之间的可用带宽是唯一的方法,而且这也是慢启动算法的设计思路。首先,服务器通过TCP连接初始化一个新的拥塞窗口(cwnd)变量,将其值设置为一个系统设定的保守值(在Linux中就是initcwnd)。

发送端对从客户端接收确认(ACK)之前可以发送数据量的限制。发送端不会通告cwnd变量,即发送端和接收端不会交换这个值。此时,位于伦敦的服务器只是维护这么一个私有变量。此时又有一条新规则,即客户端与服务器之间最大可以传输(未经ACK确认的)数据量取rwnd和cwnd变量中的最小值。那服务器和客户端怎么确定拥塞窗口大小的最优值呢?毕竟,网络状况随时都在变化,即使相同的两个网络节点之间也一样(前面的例子已经展示了这一点)。如果能通过算法来确定每个连接的窗口大小,而不用手工调整就最好了。

解决方案就是慢启动,即在分组被确认后增大窗口大小,慢慢地启动!最初,cwnd的值只有1个TCP段。1999年4月,RFC2581将其增加到了4个TCP段。2013年4月,RFC6928再次将其提高到10个TCP段。新TCP连接传输的最大数据量取rwnd和cwnd中的最小值,而服务器实际上可以向客户端发送4个TCP段,然后就必须停下来等待确认。

此后,每收到一个ACK,慢启动算法就会告诉服务器可以将它的cwnd窗口增加1个TCP段。每次收到ACK后,都可以多发送两个新的分组。TCP连接的这个阶段通常被称为“指数增长”阶段,因为客户端和服务器都在向两者之间网络路径的有效带宽迅速靠拢。

blob.png

为什么知道有个慢启动对我们构建浏览器应用这么重要呢?因为包括HTTP在内的很多应用层协议都运行在TCP之上,无论带宽多大,每个TCP连接都必须经过慢启动阶段。换句话说,我们不可能一上来就完全利用连接的最大带宽!相反,我们要从一个相对较小的拥塞窗口开始,每次往返都令其翻倍(指数式增长)。而达到某个目标吞吐量所需的时间,就是客户端与服务器之间的往返时间和初始拥塞窗口大小的函数。

blob.png

下面我们就来看一个例子,假设客户端和服务器的接收窗口为65535字节(64KB),而初始的拥塞窗口为4段(RFC2581),那么伦敦到纽约往返时间是56ms。

这里及后面例子中的初始拥塞窗口都会使用原来(RFC 2581规定)的4段,因为这仍然是目前大多数服务器中常见的值。当然,你肯定不会犯这种错误的,对吧?接下来的例子能很好地说明为什么你该更新内核了。先不管64KB的接收窗口,新TCP连接的吞吐量一开始是受拥塞窗口初始值限制的。计算可知,要达到64KB的限制,需要把拥塞窗口大小增加到45段,而这需要224ms:

blob.png

要达到客户端与服务器之间64KB的吞吐量,需要4次往返,几百毫秒的延迟!至于客户端与服务器之间实际的连接速率是不是在Mbit/s级别,丝毫不影响这个结果。这就是慢启动。

blob.png


为减少增长到拥塞窗口的时间,可以减少客户端与服务器之间的往返时间。比如,把服务器部署到地理上靠近客户端的地方。要么,就把初始拥塞窗口大小增加到RFC 9828规定的10段。

慢启动导致客户端与服务器之间经过几百毫秒才能达到接近最大速度的问题,对于大型流式下载服务的影响倒不显著,因为慢启动的时间可以分摊到整个传输周期内消化掉。可是,对于很多HTTP连接,特别是一些短暂、突发的连接而言,常常会出现还没有达到最大窗口请求就被终止的情况。换句话说,很多Web应用的性能经常受到服务器与客户端之间往返时间的制约。因为慢启动限制了可用的吞吐量,而这对于小文件传输非常不利。慢启动重启除了调节新连接的传输速度,TCP还实现了SSR(Slow-StartRestart,慢启动重启)机制。

这种机制会在连接空闲一定时间后重置连接的拥塞窗口。道理很简单,在连接空闲的同时,网络状况也可能发生了变化,为了避免拥塞,理应将拥塞窗口重置回“安全的”默认值。毫无疑问,SSR对于那些会出现突发空闲的长周期TCP连接(比如HTTP的keep-alive连接)有很大的影响。因此,我们建议在服务器上禁用SSR。在Linux平台,可以通过如下命令来检查和禁用SSR:

$>sysctlnet.ipv4.tcp_slow_start_after_idle
$>sysctl-wnet.ipv4.tcp_slow_start_after_idle=0

为演示三次握手和慢启动对简单HTTP传输的影响,我们假设纽约的客户端需要通过TCP连接向伦敦的服务器请求一个20KB的文件,下面列出了连接的参数。

blob.png•往返时间:56ms。

•客户端到服务器的带宽:5Mbit/s。

•客户端和服务器接收窗口:65535字节。

•初始的拥塞窗口:4段(4×1460字节≈5.7KB)。

•服务器生成响应的处理时间:40ms。

•没有分组丢失、每个分组都要确认、GET请求只占1段。

•0ms:客户端发送SYN分组开始TCP握手。

•28ms:服务器响应SYN-ACK并指定其rwnd大小。

•56ms:客户端确认SYN-ACK,指定其rwnd大小,并立即发送HTTPGET请求。

•84ms:服务器收到HTTP请求。

•124ms:服务器生成20KB的响应,并发送4个TCP段(初始cwnd大小为4),然后等待ACK。

•152ms:客户端收到4个段,并分别发送ACK确认。

•180ms:服务器针对每个ACK递增cwnd,然后发送8个TCP段。

•208ms:客户端接收8个段,并分别发送ACK确认。

•236ms:服务器针对每个ACK递增cwnd,然后发送剩余的TCP段。

•264ms:客户端收到剩余的TCP段,并分别发送ACK确认。

大家可以练习一下,如果将cwnd值设置为10个TCP段,那么图2-5所示的过程将减少一次往返,性能可以提升22%!通过新TCP连接在往返时间为56ms的客户端与服务器间传输一个20KB的文件需要264ms!作为对比,现在假设客户端可以重用同一个TCP连接(图2-6),再发送一次相同的请求。

•0ms:客户端发送HTTP请求。

•28ms:服务器收到HTTP请求。

•68ms:服务器生成20KB响应,但cwnd已经大于发送文件所需的15段了,因此一次性发送所有数据段。

•96ms:客户端收到所有15个段,分别发送ACK确认。

同一个连接、同样的请求,但没有三次握手和慢启动,只花了96ms,性能提升幅度达275%!

以上两种情况下,服务器和客户端之间的5Mbit/s带宽并不影响TCP连接的启动阶段。此时,延迟和拥塞窗口大小才是限制因素。事实上,如果增大往返时间,第一次和第二次请求的性能差距只会加大。大家可以练习一下,试试不同的往返时间会有什么结果。理解了TCP拥塞控制机制后,针对keep-alive、流水线和多路复用的优化就简单得多了。

增大TCP的初始拥塞窗口。把服务器的初始cwnd值增大到RFC6928新规定的10段(IW10),是提升用户体验以及所有TCP应用性能的最简单方式。好消息是,很多操作系统已经更新了内核,采用了增大后的值。可以留意相应的文档和发布说明。在Linux上,IW10是2.6.39以上版本内核的新默认值。但不要就此满足,升级到3.2以上版本还有其他重要更新。


这条帮助是否解决了您的问题? 已解决 未解决

提交成功!非常感谢您的反馈,我们会继续努力做到更好! 很抱歉未能解决您的疑问。我们已收到您的反馈意见,同时会及时作出反馈处理!