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