4.3 慢启动(slow start)--- TCP Tahoe
Last updated
Last updated
介绍的加法递增机制较为保守,如果网络当前比较空闲,那需要很多时间才能达到一个较大的CongestionWindow,进而达到一个较大的发送速率,这明显是不合理的。所以直观上来说,加法递增机制只在 TCP 发送端的发送速率已经接近网络的容量极限时是合理的。因此,TCP提供了另一种机制,名字有点反直觉,叫做慢启动:slow start。慢启动会指数级增加拥塞窗口的大小,而不是线性的。通常,慢启动用在新建的TCP连接中,以快速的增加拥塞窗口的大小。
慢启动的具体流程如下:
TCP的发送端最开始会设置CongestionWindow
为1个packet,并发送一个packet并等待ACK。
当这个packet的ACK被收到时,TCP会向CongestionWindow
增加1个packet。
此时CongestionWindow
大小为2个packet,因此TCP会发送2个packet并等待它们的ACK。
在收到对应的2个ACK之后,TCP会向CongestionWindow
再增加2个packet。
此时CongestionWindow
大小为4个packet,因此TCP会发送4个packet并等待它们的ACK。
最后的结果是,每次RTT之后,TCP都会让CongestionWindow
翻倍,对应的就是允许在网络中传输的packet数量也翻倍了。图24显示了在慢启动过程中,网络中传输的packet数量的增长。可以与图22中展示的加法递增机制中线性增长进行对比。
一个有趣的问题是,为什么这里的这么快的指数变化会被称为“慢”启动?答案是要将其与最初的TCP行为进行对比,而不是与上一节中的加法递增进行对比。
假设一个TCP连接刚刚建立,发送端正准备要发送packet,也就是说当前没有任何一个packet正在传输的过程中。在最初的TCP版本中,TCP发送端会一次发送AdvertisedWindow
数量的packet出去。即使此时网络的带宽是足够的,路由器也有可能处理不了这里的瞬时突发流量。至于路由器是否会丢包,完全取决于路由器的buffer空间有多大。正是因为这个原因,慢启动被设计出来使得数据包的发送变得分散,这样就不会出现这种瞬时突发流量。因此,即使慢启动比上一节中的加法递增要快,但是也比一次发送整个AdvertisedWindow
要“慢”的多。
慢启动会工作在两种情况下:
当一个连接刚刚建立时,TCP的发送端并不知道它能在网络中传输多少个packet。(当今的TCP运行在各个地方,从1Mbps的线路到40Gbps的线路,所以TCP发送端不可能知道网络的容量)。此时,慢启动会在每个RTT之后翻倍CongestionWindow
,直到超时丢包发生,再通过乘法递减机制将CongestionWindow
减半。
第二种情况发生在TCP因为等待超时而导致连接中断了。当发生丢包时,由于超时时间足够长,TCP发送端最终会将CongestionWindow
对应的数据(注,严格来说应该是EffectiveWindow
对应的数据)发送完,然后等待之前某个segment的ACK。但是由于丢包了,这个ACK永远也不会到来。最终达到了超时时间,TCP会重传segment,如果这个segment传输成功,接收端会返回一个累积的ACK(注,即确认segment以及其后面所有的CongestionWindow
大小的数据)。这相当于TCP的拥塞窗口被重新打开了,而此时,网络中也没有这个TCP连接的packet在传输。如果不做任何事情,TCP发送端会瞬间将整个CongestionWindow
的数据发送到网络中,从而可能导致路由器的瞬时突发流量。所以,当发生超时重传时,TCP发送端会,且也应该使用慢启动来恢复其CongestionWindow
。
在第二种情况下,尽管TCP发送端使用了慢启动,但是相比第一种情况中TCP连接刚刚建立时,TCP发送端知道了更多的信息。例如,TCP发送端已经有了一个丢包前的CongestionWindow
,至少在丢包前,这个值是有用的。基于这个信息,TCP会采用以下策略:
当发生超时丢包时,CongestionWindow
被设置为1个packet
之后每收到一个ACK,CongestionWindow
就增加一个packet(注,对应慢启动过程)
当CongestionWindow
达到了丢包前拥塞窗口大小的一半之后,每个RTT才增加一个packet(注,对应加法递增),这里将丢包前拥塞窗口大小的一半称为CongestionThreshold
在发生超时丢包时,TCP 发送端在收到 ACK 之后,会按照下面的代码来增加拥塞窗口的大小(注,也就是根据CongestionThreshold
来区分究竟是使用慢启动,还是使用加法递增):
上面代码中的 state,就是保存了一个特定的 TCP 连接状态的结构体。
图 25 展示了 TCP 的CongestionWindow
如何随着时间增加和减少,并展示了慢启动和加法递增/乘法递减。图中的数据来自一个真实的 TCP 连接。
这个图里,最开始就是由于慢启动迅速增加的拥塞窗口大小。慢启动一直持续直到 0.4 秒左右,之后部分包被丢弃了(为什么会有这么多包在慢启动过程中丢弃,后面会有介绍)。
之后CongestionWindow
一直保持在 34KB。拥塞窗口一直保持不变是因为是因为发生了丢包,从而导致一直没有收到 ACK。因为没有收到ACK,所以拥塞窗口一直没有更新。实际上,这段时间也没有发送新的 packet,图中上方的小短竖线一直为空。
随后在大约 2 秒的位置,终于发生了超时。此时设置 CongestionThreshold
为 CongestionWindow/2
(大概 17KB)。设置CongestionWindow
为 1 个 packet,并重新开始慢启动。
并没有足够的细节来说明为什么 2 秒之后有两个丢包,所以我们直接跳过,来看 2-4 秒之间拥塞窗口的线性增长。这对应了加法递增(注,其实不太明显,应该在2.8秒到4秒之间)。
在大概 4 秒的位置,再次因为丢包,导致不能收到ACK,CongestionWindow
变平了。值得注意的是,尽管没有收到ACK,TCP在之后还是发送了一些数据出去,这是因为当前的EffectiveWindow
还未完全关闭。
在 5.5 秒的位置,超时又发生了。这使得CongestionThreshold
被设置为当前CongestionWindow
的一半,也就是 11KB。而CongestionWindow
被设置为 1 个 packet,重新开始慢启动。
慢启动指数级增长CongestionWindow
直到它达到了CongestionThreshold
。
随后CongestionWindow
线性增长(注,对应加法递增)。
同样的模型在大概第 8 秒时又重复了一次。
接下来我们看一下,为什么最初的慢启动中会有这么多丢包?在最初的慢启动中,TCP 会尝试了解网络中的可用带宽是多少。这是一个很困难的任务。如果 TCP 的发送端在这个阶段不够激进,例如只是线性的增加拥塞窗口,那需要花费太多的时间才能发现可用的带宽。这会极大的影响当前 TCP 连接的吞吐。另一方面,如果 TCP 发送端在这个阶段采用激进策略,例如指数级增加窗口,那么发送端有可能会在网络中丢失一半窗口大小的 packet。
为什么会这样呢?假设发送端刚刚成功发送了 16 个 packet,接下来它的拥塞窗口要增加到 32。然而,网络的容量碰巧只能支持 16 个 packet,接下来发送的 32 个 packet,极有可能会被网络丢弃掉 16 个(这是最坏的情况,因为部分 packet 会被路由器缓存而最后成功发送)。当带宽延时乘积增加时,这里的问题会变得更加严重。例如,带宽延时乘积为 1.8MB 时,每个TCP连接在最开始的慢启动都有可能丢失最多 1.8MB的数据。
除了慢启动,还有一些其他的研究使得TCP 发送端可以预估可用的带宽。其中一个例子是快启动(quick-start)。它的核心思想是,在 SYN packet 的 IP option 中添加请求速率。网络沿途的路由器可以检查这个 IP option,评估这条流对应链路的拥塞程度,并决定(1)这个速率是否可被接受(2)还是需要一个更低的速率(3)还是只能使用标准的慢启动。当 SYN 到达 TCP 接收端时,它:
要么会包含一个被沿途所有路由器接受的速率,稍后 TCP 发送端会以这个速率开始传输
要么会包含一个标识表明沿途的一个或者多个路由器不支持快启动,稍后 TCP 发送端回退到使用标准的慢启动
如果 TCP 可以从一个更高的速率开始传输,那么一个 TCP 会话可以更快的填满网络的通道,而不是在等待多次 RTT 时间之后才填满。
很明显,快启动的一个挑战是,它需要路由器的支持。只要网络路径上一个路由器不支持快启动,那么整个 TCP 连接就会使用标准的慢启动。因此,这一类的增强还需要很长的时间才能在互联网上铺开。目前来说,它还是更多的用在受控网络中(例如,实验室网络中)。