趣谈TCP拥塞控制

趣谈TCP拥塞控制

若网络中两个端系统A和B之间进行通信,只要两者之间存在空间距离,由于光速的传播时延,一定意味着A与B之间存在一定的容量,可以容纳若干数据段,因此可以将其中的传输介质看作一般的缓存,且为了更具普遍性,我们可以认为A与B之间的所有路由器、交换机等中间节点的缓存队列也包含在A与B的网络缓存之中。为达到最高的网络利用率,最好的现象就是A与B之间的缓存(包括节点队列及网络本身)中完全充满TCP数据段,并且能够维持无间隙的持续流动。

趣谈TCP拥塞控制


如上图所示,在两个端系统之间的网络被填满时,A与B之间共存在N个数据段,事实上发送端还是可以继续发送数据。因为网络被填满后,发送端每发送一个数据段,接受端同时也会消费掉一个数据段并发出一个ACK。假定ACK的速率和数据段的速率一致,发送端能够持续发送2*N个数据段,发送的开始时间是第一个数据段发送的时间,结束时间是第一个数据段的ACK回到发送端的时间,即为一个RTT,若发送速率为r,则可以获得等式:

2 * N = r * RTT

由以上过程可以得知,前N个段是为了填满A与B之间的网络,后N个段是在A与B之间已经满载的情况下ACK时钟驱动的pacing。随后的2*N个数据段是一个新的周期,即一个RTT内2*N个数据段。这就是最理想情况下的情景,数据充满了网络,不间断地从发送端发出,ACK也不间断地从接受端返回。

TCP在经历三次握手建立连接后开始传送数据,但并不是一开始就向网络中发送大量的数据包,而是根据初始的cwnd大小逐步增加,cwnd初始化为一个最大报文段(MSS)的大小,每当有一个报文段被确认,cwnd大小指数增长:

开始 -> cwnd = 1

1个RTT -> cwnd = 2

2个RTT -> cwnd = 4

3个RTT -> cwnd = 8

……

若带宽为W,那么经过RTT * log2W时间就可以占满带宽,因此cwnd不能这样无限增长,而是需要一个限制。TCP使用慢启动门限(ssthresh)变量检测拥塞避免的触发,一旦cwnd >= ssthresh,慢启动过程结束,拥塞避免开始,此时cwdn的值不再指数上升,而是开始进行线性增加,当滑动窗口中所有的报文段都被确认时,cwnd的大小加1,直到发送方RTO超时导致重传了一个报文段后进行如下操作:

1.把ssthresh降低为cwnd值的一半;

2.把cwnd重新设置为1;

3.重新进入慢启动过程。

TCP还有一种情况会进行重传,即收到3个相同的ACK。TCP在收到乱序的数据段时会立刻进行确认,利用3个相同的ACK来判定数据包的丢失,此时进行快速重传:

1.把ssthresh降低为cwnd值的一半;

2.把cwnd再设置为ssthresh的值;

3.重新进入拥塞避免阶段。

后来的“快速恢复”算法是在上述的“快速重传”算法后添加的,当收到3个重复ACK时,TCP最后进入的不是拥塞避免阶段,而是快速恢复阶段。快速重传和快速恢复算法一般同时使用。快速恢复的思想是“数据包守恒”原则,即同一个时刻在网络中的数据包数量是恒定的,只有当“老”数据包离开了网络后,才能向网络中发送一个“新”的数据包,如果发送方收到一个重复的ACK,那么根据TCP的ACK机制就表明有一个数据包离开了网络,于是cwnd加1。如果能够严格按照该原则那么网络中很少会发生拥塞,事实上拥塞控制的目的也就在修正违反该原则的地方。快速恢复的主要步骤是:

1.当收到3个重复ACK时,把ssthresh设置为cwnd的一半,把cwnd设置为ssthresh的值加3,然后重传丢失的报文段,加3的原因是因为收到3个重复的ACK,表明有3个“老”的数据包离开了网络;

2.再收到重复的ACK时,拥塞窗口加1;

3.当收到新的数据包的ACK时,把cwnd设置为第一步中的ssthresh的值。原因是因为该ACK确认了新的数据,说明从重复ACK时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态。

为便于理解TCP拥塞控制的简单过程,可进行如下类比:

慢启动(Slow Start)

亚当隔着山头扔玉米棒子给夏娃,亚当不知道夏娃能接多快,于是一次扔1个,编号为1。

夏娃喊2,意思是老娘1号玉米棒子已经收到,准备迎接2号玉米棒子。

亚当一次扔2个,编号为2、3。

夏娃喊4,准备迎接更多的玉米棒子。

亚当一次扔4个,编号为4、5、6、7。

夏娃喊8,意思是让玉米棒子来得更猛烈些吧!

亚当一次扔8个,编号为8、9、…15。

夏娃嘴里一直重复喊编号12,次数为3次,这里传达以下信息:

8-11号玉米棒子已经安全到达

12号玉米棒子肯定丢了

13、14、15号玉米棒子也应该安全到达,否则夏娃只会喊一次12,是13、14、15号玉米触发夏娃重复的叫喊。

亚当意识到自己扔太快了,需要降速,降到多少合适呢?降一半,一次扔4个没有问题。

if ( dupacks >= 3 ) {

ssthresh = max( 2 , cwnd / 2) ;

}

这里cwnd =8,所以ssthresh=4。

但外面至少还有3个发出的玉米棒子还没有确认(Outstanding Packet),如果将cwnd = ssthresh=4则意味着亚当最多一次可以扔四个玉米,但3个发出却没有确认的玉米棒子占了3个名额,所以亚当最多一次只能扔一个玉米棒子,发送速率急剧下降,这不合理。

快速重传

既然收到夏娃三次重复的确认,说明丢的玉米棒子(12)后的3个已经成功接收,不在空中飞,这3个虽然还没有明确地确认,但已经隐含地确认了,所以这3个玉米棒子不应该占据在空中飞玉米的数量,在空中飞的玉米应该是4个,再加上到达夏娃的3个,所以亚当的cwnd (Congestion Window)应该为7个。

cwnd = ssthresh + 3 * SMSS= 4+3=7

亚当的快速重传

1.重传12号玉米

2.扔16、17、18、19号玉米

快速重传结束信号

一旦亚当接到夏娃16号玉米或之后的确认,快速重传/快速修复完成。

拥塞避免(Congestion Avoidance)

亚当意识到一次扔4个安全,于是选择以cwnd = ssthresh=4为基准线,如果一次扔4个没有问题,那就一次扔5个、6个,线性增长到夏娃的接收极限。

参考:知乎--车小胖


分享到:


相關文章: