一、Linux中的慢启动和拥塞避免
Linux中采用了Google论文的建议把IW初始化成了10了。在linux中一般有三种场景会触发慢启动过程
-
1、连接初始建立发送数据的时候,此时cwnd初始化为10,ssthresh初始化为0x7fffffff,因此会触发慢启动。但是当路由表中有对应的设置的时候,cwnd和ssthresh会被路由表中的设置的值覆盖,有可能连接建立后直接进入拥塞避免阶段。
2、RTO超时进入Loss状态后,此时cwnd初始化为1,ssthresh的值会调用具体拥塞控制算法的回调函数(ssthresh函数)来设置,如reno算法中ssthresh = max(snd_cwnd/2, 2U)。
3、TCP连接idle时间在一个RTO以上时候,会重新初始化cwnd。这种场景下的处理我们留到后面CWV的文章中进行介绍。
在reno算法中的慢启动过程与前面文章概述的慢启动流程类似,接收的ack number每确认一个报文,就会更新cwnd=cwnd+1。而在reno的拥塞避免过程中,会维护一个cwnd_cnt的变量,初始值为0,当cwnd>=ssthresh的时候,进入拥塞避免后,接收到的ack number每确认一个报文的时候,就会更新cwnd_cnt=cwnd_cnt+1,如果cwnd_cnt>=cwnd,那么就会更新cwnd=cwnd+1,cwnd_cnt=cwnd_cnt-cwnd。在reno算法中只有TCP发送端处于network-limited状态下才会根据慢启动和拥塞避免来更新ssthresh和cwnd,关于network-limited状态的介绍,以及非network-limited状态下的拥塞控制,请参考后面的CWV相关文章。另外packets_out、 sacked_out、 lost_out、 retrans_out、 in_flight这些状态变量的说明请参考前面的文章。
至于linux中对于延迟ACK、stretched ACK、ABC、ACK压缩等的处理我们会在后面的示例中加以演示介绍。建议在读本篇示例前先把前面两篇文章读完。
二、wireshark示例
为了演示方便我们把server端的tcp连接的初始拥塞窗口设置为3,并把拥塞控制算法设置为reno。同时我们关闭网卡的tso和gso功能这样wireshark才能看到网卡最终发出去的报文。另外在下面的wireshark截图中为了方便观察SACK信息,info列中不在显示TSopt选项的信息,并不是报文不带有TSopt选项了。
******@Inspiron:~$ sudo ip route add local 127.0.0.2 dev lo congctl reno initcwnd 3 #相关设置庆参考本系列destination metric相关文章
******@Inspiron:~$ ip route show table all | grep 127.0.0.2
local 127.0.0.2 dev lo table local scope host initcwnd 3 congctl reno
******@Inspiron:~$ sudo ethtool -K lo tso off gso off #关闭tso gso以方便观察cwnd变化
1、慢启动、拥塞避免与RTO超时、FACK、TLP综合示例
如下图所示client为127.0.0.2:10000,server为127.0.0.1:9877,client端与server端时延为50ms,client端使用quickack,对于收到的server端的每个数据包都会回复一个ACK确认包。下图可以看成是server端的wireshark抓包。server端在连接建立后先休眠1s,然后以5ms为发送间隔连续发送20个数据包,每个数据包的大小为50bytes,加上TSopt选项的大小后正好为client端的mss大小。
No1-No3:TCP连接建立,client端SYN报文中的MSS设置为62,扣除12bytes的TSopt选项的大小后,正好为50byte,此时server端处于Open状态,初始cwnd为路由表中配置的3,初始化ssthresh为2147483647(十六进制为0x7fffffff),packets_out、sacked_out、lost_out、retrans_out、cwnd_cnt等变量都会初始化为0。
No4-No5:client发送一个22bytes大小的请求报文,server端回复一个ACK确认报文。
No6-No8:server端以5ms为间隔连续写入20次50bytes大小的数据,可以看到写入的前三个报文都顺利发送出去了。从wireshark中可以看到No8报文发出时间为1.061,server端休眠5ms后,第四个报文写入时间应该大约为1.066,但是可以从wireshark中看到在1.066这个时间点上server端并没有发出报文。原因就是受到拥塞控制的限制。在发送完No8报文后in_flight = packets_out - ( sacked_out + lost_out) + retrans_out = 3-(0+0)+0=3,可以看到in_flight的大小已经达到用拥塞窗口的大小,因此不能额外发送新的数据了。
No9-No11:client对No6报文的ACK确认包No9经过大约50ms后到达server端,server端收到No9确认包后,cwnd=cwnd+1=4,而此时只有No7和No8两个报文还没有收到ACK确认包因此packets_out=2,in_flight=2-(0+0)+0=2,in_flight比cwnd小2,因此收到No9确认包后,可以发出NO10和No11两个新的数据包了
No12-No14、No15-No17、No18-No20、No21-No23、No24-No26、No27-No29这个过程与No9-No11类似,cwnd在每收到一个ACK报文后自增1,in_flight则可以从最新发出的报文和最近收到的ack number计算出来。最终server端在发出No29这个tcp报文后,cwnd=10,in_flight=10,可以看到此时同样是由于拥塞控制限制而不能发出新的数据包。
client端对于No17之后的报文直接丢弃,而且不回复ACK确认包,以触发server端RTO超时,观察RTO超时后server端的行为。
No30:可以看到这个报文与No29报文之间的时间差大约为100ms。server端与client端的RTT大约为50ms,想起来这个报文是什么包了吧。这个就是TLP的loss probe报文,server端在发出No29报文的时候会启动TLP定时器,定时间隔为2*RTT。TLP超时后发现还有未发出的新数据,因此选择了发送新的数据而不是重传No29报文。No30发出后,cwnd=10,in_flight=11,可以看到此时in_flight已经超越了cwnd的限制,如之前介绍TLP时候所说,TLP的发送不受拥塞控制的限制。TLP的内容前面不少文章都有过实例详解,详细请参考之前的文章。
No31:在No30报文发出后,启动RTO定时器,从server端程序可以看到此时server端的RTO为252ms。最终RTO超时后,server端重传了第一个还未收到ACK确认包的报文。可以看到No31和No30之间的时间差正好为252ms。如上所说,在RTO超时后,ssthresh的值由具体的拥塞控制算法的回调函数确定,在我们使用的reno算法中,ssthresh=max(cwnd/2, 2)=5。而cwnd固定设置为1。在RTO超时后linux还会把所有发出去还未被ack number或者SACK确认的数据包标记为lost。此时packets_out=lost_out=11,在发出No31报文后retrans_out=1,则in_flight = packets_out - ( sacked_out + lost_out) + retrans_out=1,正好与cwnd相同,因此发出No31重传包后,不能在发出其他的重传报文。
No32、No33:同样是两个RTO超时发出的报文,在同一个数据包多次连续RTO超时进行指数回退的过程中ssthreah的值并不会改变,但是每次RTO超时都会把cwnd初始化为1,把cwnd_cnt初始化为0。但是如果另外的其他数据包触发RTO超时,那么ssthresh的值则会初始化为max(cwnd/2, 2)。注意是其他的数据包触发了RTO超时才会初始化ssthresh,如果是我们之前介绍的慢启动重传(SlowStartRetrans)并不会更新ssthresh。
No34-No36:server端在收到No34ACK确认包后,更新packets_out=packets_out-1=10,lost_out=lost_out-1=10,retrans_out=retrans_out-1=0,in_flight = packets_out - ( sacked_out + lost_out) + retrans_out=0,而此时cwnd为1,因此允许发出一个额外的数据包,接着进入慢启动重传流程,重传No35护具包后retrans_out=1,in_flight也变为1,此时受限与拥塞窗口cwnd而不能在重传其他数据包,因此退出慢启动重传。接着因为收到了新的ACK报文会使用具体的拥塞控制算法更新拥塞窗口,在reno的回调函数中更新cwnd=cwnd+1=2,ssthreah不变仍为5,更新拥塞窗口后会尝试发送之前未发送的新数据,因此又发出了一个新的数据包No36,更新packets_out为11。注意这里的顺序是先进行重传然后进行拥塞窗口的更新,最后尝试发送新的未发送数据。这是一个linux通常的处理顺序,但是有些场景下可能会先更新拥塞窗口然后进行重传,最后尝试发送新数据,例如收到包含有SACK信息的old ACK的时候(old ACK是指ack number在SND.UNA之前)。
No37-No39:这个过程与No34-No36类似,收到ack报文后重传一个数据包,然后更新cwnd,接着发出一个新数据包。注意No39系列号范围为(951,1000),我们一共写入了20个50bytes大小的数据包,因此这个数据已经是最后的一个数据包了。后面已经没有待发送的新数据了。在No39之后packets_out=11,lost_out=9,retrans_out=1,cwnd=3,in_flight = packets_out - ( sacked_out + lost_out) + retrans_out=3,可以看到正好是cwnd的大小。注意这里的No38重传就是慢启动重传,并没有触发RTO超时,因此不会更新ssthresh。注意我们之前介绍过TCP只会维护一个RTO定时器,Seq=351的数据包RTO超时后,触发的No35超时重传,但是随后No38是慢启动重传,位于recovery point之前。详细内容参考前面重传部分的文章。
No40-No41:No40是对No36的ACK回复报文,带有一个SACK信息通知server已经收到No36报文,server端收到No40后更新sacked_out=1,in_flight = packets_out - ( sacked_out + lost_out) + retrans_out=11-(1+9)+1=2,而此时cwnd为3,因此可以在额外重传一个No41报文,虽然wireshark显示No41为TCP Out-Of-Order,但No41实际上就是一个慢启动重传,此时更新retrans_out=2。因为No40这个ACK报文的ack number并没有确认收到新的数据包,因此cwnd也不能进行更新。
No42-No43:server端收到No38的ACK确认包后,retrans_out=retrans_out-1=1,packets_out=packets_out-1=10,lost_out=lost_out-1=8,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=10-(1+8)+1=2,因此又可以额外重传一个No43报文了,重传完毕后更新retrans_out=retrans_out+1=2,接着reno更新cwnd=cwnd+1=4,然后尝试发送新的未发送数据,上面我们已经说了No39已经是最后一个数据包,已经没有新的未发送数据了,因此尝试发送新的未发送数据失败。注意这时候in_flight=3,cwnd=4,此时拥塞窗口实际上还允许额外发送一个TCP报文的。
No44-No46:从No44的SACK信息可以看到,No44实际上是No39的ACK确认包,server端收到No44后,更新sacked_out=sacked_out+1=2,然后尝试进行慢启动重传,此时in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=10-(2+8)+2=2,cwnd=4因此可以重传No45、No46两个报文,更新retrans_out=retrans_out+2=4,No44的ack number并没有确认新的数据包,因此重传完后并不会更新cwnd。
No47-No48:No47是No41的ACK确认包,server端在收到这个数据包后,更新retrans_out=retrans_out-1=3,packets_out=packets_out-1=9,lost_out=lost_out-1=7,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=9-(2+7)+3=3,而此时cwnd=4,因此可以重传一个TCP报文No48,然后更新retrans_out=retrans_out+1=4,接着reno更新cwnd=cwnd+1=5。注意此时的ssthresh=5,因此这次cwnd更新后,TCP将从慢启动进入到拥塞避免。
No49-No51:No49是No43的ACK确认包,server端收到No49报文后更新retrans_out=retrans_out-1=3,packets_out=packets_out-1=8,lost_out=lost_out-1=6,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=8-(2+6)+3=3,而此时cwnd=5,因此TCP可以额外发出两个新的重传包No50和No51,然后更新retrans_out=retrans_out+2=5,接着reno更新cwnd_cnt=cwnd_cnt+1=1,因此cwnd_cnt<cwnd,不满足拥塞避免更新cwnd的条件,因此cwnd不更新。
No52-No53:server端收到No52后,更新retrans_out=retrans_out-1=4,packets_out=packets_out-1=7,lost_out=lost_out-1=5,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=7-(2+5)+4=4,而此时cwnd=6,因此可以额外的重传两个数据包出去,但是此时只剩下最后一个数据包等待重传了,因此server端只发出了No53重传包,然后更新retrans_out=retrans_out+1=5,接着reno更新cwnd_cnt=cwnd_cnt+1=2,cwnd不变。
No54:No54是对应No46的ACK确认包,server端收到No54后,更新retrans_out=retrans_out-1=4,packets_out=packets_out-1=6,lost_out=lost_out-1=4,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=6-(2+4)+4=4,接着reno更新cwnd_cnt=cwnd_cnt+1=3,cwnd不变。
No55:No55是对应No48的ACK确认包,server端收到No55后,更新retrans_out=retrans_out-1=3,packets_out=packets_out-1=5,lost_out=lost_out-1=3,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=5-(2+3)+3=3,接着reno更新cwnd_cnt=cwnd_cnt+1=4,cwnd不变。
No56:No56是对应No50的ACK确认包,server端收到No56后,更新retrans_out=retrans_out-1=2,packets_out=packets_out-1=4,lost_out=lost_out-1=2,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=4-(2+2)+2=2,接着reno更新cwnd_cnt=cwnd_cnt+1=5,注意这个时候cwnd_cnt已经满足拥塞避免更新cwnd的cwnd_cnt>=cwnd条件,因此reno更新cwnd=cwnd+1=6,cwnd_cnt=0。
No57:No57是对应No51的ACK确认包,server端收到No57后,更新retrans_out=retrans_out-1=1,packets_out=packets_out-1=3,lost_out=lost_out-1=1,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=3-(2+1)+1=1,接着reno更新cwnd_cnt=cwnd_cnt+1=1,cwnd不变。
No58:No58是数据传输过程中的最后一个报文,是对No53包的ACK确认,注意No58相对于No57,ack number新确认了三个数据包,其中包含No53、另外还有SACK块中的两个数据包即No36和No39。server端收到这个报文后会更新retrans_out=retrans_out-1=0,packets_out=packets_out-3=0,lost_out=lost_out-1=0,sacked_out=sacked_out-2=0,in_flight=packets_out - ( sacked_out + lost_out) + retrans_out=0,接着reno更新cwnd_cnt=cwnd_cnt+3=4,cwnd不变。
补充说明:
1、https://developers.google.com/speed/articles/tcp_initcwnd_paper.pdf
2、linux中三种慢启动场景对应的cwnd初始化处理代码tcp_init_sock、 tcp_enter_loss、 tcp_init_metrics、 tcp_slow_start_after_idle_check