5.3.9.2 快速重传和快速恢复
上一小节所讲述的网络拥塞,说的是 TCP 发送方发送了一批数据(1个或多个报文),一直等到超时(RTO),也没有收到对方的 ACK 报文。但是上一小节关于网络拥塞的处理,有一个潜台词的假设没有说出来,那就是:网络长时间拥塞,如图5-146所示:
图5-146 网络长时间拥塞
图5-146中,我们假设 B 对 A 所发送的每一个报文都应该回以1个 ACK 报文,但是很遗憾,由于网络拥塞,每一个 ACK报文,A 都没有收到。此之谓网络长时间拥塞。
上一小节中,TCP发送方“感知”到1个报文的超时,就认为网络是长时间拥塞了,所以它来了一个“垂直降落”。
但是有一种场景,那就是网络偶尔拥塞,对应到图5-146中,就是仅仅其中1个 ACK 报文没有收到,那么 TCP 是不是有更好的处理方案呢?答案是肯定的,那就是快速重传和快速恢复。
1)关于报文乱序和丢包的 ACK
在介绍快速重传和快速恢复之前,我们先介绍一个补充知识:如果 TCP 接收方所收到的报文是乱序的或者丢包了,它该如何回应 ACK。
先看乱序,如图5-147所示:
图5-147 报文乱序的 ACK
图5-147中,A 按照顺序向 B 发送了3个报文:SEG1、SEG2、SEG3,但是 B 所收到的报文的顺序却变成了 SEG1、SEG3、SEG2。那么,B 应该这么应答 A 呢?
对于所收到的第1个报文 SEG1,这没有问题,B 就正常的回以 ACK(SEG1, AKN = 200)。
对于所收到的第2个报文 SEG3,B 能够通过 SEG3 的 SEQ(300) 判断出,这不是它所期望的报文(它所期望的报文的 SEQ = 200),而是它所期望的报文的后续报文,此时它仍然回以 ACK(SEG1, AKN = 200)——告诉 A,它所期望的报文的 SEQ = 200。
但是,B 并不会将 SEG3 丢弃,而是保存起来,因为这种场面对于 TCP 来说是一个比较正常的事情——TCP 会认为,很可能是报文乱序了,那就不着急,耐心等待。
对于所收到的第2个报文 SEG2,B 会感慨:耐心终有回报,它所期望的那个报文终于来到了。而且非常凑巧,本次所收到的报文 SEG2,和上次所收到的报文 SEG3,正好可以连在一起(SEG2.SEQ + SEG2.DataLen = SEG3.SEQ),于是 B 就将 SEG2 和 SEG3 一起给确认了,回以 ACK(SEG3, AKN = 400)——告诉 A,从 SEQ = 200,到 SEQ = 399,这么多数据都收到了(对应报文 SEG2、SEG3),下一个所期望收到的报文的 SEQ = 400。
说明:ACK(SEGi)、ACK(SEGi, AKN = N) 是一种简化的表达,其含义是:这个 ACK 报文是针对 SEGi及其之前所有未确认报文的 ACK 报文,其中 AKN 字段的值等于 N,也就是 SEGi.SEQ + SEGi.DataLen。
乱序对于 TCP 的双方来说,仅仅是网络对它们开了一个小小的玩笑,无伤大雅,但是如果丢包呢?如图5-148所示:
图5-148 报文丢包的 ACK
图5-148中,A 发送的报文 SEG2 由于某种原因没有到达 B,可以看到,B 是一直倔强地回以 ACK(SEG1)。这比较好理解,假设 B 收到报文 SEG3 时,回以 ACK(SEG3),那么 A 就会认为 B 也收到了 SEG2,并且是将 SEG2、SEG3 打包 ACK(基于 Delay ACK 机制)。
更一般地,TCP 并不知道(或者并不想知道)报文是乱序了还是丢包了,反正只要它所接收的报文的 SEQ 大于它所期望的报文的 SEQ(SEG.SEQ > ACK.AKN),它就一直倔强地回以 ACK,其中 ACK.AKN 等于它所期望的 SEQ。
由于后面的内容中涉及“重复 ACK”这个概念,我们这里就将“重复 ACK”做一个定义。从图5-148可以看到,B 回应的第1个 ACK 报文是一个正常的回应,而后续的3个 ACK 报文,都是重复 ACK。这是我们直观的感觉,RFC 5681 关于“重复 ACK”的定义是:
(1)收到 ACK 报文的一方(也就是图5-148中的 A),得有已发送未确认的数据
(2)所收到的 ACK 报文中,没有包含数据
(3)所收到的 ACK 报文中,SYN 和 FIN 标记都是关闭的(其值为0)
(4)所收到的 ACK 报文中的 AKN,等于自己发送空间的 UNA
(5)所收到的 ACK 报文,其所通告的 Window 大小与上次 ACK 报文的 Window 的大小相同。
这个定义同时也暗示了,TCP 面对乱序或者丢包时,其所回应的 ACK 报文是不会变化 Window 字段的。
我们以图5-148为例,将这个定义解释如下:
(1)A 发送了报文 SEG1,B 回以 ACK(SEG1),其中 ACK.AKN = 200, ACK.Window = 1000
(2)A 发送了报文 SEG2,假设 SEG2.SEQ = 200,此时 A 的已发送未确认报文的 SEQ等于 200,即 A.UNA = 200
(3)但是由于 SEG2 丢失了,B 并没有收到,所以 B 也不会回以报文 ACK(SEQ2)
(4)A 发送了报文 SEG3,根据前文描述,B 收到 SEG3 以后,会回以 ACK(SEQ1),即 ACK.AKN = 200 = A.UNA, ACK.Window = 1000。我们把这个 ACK 报文记为 ACK1
(5)A 发送了报文 SEG4 ......B 回复的报文,从内容上来说,仍然是 ACK1——这是第1次重复 ACK
(6)A 发送了报文 SEG5 ......B 回复的报文,从内容上来说,仍然是 ACK1——这是第2次重复 ACK
(7)A 发送了报文 SEG6 ......B 回复的报文,从内容上来说,仍然是 ACK1——这是第3次重复 ACK
......
RFC 5681 为什么要对“重复 ACK”做如此比较严格的定义呢?这与快速重传机制有关。
2)快速重传和快速恢复算法
严格来说,单纯的快速重传与拥塞控制没有多大关系,它仅仅是为了提升发送效率。为了方便阅读,我们将图5-148重新贴一下,取名“3次重复 ACK”,如图5-149所示:
图5-149 3次重复 ACK
对于 A 来说,它每收到1次重复 ACK,都会有疑问:为什么会收到重复 ACK?是 SEG2 丢包了?是 SEG2 乱序了(到达 B 的时间比较晚)?还是网络将 ACK 报文错误地重复发送了?
古人云“事不过三”,当 A 收到了3次重复的 ACK,它该怎么做?
一种做法是心中继续那些疑问,但是也仅仅是疑惑而已,还是继续发送它的报文 SEG6、SEG7 ......一直到超时时间(RTO)到,A 发送自己还没有收到 SEG2 的 ACK 报文,这时候才恍然大悟:哦,原来 SEG2 超时了,需要重传。这就是传统的超时重传策略。
还有一种做法是:既然已经收到了3次重复 ACK,那很可能意味着 SEG2 丢包了,那么与其坐等超时,不如就直接认为 SEG2 超时了,干脆就重传 SEG2 吧。这种收到3次重复 ACK 就认为是丢包,并且重传该报文的策略,称为快速重传。
从这个意义上讲,快速重传确实与拥塞控制没有多大关系。但是:
(1)既然认为是丢包了——就算实际上没有丢包,SEG2 还在网络中,那时延也已经很大了——那说明网络发生了拥塞。既然网络发送了拥塞,那么是不是应该象上一小节介绍的那样:垂直降落,然后进入慢速启动/拥塞避免状态呢?
(2)但是,网络拥塞的状况似乎也没有多严重。毕竟随后发送的报文(SEG3~SEG5),对方应该是都收到了,并且也回应了 ACK(虽然是3次重复的 ACK(SEG1))。那么是不是可以认为网络已经不拥塞了(就算曾经拥塞过一小段时间,现在也已经变好了)?是不是就不必“垂直降落/慢速启动”了?是不是就保持当前状态?
这两个想法,前者偏悲观,后者偏乐观,选择哪个,似乎都有点纠结。于是,RFC 5681 进行了一定的折中,提出了“快速重传-快速恢复”算法。
超时重传也好,快速重传也好,它们的本质是一样的:
(1)都是对“丢包”的一种探测。TCP 的世界里没有上帝——没有上帝告诉 TCP,这个报文是真的被网络丢弃了,还是仍然在网络中传输(只是传输的有点慢)——TCP 只能根据自己的“感觉”来“赌”一把,或者通过“超时”,或者通过“3次重复 ACK”。两种“赌”法的概率都比较高,相对而言,通过“3次重复 ACK”的效率更高。
(2)都是对“丢包”的一种“修复”。既然认为丢包了,那就得修复它——这一点两者是一样的,都是重传该报文。
超时重传后,“垂直降落-慢速启动=拥塞避免”算法接管了后续的发送策略,而快速重传后,接管发送策略的快速恢复算法。
“快速重传-快速恢复”的算法如下:
(1)当 TCP 发送方收到第1个和第2个重复 ACK 时,它仍然是按照原来的策略发送新报文,比如5-149中继续发送 SEG4、SEG5。此时,发送方不能调整它的 cwnd。
(2)当 TCP 收到第3个重复 ACK 时,它需要调整 ssthresh:
ssthresh <= max (FlightSize / 2, 2*SMSS)
(3)那个“丢包”被重传(SEG.SEQ = TCP.UNA)——也就是所谓的“快速重传”
(4)将 cwnd 设置为:
cwnd = ssthresh + 3 * SMSS
(5)后续如果还收到重复 ACK(第5次、第6次 ......),每收到1次,cwnd = cwnd + SMSS。而且此时可以继续发送“新”数据(发送空间里的“已允许未发送”数据)。
(6)当收到对方新的 ACK 时(即对方收到了重传的报文,并给予正确的回应),需要重新将 cwnd 设置为:
cwnd = ssthresh
(7)在随后的过程中,TCP 进入拥塞避免状态,也就是采用拥塞避免算法:
cwnd = ssthresh
纵观“快速重传-快速恢复”算法和“慢速启动-拥塞避免”算法,两者的不同点是:
(1)对于丢包的检测方法不同(丢包也就意味着拥塞)。前者使用“3次重复 ACK”当作判断标准,而后者使用“超时”当作判断标准
(2)拥塞(丢包)发生后,前者并没有将 cwnd“垂直降落”,而是将 cwnd 设置为:cwnd = ssthresh + 3 * SMSS,后者则将 cwnd 垂直降落:cwnd <= SMSS
(3)拥塞(丢包)发生后,前者进入“拥塞避免”状态,而后者进入“快速启动”状态。
“快速重传-快速恢复”算法和“慢速启动-拥塞避免”算法,两者的相同点是:
(1)拥塞(丢包)发生后,两者都将 sstrhresh 进行了调整,也都可以简单认为调整公式也相同:ssthresh = max (FlightSize / 2, 2*SMSS)
我们把以上的比较,继续总结,如表5-30所示:
表5-30 “快速重传-快速恢复”和“慢速启动-拥塞避免”的比较
比较项 | 比较 结果 | 快速重传-快速恢复 | 慢速启动-拥塞避免 |
拥塞检测 | 不同 | 3次重复 ACK | 超时 |
拥塞后,ssthresh 的调整 | 相同 | ssthresh = max (FlightSize / 2, 2*SMSS) | |
拥塞后,cwnd 的调整 | 不同 | cwnd = ssthresh + 3 * SMSS | cwnd = SMSS |
拥塞后的状态 | 不同 | 拥塞避免 | 慢速启动 |
两者之所以有如此不同,归根结底在于两者对于拥塞程度的认识。“慢速启动-拥塞避免”算法比较悲观,因为到了超时。“快速重传-快速恢复”算法相对来说,比较乐观,因为它在感知到丢包的同时,也发现后续的报文好像没有什么问题。
最后再解释一下“快速重传-快速恢复”算法中关于 cwnd 的调整,它一会等于 cwnd = ssthresh + 3 * SMSS,一会又等于 cwnd = ssthresh。这是为什么?
笔者以为,这体现了 TCP 面对网络拥塞时的很多正能量思想。
首先,我们抛开那个变来变去的 cwnd,先说说拥塞控制本身。笔者第一眼看到网络拥塞是崩溃的。试想一下,一个 TCP 连接发送了网络拥塞的心里活动:这网络怎么拥塞了?这是谁造成的?我没有发送多少数据啊?这也不是我造成的啊,我又能怎么办?就算我现在一点数据不发送,网络还是会拥塞啊!我该怎么办?
这样的心里活动确实令人崩溃!但是,TCP 的想法却是很正能量:只要人人都献出一份爱,世界将变成美好的明天。于是我们看到,面对网络拥塞时,TCP 并没有抱怨、并没有迷惘,更没有推卸责任,而是马上进入拥塞控制算法。而且不仅如此,在连接刚开始创建时,TCP 就主动进入了拥塞控制的角色(慢速启动)。
帮助别人,也就是帮助自己。TCP 这种主动而无私的精神,同样也使自己受益,它可以是自己避免陷入无限的重传陷进中。
同时,我们也看到,TCP 在互帮互助、互相谦让的同时,也是齐心协力在充分利用网络的带宽。毕竟,大家的目标还是要传输数据,总不能网络一拥塞时,大家都在家里葛优躺。
齐心协力,充分利用网络带宽,比如“快速重传”等我们就不多说了,这里谈谈它的思想。
TCP 认为网络就是一个大管道,而所谓网络拥塞,就是报文在管道上淤积。TCP 的思想,有一点类似“配额制”:谁在网络上淤积,谁就减少配额;谁的淤积从网络中消失了,谁就可以增加配额。
所以我们看到前文所描述的算法:
(1)收到3次重复 ACK 以后,不仅快速重传、调整 ssthresh,而且将cwnd 调整为 cwnd = ssthresh + 3 * SMSS。
这里的“3 * SMSS”,是TCP 认为自己所发送的报文“SEG3~SEG5”并没有在网络上淤积,因为它收到了3次重复的 ACK。所以,TCP 就增加了自己的配额——增加“3 * SMSS”。
(2)当快速重传并且收到对应的 ACK 以后,TCP 又将 cwnd 调整回来 cwnd = ssthresh,这......我编不下去了,这应该是 TCP 觉得应该进入正常的轨道:进入拥塞避免状态,此时 cwnd 应该等于 ssthresh,然后再利用拥塞避免算法慢慢增加 cwnd。
不要认为 cwnd 这种变来变去是莫名奇妙,事实上正如它的名字所暗示的那样,TCP 不是一个传输协议,而是一个控制协议。无数个 TCP 连接竞争同一个网络,既要公平,又要效率,是 TCP 永远的追求!