拥塞窗口,TCP的拥塞控制机制

为实现可靠传输,TCP 实现了接收确认机制:当数据发生丢包时,重传数据。当网络链路负载很重,甚至发生拥塞时,丢包就会很频繁。这时如果 TCP 还拼命地重传数据,将进一步压垮网络!

为了避免网络拥塞时压垮网络,TCP 还实现了 拥塞控制 机制,主要包含这几个算法:

  • 慢启动
  • 拥塞避免
  • 快速重传
  • 快速恢复

网络拥塞

每条网络线路都有一定的带宽上限,如果承载于其上的网络流量超过带宽,丢包就不可避免。这就是所谓的 网络拥塞network congestion ),一种持续过载的网络状态,网络传输质量下降。

如下图,主机 antapple 之间有段链路发生网络拥塞,两台主机间的通信必然受到影响:

如果主机不知道网络拥塞的发生,还拼命地发送数据,最终肯定会进一步压垮网络链路。

拥塞控制

我们希望 TCP 能够根据网络链路的实时状态,自动调整发送速度,这就是 TCP 协议的 拥塞控制congestion control )机制。拥塞控制机制细节很复杂,但原理却出奇的简单:

  • 维护拥塞窗口,限制数据发送量;
  • 根据网络当前状态,实时调节拥塞窗口:
    • 如果长时间未收到 ACK 而发生数据重传,说明网络可能拥塞,缩小拥塞窗口,降低发送速度;
    • 每收到一个有效 ACK 都增大拥塞窗口,提高发送速度,因为这通常意味着网络状态良好;

拥塞窗口

在流量控制一节,我们知道 TCP 通过 滑动窗口机制 来控制发送方的发送速度,以免打爆接收方的缓冲区。这个窗口也被称为 通告窗口 ,它规定了发送方此刻能够发送的最大数据量。

但滑动窗口只考虑接收方的缓冲区,对网络链路的拥塞程度则无能为力。因此,TCP 还维护了 拥塞窗口congestion window ),根据链路的拥塞程度来约束发送速度。

拥塞窗口跟滑动窗口类似,同样规定了发送方此刻能够发送出去的字节数,只不过它通过评估网络链路的拥塞程度,并由一定的算法计算而来的。

拥塞窗口可缩写为 cwnd ,通常以 TCP 报文段为单位,表示还能发送多少个段出去。由于 最大报文段大小 缩写为 MSS ,有时也会以 MSS 为单位来讨论。

当网络链路比较顺畅时,拥塞窗口逐步增大,发送速度也就逐步提升;当网络链路发生拥塞时,拥塞窗口快速减小,发送速度急踩刹车,避免进一步压垮网络。

拥塞窗口跟滑动窗口并不冲突,前者避免压垮网络链路;后者则保证不打爆接收缓冲区。发送方某一时刻能够发送的最大数据量由两者共同决定,以较小的那个为准。因此,发送方实际发送窗口大小 W 为:

$W = min(awnd, cwnd)$

其中,awnd 为通告窗口大小,而 cwnd 为拥塞窗口大小。本节,我们假设通告窗口 awnd 足够大,重点考察拥塞窗口 cwndTCP 发送速度的影响。

那么,TCP 如何选择合适的 cwnd 呢?由于 TCP 连接建立之初,对网络容量和运行负载均一无所知。因此,它只能通过学习探测出一个合适数值:先不断提高发送速度,直到出现丢包(网络拥塞)。

TCP 学习 cwnd 的算法可以总结为:慢启动、拥塞避免、快速重传和快速恢复。

慢启动

TCP 连接刚建立后,它对网络链路的运行状况仍一无所知。为避免网络拥塞时火上浇油,TCP 先执行 慢启动slow start )算法:将拥塞窗口 cwnd 初始化成最小的 1 个报文段,再随着数据 ACK 确认的达到慢慢扩张。

指数增长

在慢启动前期,TCP 每收到一个 ACK 确认,cwnd 就增加一个报文段。假设每个报文段都会回复 ACK 确认,在网络没有丢包的情况下,第一个分组被确认后,cwnd 就变成了 2 。

接下来,TCP 可以发送 2 个报文段。如果这 2 个报文段均顺利送到,可以收到 2ACK 确认,cwnd 增大到 4 。再往下 TCP 可以发送 4 个报文段,若顺利收到 4ACKcwnd 将增大至 8

以此类推,经过 $k$ 次往返后,拥塞窗口大小将达到 $cwnd=2^k$ 。因此,慢启动期间 cwnd 呈指数增长。指数型增长速度并不慢,所谓的慢启动其实是指拥塞窗口从一个较低的起点开始增长,而非一步到位。

接收方可能会开启 延迟确认delayed ACK ),每接收两个报文段才回复一个 ACK 确认。这样的话,慢启动拥塞窗口仍呈指数函数增长,只是速度稍慢一些,如上图灰色曲线所示。

随着拥塞窗口 cwnd 快速增长,最终将变得太大而拖累网络,出现丢包现象。这时 cwnd 必须充分降低,通常至少要降低一半。此外,TCP 还会结束慢启动,进入 拥塞避免congestion avoidance )阶段。

慢启动和拥塞避免阶段的切换点由拥塞窗口 cwnd慢启动阈值ssthresh )的相对关系决定:

  • $cwnd < ssthresh$ ,执行慢启动;
  • $cwnd > ssthresh$ ,执行拥塞避免;
  • $cwnd = ssthresh$ ,执行慢启动或拥塞避免均可(边界无关紧要);

拥塞避免

通过慢启动,TCP 快速摸清当前线路的容量情况,进入稳定状态。这时,拥塞窗口 cwnd 是否就不需要再调整了呢?

答案是否定的,因为网络随时可能释放出更多的可用容量。假设网络容量被一个突发流量占用,导致同一链路下的所有连接都出现丢包,从而进入拥塞避免阶段。当突发流量消失,其他连接应该充分利用这些释放的网络容量。

为了能够温和地找出额外的可用容量,不对网络产生新的冲击,TCP 实现了 拥塞避免congestion avoidance )算法。当拥塞窗口 cwnd 不低于慢启动阈值 ssthreshTCP 就执行该算法。

线性增长

在拥塞避免阶段,TCP 以更慢的速度扩张拥塞窗口,寻求额外容量。每当成功发送跟拥塞窗口大小等量的数据后,TCP 就给 cwnd 增加一个报文段。

假设当前拥塞窗口大小为 $k$ ,这时可以发出 $k$ 个报文段。当这 $k$ 报文段均顺利送达后,TCP 才给 cwnd 增加一个报文段。换言之,现在每收到一个 ACK 确认,cwnd 只增加 $\frac{1}{k}$ 个报文段。

举个例子,假设 TCP 进入拥塞避免阶段时拥塞窗口大小为 4 ,因此第一轮往返可以发出 4 个报文段。假设这 4 个段均成功送达并收到 ACK 确认,每个 ACK 确认为拥塞窗口增加 $\frac{1}{4}$ 个报文段,4ACK 刚好增加 1 个报文段。

这样一来,第二轮往返可以发出 5 个报文段。如果这 5 个报文段也顺利送达并收到 ACK 确认,cwnd 又增加 1 个报文段。因此,第三轮往返可以发出 6 个报文段,其他依次类推。

很显然,拥塞避免阶段 cwnd 呈线性增长趋势。这种相对温和的扩张策略,既能有效利用网络释放的额外容量,又不至于给网络带来较大波动,可谓一举两得。

最后,我们通过一个曲线图,来进一步学习慢启动和拥塞避免的关系:

这个图片来自其他网络资料,慢启动在里面叫做“慢开始”,不影响理解。此外,我不清楚该图片的版权,如有侵权,请联系我删除。

TCP 最开始执行慢启动,早期实现会一直增长到出现拥塞才会停止。由于指数增长速度很快,慢启动阶段后期会有大量数据报文进入网络,引起不必要的网络拥塞。

因此,新版 TCP 实现会设置初始的 ssthresh ,慢启动达到该值就进入拥塞避免阶段。换句话讲,在造成网络拥塞之前,慢启动就提前进入更温和的线性增长阶段,对网络更友好。

进入拥塞避免阶段后,窗口保持缓慢增长。但遇到网络拥塞时(比如发生超时②),TCP 会将 ssthresh 设为当前拥塞窗口 cwnd 的一半,重新开始慢启动过程。

当慢启动增长到新的 ssthresh 后,切换为拥塞避免③,开始缓慢第寻找新的平衡点。当又遇到偶发丢包时(比如重复ACK④),又将 ssthresh 设为当前窗口的一半,直接进入拥塞避免(快速恢复)。

因此,ssthresh 就像是一个动态的参考基准,指导 TCP 更好地测量网络的容量。TCP 检测到网络拥塞,说明最佳的窗口大小应该比当前窗口小。因此将 ssthresh 设为当前窗口的一半,可以更好地提供指导。

快速重传

我们知道,TCP 实现可靠传输依赖的是 超时重传 机制。TCP 在发送完数据后,会启动一个定时器。如果在定时器超时前没收到接收方发来的 ACK 确认(认为数据丢了),就重传数据。于此同时,TCP 还会执行拥塞控制机制,将拥塞窗口 cwnd 减半。

我们来考察这样一个例子:发送方同时发出 5 个包,假设 2 号包丢了,其他包都顺利送达。345 号包达到后,接收方应该发送确认,只是确认号仍是 1 号包。

换句话讲,发送方连续收到 1 号包的确认,说明 2 号包丢了。这时可以马上重传 2 号包及以后的数据,不用等待确认定时器超时,这就是所谓的 快速重传 算法。

收到重复的确认,说明发出去的数据发生丢包,意味着网络可能发生拥塞。TCP 同样会将 ssthresh 设为当前拥塞窗口 cwnd 的一半,并重新执行慢启动算法加以应对。

另外,TCP 为了提高传输效率,实现了 延迟确认 机制:收到数据时不会立即发送 ACK 确认,而是等一段时间,跟要发送的数据一起发出去。这样数据和 ACK 确认可共用一个分组报文,跟拼车一样更高效环保。

很显然,快速重传算法会受到捎带确认机制的制约。试想接收方刚好没有数据要发,因此 ACK 确认被延迟,乃至合并,发送方就不会检测到重复确认。

为了解决这个矛盾,TCP 规定接收方接到乱序数据后就立马发送 ACK 确认。回到前面的例子,接到 3 号包但 2 号仍未收到,这时就应该立马发送 ACK 确认,确认 1 号包。收到 45 号包时,也是类似的,这样发送方就能检查到重复确认。

相反,如果没有这个机制,接收方收到 1 号包时,由于刚好没有数据要发送,延迟发送 ACK 确认。后续接到 345 号包也是这样。最后等待一段时间还没有数据要发,TCP 才发送 ACK 确认,确认 1 号包。这样的话,发送方就不会收到重复确认,因此无法获悉 2 号包丢包的事实。

快速恢复

老版 TCP 实现一遇到丢包重传,就重新执行慢启动,发送速度一夜回到解放前。这种实现方式对 TCP 的发送效率影响很大,必须经历好几次往返,拥塞窗口 cwnd 才能恢复到原来的一半。

有时遇到的是偶发性丢包,网络不一定发生拥塞,这时执行慢启动是没有必要的。快速重传中收到重复确认的场景就是典型例子,虽然有包丢失了,但其后的包能正常送达说明网络应该还通畅。

因此,新版 TCPReno )选择跳过慢启动,直接进入拥塞避免阶段,这就是所谓的 快速恢复

  • ssthresh 设为当前窗口的一半;
  • 将当前窗口设为 ssthresh
  • 执行拥塞避免,窗口大小线性扩张;

您可能会有疑问,如果网络真的出现拥塞,最佳的窗口大小在新的 ssthresh 下方怎么办呢?其实并无大碍,TCP 马上又会检测到网络拥塞,窗口和 ssthresh 又立马减半了。

实际上,在 TCP 检测到网络拥塞时,窗口和 ssthresh 都是立马减半,因而被称为 乘法减小 或者 指数退避 。每次降低一半,这种下降速度其实也是很快的。

小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注:

【小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注: