拥塞处理主要涉及到下面这几个算法:

  • 慢启动(Slow Start)
  • 拥塞避免(Congestion Avoidance)
  • 快速重传(Fast Retransmit)和快速恢复(Fast Recovery)

为了实现上面的算法,TCP 的每条连接都有两个核心状态值:

  • 拥塞窗口(Congestion Window,cwnd)
  • 慢启动阈值(Slow Start Threshold,ssthresh)

拥塞窗口(Congestion Window,cwnd)

拥塞窗口指的是在收到对端 ACK 之前自己还能传输的最大 MSS 段数。
**
它与前面介绍的接收窗口(rwnd)有什么区别呢?

  • 接收窗口(rwnd)是接收端的限制,是接收端还能接收的数据量大小
  • 拥塞窗口(cwnd)是发送端的限制,是发送端在还未收到对端 ACK 之前还能发送的数据量大小

拥塞窗口初始值等于操作系统的一个变量 initcwnd,最新的 linux 系统 initcwnd 默认值等于 10。

拥塞窗口与前面介绍的发送窗口(Send Window)又有什么关系呢?

  • 真正的发送窗口大小 = 「接收端接收窗口大小」 与 「发送端自己拥塞窗口大小」 两者的最小值

拥塞控制的算法的本质是控制拥塞窗口(cwnd)的变化。

拥塞处理算法一:慢启动

每个 TCP 连接都有一个拥塞窗口的限制,最初这个值很小,随着时间的推移,每次发送的数据量如果在不丢包的情况下,“慢慢”的递增,这种机制被称为「慢启动」

算法的过程如下:

  1. 三次握手以后,双方通过 ACK 告诉了对方自己的接收窗口(rwnd)的大小,之后就可以互相发数据了
  2. 通信双方各自初始化自己的「拥塞窗口」(Congestion Window,cwnd)大小。
  3. cwnd 初始值较小时,每收到一个 ACK,cwnd + 1,每经过一个 RTT,cwnd 变为之前的两倍。

image.png

公式解释:

cwnd 的单元是 MSS, 在一个 RTT 内可能发出了 cwndMSS 大小的数据, 那么也会收到 cwnd 个 ACK. 最后其实就是 cwnd = cwnd2.

在初始拥塞窗口为 10 的情况下,拥塞窗口随时间的变化关系如下图:

image.png

因此可以得到拥塞窗口达到 N 所花费的时间公式为:

image.png

cwndN = cwnd0 * 2^(n-1), 1<= n

慢启动阈值(Slow Start Threshold,ssthresh)

慢启动拥塞窗口(cwnd)肯定不能无止境的指数级增长下去,否则拥塞控制就变成了「拥塞失控」了,它的阈值称为「慢启动阈值」(Slow Start Threshold,ssthresh),这是文章开头介绍的拥塞控制的第二个核心状态值。ssthresh 就是一道刹车,让拥塞窗口别涨那么快。

  • 当 cwnd < ssthresh 时,拥塞窗口按指数级增长(慢启动)
  • 当 cwnd > ssthresh 时,拥塞窗口按线性增长(拥塞避免)

拥塞避免(Congestion Avoidance)

当 cwnd > ssthresh 时,拥塞窗口进入「拥塞避免」阶段,在这个阶段,每一个往返 RTT,拥塞窗口大约增加 1 个 MSS 大小,直到检测到拥塞为止。

image.png

与慢启动的区别在于:

  • 慢启动的做法是 RTT 时间内每收到一个 ACK,拥塞窗口 cwnd 就加 1,也就是每经过 1 个 RTT,cwnd 翻倍
  • 拥塞避免的做法保守的多,每经过一个RTT 才将拥塞窗口加 1,不管期间收到多少个 ACK

image.png

算法三:快速重传(Fast Retransmit)

快速重传的含义是:当接收端收到一个不按序到达的数据段时,TCP 立刻发送 1 个重复 ACK,而不用等有数据捎带确认,当发送端收到 3 个或以上重复 ACK,就意识到之前发的包可能丢了,于是马上进行重传,不用傻傻的等到重传定时器超时再重传。

image.png

选择确认(Selective Acknowledgment,SACK)

前面有一章讲过这个, 重复了.

这个有一个问题,发送 3、4、5 包收到的全部是 ACK=1001,快速重传解决了一个问题: 需要重传。因为除了 2 号包,3、4、5 包也有可能丢失,那到底是只重传数据包 2 还是重传 2、3、4、5 所有包呢?

聪明的网络协议设计者,想到了一个好办法

  • 收到 3 号包的时候在 ACK 包中告诉发送端:喂,小老弟,我目前收到的最大连续的包序号是 1000(ACK=1001),[1:1001]、[2001:3001] 区间的包我也收到了
  • 收到 4 号包的时候在 ACK 包中告诉发送端:喂,小老弟,我目前收到的最大连续的包序号是 1000(ACK=1001),[1:1001]、[2001:4001] 区间的包我也收到了
  • 收到 5 号包的时候在 ACK 包中告诉发送端:喂,小老弟,我目前收到的最大连续的包序号是 1000(ACK=1001),[1:1001]、[2001:5001] 区间的包我也收到了

这样发送端就清楚知道只用重传 2 号数据包就可以了,数据包 3、4、5已经确认无误被对端收到。这种方式被称为 SACK(Selective Acknowledgment)。

image.png

算法四:快速恢复

当收到三次重复 ACK 时,进入快速恢复阶段。解释为网络轻度拥塞。

  • 拥塞阈值 ssthresh 降低为 cwnd 的一半:ssthresh = cwnd / 2
  • 拥塞窗口 cwnd 设置为 ssthresh
  • 拥塞窗口线性增加

慢启动、快速恢复中的快慢是什么意思

刚开始学习这部内容的时候,有一个疑惑,明明慢启动拥塞窗口是成指数级增长,那还叫慢?快速恢复拥塞窗口增长的这么慢,还叫快速恢复?

我的理解是慢和快不是指的拥塞窗口增长的速度,而是指它们的初始值。慢启动初始值一般都很小,快速恢复的 cwnd 设置为 ssthresh

拥塞避免是一个很复杂的话题,有很多种算法:TCP Reno、TCP new Reno、TCP Vegas、TCP CUBIC等,这里不做太多的展开。

为什么初始化拥塞窗口 initcwnd 是 10

最初的 TCP 初始拥塞窗口值为 3 或者 4,大于 4KB 左右,如今常见的 web 服务数据流都较短,比如一个页面只有 4k ~ 6k,在慢启动阶段,还没达到传输峰值,整个数据流就可能已经结束了。对于大文件传输,慢启动没有什么问题,慢启动造成的时延会被均摊到漫长的传输过程中。

根据 Google 的研究,90% 的 HTTP 请求数据都在 16KB 以内,约为 10 个 TCP 段。再大比如 16,在某些地区会出现明显的丢包,因此 10 是一个比较合理的值。