更新:我原来的答案同意解决方案,但经过仔细考虑,我认为解决方案是错误的。这个答案是从头开始重写的;请仔细阅读。我展示了为什么在时间 T=16 进入快速恢复以及为什么协议一直保留到 T=22。图表中的数据支持我的理论,所以我非常肯定解决方案是完全错误的。
让我们先明确一点:慢启动呈指数增长;拥塞避免线性增长,快速恢复线性增长,即使它使用与慢启动相同的公式来更新 的值cwnd
。
请允许我澄清一下。
为什么我们说慢启动cwnd
呈指数增长?
请注意,接收到的每个 ACKcwnd
都会增加MSS
字节数。
让我们看一个例子。假设cwnd
初始化为 1 个 MSS(MSS 的值通常为 1460 字节,因此在实践中这意味着cwnd
初始化为 1460)。此时,由于拥塞窗口大小只能容纳 1 个数据包,因此 TCP 将不会发送新数据,直到该数据包被确认。假设 ACK 没有丢失,这意味着大约每 RTT 秒传输一个新数据包(回想一下 RTT 是往返时间),因为我们需要 (1/2)*RTT 来发送数据包,并且 ( 1/2)*RTT 用于 ACK 到达。
因此,这导致发送速率大致为 MSS/RTT bps。现在,请记住,对于每个ACK
,cwnd
都会增加MSS
。因此,一旦第一个ACK
到达,cwnd
就变成2*MSS
,所以现在我们可以发送 2 个数据包。当这两个数据包被确认时,我们增加cwnd
两次,所以现在cwnd
是4*MSS
。伟大的!我们可以发送 4 个数据包。这 4 个数据包被确认,所以我们要增加cwnd
4 倍!所以我们有cwnd = 8*MSS
. 然后我们得到cwnd = 16*MSS
. 我们基本上cwnd
每 RTT 秒翻一番(这也解释了为什么cwnd = cwnd+MSS*(MSS/cwnd)
在拥塞避免中导致线性增长)
是的,这很棘手,这个公式cwnd = cwnd+MSS
很容易让我们相信它是线性的——这是一个常见的误解,因为人们经常忘记它适用于每个确认的数据包。
请注意,在现实世界中,传输 4 个数据包不一定会产生 4 个 ACK。它可能只生成 1 个ACK
,但由于 TCP 使用累积 ACK,因此该单个ACK
仍然确认 4 个数据包。
为什么快速恢复是线性的?
该cwnd = cwnd+MSS
公式适用于慢启动和拥塞避免。人们会认为这会导致两种状态都引发指数增长。但是,快速恢复在不同的上下文中应用该公式:接收到重复的 ACK 时。区别就在这里:在慢启动中,一个 RTT 确认了一大堆段,并且每个确认的段为 的新值贡献了 +1MSS cwnd
,而在快速恢复中,重复的 ACK 浪费了一个 RTT 来确认丢失了一个单个段,因此我们不是在cwnd
每个 RTT 秒内更新 N 次(其中 N 是传输的段数),而是为丢失的段更新cwnd
一次。所以我们只用一个段“浪费”了一次往返,所以我们只增加cwnd
1。
关于拥塞避免——我将在下面分析图表时解释这一点。
分析图表
好的,让我们逐轮看看该图中发生了什么。你的照片在某种程度上是正确的。让我先澄清一些事情:
- 当我们说慢启动和快速恢复呈指数增长时,这意味着它逐轮呈指数增长,正如您在图片中显示的那样。所以,这是正确的。您正确识别了带有蓝色圆圈的圆形:注意 1、2、4、8、16
cwnd
、...
- 你的图片好像是说Slow Start后,协议进入Fast Recovery。这不是发生的事情。如果它从慢启动进入快速恢复,我们会看到
cwnd
减半。这不是图表所显示的:cwnd
从 T=6 到 T=7 的值不会减少到一半。
好的,现在让我们看看每一轮到底发生了什么。请注意,图中的时间单位是一轮。因此,如果在时间 T=X 我们传输 N 个段,那么假设在时间 T=X+1 这 N 个段已被确认(当然,假设它们没有丢失)。
还要注意我们如何ssthresh
仅通过查看图表来判断 的值。在 T=6 时,cwnd
停止指数增长并开始线性增长,并且其值不会减少。从慢启动到另一个不涉及减少cwnd
的状态的唯一可能转换是到拥塞避免的转换,这发生在拥塞窗口大小等于 时ssthresh
。我们可以在图中看到,这发生在cwnd
32 时。因此,我们立即知道它ssthresh
被初始化为 32 MSS。这本书在第 276 页显示了一个非常相似的图表(图 3.53),其中作者得出了类似的结论:

在正常情况下,会发生这种情况——当 TCP 第一次从指数增长切换到线性增长而不减小窗口大小时,总是因为它达到阈值并切换到拥塞避免。
最后,假设它MSS
至少是 1460 字节(通常是 1460 字节,因为以太网的 MTU = 1500 字节,我们需要考虑 TCP + IP 标头的大小,它们总共需要 40 字节)。这对于查看何时cwnd
超过很重要ssthresh
,因为cwnd
' 的单位是MSS
并且ssthresh
以字节表示。
所以我们开始:
T = 1:
cwnd = 1 MSS;ssthresh = 32 kB
传输 1 段
T = 2
1 个段已确认
cwnd += 1; ssthresh = 32 kB
cwnd 的新值:2
传输 2 段
T = 3
2 段已确认
cwnd += 2; ssthresh = 32 kB
cwnd 的新值:4
传输4段
T = 4
4 段确认
cwnd += 4; ssthresh = 32 kB
cwnd 的新值:8
传输8段
T = 5
8 段确认
cwnd += 8; ssthresh = 32 kB
cwnd 的新值:16
传输 16 段
T = 6
16 段确认
cwnd += 16; ssthresh = 32 kB
cwnd 的新值:32
传输 32 段
好的,让我们看看现在会发生什么。cwnd
达到ssthresh
(32*1460 = 46720 字节,大于 32000)。是时候切换到拥塞避免了。注意 的值是如何cwnd
在轮次中呈指数增长的,因为每个确认的数据包都会为 的新值贡献 1 个 MSS cwnd
,并且每个发送的数据包都会在下一轮得到确认。
切换到拥塞避免
现在,cwnd
不会以指数方式增加,因为每个ACK
人都不会再贡献 1 个 MSS。相反,每个人都有ACK
贡献MSS*(MSS/cwnd)
。因此,例如,如果MSS
是 1460 字节并且cwnd
是 14600 字节(所以在每一轮开始时我们发送 10 个段),那么每个ACK
(假设ACK
每个段一个)将增加cwnd
MSS 1/10
(146 字节)。由于我们发送了 10 个段,并且在一轮结束时我们假设每个段都被确认,那么在一轮结束时我们增加cwnd
了10 * 1/10 = 1
. 换句话说,每个段只贡献一小部分,cwnd
因此我们cwnd
每轮只增加 1 个 MSS。所以现在每一轮都会递增cwnd
1 而不是传输/确认的段数。
我们将保持拥塞避免,直到检测到一些丢失(3 个重复的 ACK 或超时)。
现在,让时钟恢复...
T = 7
已确认 32 段
cwnd += 1; ssthresh = 32 kB
cwnd 的新值:33
传输 33 段
请注意cwnd
即使确认了 32 个段(ACK
因此每个段贡献 1/32),从 32 到 33 是如何变化的。如果我们处于慢启动状态,例如 T=6,我们将有cwnd += 32
. 这个新值cwnd
也与我们在时间 T = 7 时在图表中看到的一致。
T = 8
33 段确认
cwnd += 1; ssthresh = 32 kB
cwnd 的新值:34
传输 34 段
T = 9
已确认 34 段
cwnd += 1; ssthresh = 32 kB
cwnd 的新值:35
传输 35 段
请注意,这与图表一致:在 T=9 时,我们有cwnd = 35
. 这一直发生到 T = 16 ......
T = 10
已确认 35 段
cwnd += 1; ssthresh = 32 kB
cwnd 的新值:36
传输 36 段
T = 11
已确认 36 段
cwnd += 1; ssthresh = 32 kB
cwnd 的新值:37
传输 37 段
T = 12
已确认 37 段
cwnd += 1; ssthresh = 32 kB
cwnd 的新值:38
传输 38 段
T = 13
已确认 38 段
cwnd += 1; ssthresh = 32 kB
cwnd 的新值:39
传输 39 段
T = 14
已确认 39 段
cwnd += 1; ssthresh = 32 kB
cwnd 的新值:40
传输 40 段
T = 15
40 段确认
cwnd += 1; ssthresh = 32 kB
cwnd 的新值:41
传输 41 段
T = 16
41 段已确认
cwnd += 1; ssthresh = 32 kB
cwnd 的新值:42
传输 42 段
暂停
现在会发生什么?该图显示拥塞窗口大小减小到其大小的大约一半,然后在轮次中再次线性增长。唯一的可能是有 3 个重复的 ACK 并且协议切换到快速恢复。该图显示它不会切换到慢启动,因为这会cwnd
降低到 1。所以唯一可能的转换是快速恢复。
通过进入快速恢复,我们得到ssthresh = cwnd/2
. 请记住,cwnd
的单位是字节MSS
,ssthresh
我们必须小心。因此,新值为ssthresh = cwnd*MSS/2 = 42*1460/2 = 30660
。
同样,这与图表一致;请注意,ssthresh
当 略小于 30 时,将在不久的将来被击中cwnd
(回想一下,当 MSS = 1460 时,该比率不完全是 1:1,这就是为什么即使拥塞窗口大小略低于 30,我们也达到了阈值)。
切换到拥塞避免也会导致新的值cwnd
是ssthresh+3MSS = 21+3 = 24
(记住要小心单位,这里我ssthresh
再次转换为MSS,因为我们的值cwnd
计入MSS)。
截至目前,我们处于拥塞避免状态,T=17,ssthresh = 30660 bytes
并且cwnd = 24
。
输入 T=18 后,可能会发生两件事:要么我们收到重复的 ACK,要么没有。如果我们不这样做(所以这是一个新的 ACK),我们将过渡到拥塞避免。但这会cwnd
降低 的值ssthresh
,即 21。这与图表不匹配 - 图表显示cwnd
保持线性增加。此外,它不会切换到慢启动,因为这会cwnd
降低到 1。这意味着不会留下快速恢复并且我们会收到重复的 ACK。这发生在时间 T=22 之前:
T = 18
重复的 ACK 到达
cwnd += 1; ssthresh = 30660 字节
cwnd 的新值:25
T = 19
重复的 ACK 到达
cwnd += 1; ssthresh = 30660 字节
cwnd 的新值:26
T = 20
重复的 ACK 到达
cwnd += 1; ssthresh = 30660 字节
cwnd 的新值:27
T = 21
重复的 ACK 到达
cwnd += 1; ssthresh = 30660 字节
cwnd 的新值:28
T = 22
重复的 ACK 到达
cwnd += 1; ssthresh = 30660 字节
cwnd 的新值:29
** 暂停 **
我们还在Fast recovery,现在突然cwnd
下降到1,说明又进入了slow start。的新值ssthresh
将是29*1460/2 = 21170
和cwnd = 1
。这也意味着尽管我们努力重新传输该段,但还是出现了超时。
T = 23
cwnd = 1; ssthresh = 21170 字节
传输 1 段
T = 24
1 个段已确认
cwnd += 1; ssthresh = 21170 字节
cwnd 的新值:2
传输 2 段
T = 25
2 段已确认
cwnd += 2; ssthresh = 21170 字节
cwnd 的新值:4
传输4段
T = 26
4 段确认
cwnd += 4; ssthresh = 21170 字节
cwnd 的新值:8
传输8段
...
我希望这能说清楚。