跳到主要内容

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 重传机制的种种细节),同时也增加了新算法部署实施的难度。