TCP系列41—拥塞控制—4、Linux中的慢启动和拥塞避免(一)
一、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
TCP系列41—拥塞控制—4、Linux中的慢启动和拥塞避免(一)的更多相关文章
- TCP系列42—拥塞控制—5、Linux中的慢启动和拥塞避免(二)
在本篇中我们继续上一篇文章wireshark的示例讲解,上一篇介绍了一个综合示例后,本篇介绍一些简单的示例,在读本篇前建议先把上一篇读完,为了节省篇幅,本篇只针对一些特殊的场景点报文进行讲解,不会像上 ...
- 牛客网Java刷题知识点之拥塞发生的主要原因、TCP拥塞控制、TCP流量控制、TCP拥塞控制的四大过程(慢启动、拥塞避免、快速重传、快速恢复)
不多说,直接上干货! 福利 => 每天都推送 欢迎大家,关注微信扫码并加入我的4个微信公众号: 大数据躺过的坑 Java从入门到架构师 人工智能躺过的坑 ...
- TCP系列54—拥塞控制—17、AQM及ECN
一.概述 ECN的相关内容是在RFC3168中定义的,这里我简单描述一下RFC3168涉及的主要内容. 1.AQM和RED 目前TCP中多数的拥塞控制算法都是通过缓慢增加拥塞窗口直到检测到丢包来进行慢 ...
- TCP系列46—拥塞控制—9、SACK下的快速恢复与Limited transmit
一.概述 1.SACK下的特殊处理过程 SACK下的拥塞控制处理是linux中拥塞控制的实现依据,再次强调一遍RFC6675的重要性,linux中拥塞控制主体框架的实现是与RFC6675一致的,所以如 ...
- TCP系列45—拥塞控制—8、SACK关闭的拥塞撤销与虚假快速重传
一.概述 这篇文章介绍一下TCP从Recovery状态恢复到Open状态的时候cwnd的更新.我们在tcp重传部分的文章中曾经介绍过虚假重传的概念,Linux在探测到虚假重传的时候就会执行拥塞撤销操作 ...
- TCP系列44—拥塞控制—7、SACK关闭的快速恢复
) return; delta = ssthresh - in_flight; prr_delivered += newly_acked_sacked; if (delta < 0 ...
- TCP系列43—拥塞控制—6、Congestion Window Validation(CWV)
一.概述 在RFC2861中,区分了TCP连接数据传输的三种状态 After sending a data segment: If tcpnow - T_last >= RTO ...
- TCP系列40—拥塞控制—3、慢启动和拥塞避免概述
本篇中先介绍一下慢启动和拥塞避免的大概过程,下一篇中将会给出多个linux下reno拥塞控制算法的wireshark示例,并详细解释慢启动和拥塞避免的过程. 一.慢启动(slow start) 一个T ...
- TCP系列39—拥塞控制—2、拥塞相关算法及基础知识
一.拥塞控制的相关算法 早期的TCP协议只有基于窗口的流控(flow control)机制而没有拥塞控制机制,因而易导致网络拥塞.1988年Jacobson针对TCP在网络拥塞控制方面的不足,提出了& ...
随机推荐
- 解决在 win10 下 vs2017 中创建 MFC 程序拖放文件接收不到 WM_DROPFILES 消息问题
解决方案 这个问题是由于 win10 的安全机制搞的鬼,即使以管理员权限运行也不行,因为它会把 WM_DROPFILES 消息过滤掉,那怎么办呢?只需在窗口初始化 OnInitDialog() 里添加 ...
- urllib库使用方法1 request
urllib是可以模仿浏览器发送请求的库,Python自带 Python3中urllib分为:urllib.request和urllib.parse import urllib.request url ...
- 关于uip中的log和打印
简单易用,好区分 void uip_log(char *m){ printf("uIP log message: %s\n", m);}
- 手动封装一个属于自己的AJAX类库
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- go语言的cron包的简单使用
3.cron举例说明 每隔5秒执行一次:*/5 * * * * ? 每隔1分钟执行一次:0 */1 * * * ? 每天23点执行一次:0 0 ...
- 20155212 实验二 Java面向对象程序设计
20155212 实验二 Java面向对象程序设计 单元测试 三种代码 用编程去解决问题,必须学会写三种码: 伪代码 产品代码 测试代码 例: MyUtil 类解决一个百分制成绩转成"优.良 ...
- 20155307 2017-2018-2 《Java程序设计》第2周学习总结
20155307 2017-2018-2 <Java程序设计>第2周学习总结 教材学习内容总结 整数:short(2 byte).int(4 byte).long(8 byte) 字节(b ...
- vim 中文乱码问题
编辑~/.vimrc文件,加上如下几行: set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936 set termencoding=utf ...
- thinkphp 去除空格
- 【SCOI2009】迷路
题面 题解 如果给我们的是一个邻接矩阵,那么直接给邻接矩阵\(T\)次幂即可. 这里的图有边权,那么我们就将它拆成\(9\)个点即可. 代码 #include<cstdio> #inclu ...