跳到主要内容

4.5 其他的修修补补 --- TCP NewReno

如果我们从TCP拥塞控制中只能学到一件事情,那就是拥塞控制太过复杂,要处理太多的细节才能让其正常工作,而这些细节的处理只能经过实际运行经验的反馈才能知道。这一节介绍其中的两个例子。

4.5.1 TCP SACK​

最初的TCP规范使用了累积的ACK。所谓的累积的ACK是指 TCP 接收端会确认在丢包前收到的最后一个packet(注,也就是只确认按照顺序接收的最后一个packet)。你可以认为接收端会排列所有接收到的packet,任何丢失的packet在接收端的字节流里都对应一个缺口。在最初的规范里,即使已经丢失了多个packet,接收端也只能告诉发送端第一个缺口的起始位置。

很明显,这里信息的缺失会降低发送端对实际丢包情况的判断能力。用来解决这里问题的方法被称为selective acknowledgements,或者SACK。SACK也是一个TCP的扩展,它在Jacobson和Karels的早期工作(注,也就是TCP Tahoe版本)之后很快就被提出,但是过了好些年才被大众接受,因为很难证明它的有效性。

如果没有SACK,那发送端只有两种合理的策略来应对包乱序了。

  • 悲观的策略。当发生快速重传或者超时重传时,除了重传明显已经丢失的packet(接收端处第一个丢失的packet),同时也重传所有后续的packet。悲观策略假设最坏的情况发生了:所有不确定的packet都丢失了。悲观策略的缺点是,它可能不必要的重传已经被接收端成功接收的packet。
  • 乐观的策略。在发生丢包时(由超时或者重复的ACK触发),仅重传相应的那1个segment。乐观策略的缺点是,当一段连续的segment丢失时,要花较长时间才能恢复。而一段连续的segment丢失,在网络拥塞时是可能发生的。之所以要花较长时间是因为每个segment的丢失,都要等到前一个segment的重传对应的ACK收到了之后才能被发现。也就是说,对于一段连续丢失的segment,每个segment的重传都要消耗一个RTT。

如果有SACK,发送端可以有更好的策略:只重传被选择性确认的segment之间的segment,也就是只重传丢失的segment。

SACK需要在TCP连接建立之初,在发送端和接收端之间协商。当使用SACK时,接收端还是按照之前一样回复ACK,并且TCP header中的Acknowledgement字段的含义也没有变化。但同时在TCP header中包含接受的乱序segment(注,存在于TCP option中)。这样发送端就能识别出接收端缺失的数据,并且只重传缺失的segment,而不是重传丢失segment之后的所有segment。

SACK在TCP Reno版本中带来了性能的提升,尤其在一个RTT中多个packet丢包时(因为如果使用乐观策略,累积ACK和SACK在只丢一个包时是一样的,所以只有丢多个包才能看出性能区别)。随着带宽延时积的增加,SACK的效果越来越明显,因为带宽延时积越大,对应的TCP EffectiveWindow也就越大,相应的,在一个RTT中,会有更多packet在网络中传输,这样一个RTT中多个packet丢包的概率就更大。

SACK在1996年成为了一个IETF标准(RFC2018),对TCP来说是个及时的补充。

4.5.2 NewReno​

NewReno是源自 MIT 的Janey Hoe在 1990 年代中期的研究。它可以在一个拥塞窗口内丢失多个packet时,避免像TCP Reno一样多次对拥塞窗口减半,从而提升TCP Reno在这个场景下的性能。

NewReno 的核心思想是:即使没有 SACK,重复 ACK 也携带了:(1)是否还有丢包(2)丢了哪些包,的信息。发送端可以基于这些信息,决定重传哪些包,以及如何调整自己的拥塞窗口。

下面的例子展示了TCP NewReno如何识别重复的ACK,以及如何调整拥塞窗口。

假设初始时,CongestionWindow = 10 packets,Packet 3和4丢包了。

StepPackets ACKedCongestionWindow传输中的包数传输的包
1.None1010 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
2.1111111, 12
3.2121213, 14
4.2(DA1, R5) 131315, 16
5.2(DA2, R6) 1414 17, 18
6.2(DA3, R7) * 7 13 重传 3
7.2(DA4, R8) 712–
8.2(DA5, R9) 711–
9.2(DA6, R10)710–
10.2(DA7, R11) 79 –
11.2(DA8, R12) 78–
12.2(DA9, R13) 77–
13.2(DA10, R14)7719
14.2(DA11, R15) 7720
15.2(DA12, R16) 7721
16.2(DA13, R17) 7722
17.2(DA14, R18) 7723
18.37724
19.3(DA1, R19) 7725
20.3(DA2, R20) 7726
21.3(DA3, R21)* 77重传 4
22.3(DA4, R22) 7727
23.3(DA5, R23) 7728
24.3(DA6, R24) 7729
25.3(DA7, R25) 7730
26.3(DA8, R26) 7732
27. 26(7+1/7)732
  • Step 1-3:初始时的慢启动,每个ACK都会使得CongestionWindow增加一个packet,也就是每个ACK都会带来2个新的in-flight的packets。
  • Step 4-5:虽然此时已经开始在接受Packet 2的重复ACK,但是因为数量还不够,TCP认为可能是包乱序引起的重复ACK,所以仍然每个ACK都会使得CongestionWindow增加一个packet,在Step 5时,拥塞窗口增加至14。
  • Step 6:收到了Packet 2的3个重复ACK,重传3,并将拥塞窗口减半至7,进入Fast Recovery。
  • Step 7-12:因为拥塞窗口为7,在传输中的packet大于等于7,此时不发送任何数据。
  • Step 13-17:虽然还是在收到重复ACK,之前堆积的在传输中的packet数量下降到了7。之后的每一步,每个ACK都会释放CongestionWindow一个packet的大小,同时使得发送端可以再发送一个新数据。在这个过程中,CongestionWindow一直保持不变。
  • Step 18:收到了Packet 3的ACK,但是在重传3之前,已经发送了Packet 18,这说明Packet 3之后的包也有可能丢了,否则此时应该收到packet 18的ACK。
  • Step 19-21:收到了Packet 3的3个重复ACK,Packet 4丢失无疑,重传4。但是发送端可以识别出Packet4与Packet3在一个拥塞窗口内,因此这次只有重传,拥塞窗口不会减半(NewReno的核心)。
  • Step 22-26:与Step13-17类似。
  • Step 27:收到了Packet 26的ACK,这是重传Packet 4之前发送的Packet。因此发送端可以判定,目前已经没有丢包了,因此可以进入加法递增阶段,每个ACK增加 1/CongestionWindow 个packet的大小。

值得注意的是,NewReno 被记录在 1999-2012 年的 3 个 RFC 中,每一个都修复了前一个的一些问题。这是个理解拥塞控制算法究竟有多复杂的好的例子(尤其还需要考虑到 TCP 重传机制的种种细节),同时也增加了新算法部署实施的难度。