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丢包了。

Step

Packets ACKed

CongestionWindow

传输中的包数

传输的包

1.

None

10

10

1, 2, 3, 4, 5, 6, 7, 8, 9, 10

2.

1

11

11

11, 12

3.

2

12

12

13, 14

4.

2(DA1, R5)

13

13

15, 16

5.

2(DA2, R6)

14

14

17, 18

6.

2(DA3, R7) *

7

13

重传 3

7.

2(DA4, R8)

7

12

8.

2(DA5, R9)

7

11

9.

2(DA6, R10)

7

10

10.

2(DA7, R11)

7

9

11.

2(DA8, R12)

7

8

12.

2(DA9, R13)

7

7

13.

2(DA10, R14)

7

7

19

14.

2(DA11, R15)

7

7

20

15.

2(DA12, R16)

7

7

21

16.

2(DA13, R17)

7

7

22

17.

2(DA14, R18)

7

7

23

18.

3

7

7

24

19.

3(DA1, R19)

7

7

25

20.

3(DA2, R20)

7

7

26

21.

3(DA3, R21)*

7

7

重传 4

22.

3(DA4, R22)

7

7

27

23.

3(DA5, R23)

7

7

28

24.

3(DA6, R24)

7

7

29

25.

3(DA7, R25)

7

7

30

26.

3(DA8, R26)

7

7

32

27.

26

(7+1/7)

7

32

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

Last updated