一、SWS介绍

前面我们已经通过示例看到如果接收端的应用层一直没有读取数据,那么window size就会慢慢变小最终可能变为0,此时我们假设一种场景,如果应用层读取少量数据(比如十几bytes),接收端TCP有了少量的新的接收缓存后如果立即进行window update把新的window size通告发送端的话,发送端如果立即发送数据,那么接收端缓存可能又会立即耗尽,window size又变为0,接着应用层重复读取少量数据,这个过程重复的话,那么发送端就会频繁的发送大量的小包,这种场景我们就称呼为

这个命令的含义是,当client发起主动连接到127.0.0.1的时候,优选源地址为127.0.0.2,MTU大小设置为1530。设置MTU的大小的目的是限制client端的发送MSS为1490bytes(1500减掉20bytes的ip头在减掉20bytes的tcp头),这样方便观察对比,同时也说明了server端维护rcv_mss的时候为什么没有直接使用SYN报文中的MSS选项而采取了保守估计的方式。另外我们在程序中通过通过SO_RCVBUF选项设置server端用于接收有效TCP数据的缓存为3500bytes,即window size大小为3500。SO_RCVBUF这个选项设置为3500的时候,实际内核内部的接收缓存为7000bytes,其中有1/2用于接收TCP载荷数据,另外的缓存用于存储相关的数据结构等。这个比例可以通过/proc/sys/net/ipv4/tcp_adv_win_scale设置。

此处示例不再细致解释延迟ACK、quick ACK、window probe的指数回退和对window probe的ACK特殊处理,详细请参考前面文章。

1、SWS综合示例

client和server建立连接后,client立即连续写入三次数据,每次write写入2048bytes,server端则先休眠15s,然后每隔2s读取256bytes的数据

  • No1-No3:client与server通过三次握手建立连接,可以看到client主动发起连接到127.0.0.1的server端的时候,使用的ip地址为127.0.0.2。另外可以看到server端SYN-ACK报文中的MSS为65495,但是我们通过路由设置了到127.0.0.1地址的MTU为1530(换算为MSS为1490),因此client端会取两者的较小值为MSS,即client端的发送MSS为1490,在扣除TSopt选项所占的12bytes的空间后,client端最大能发送的数据报文的大小为1478bytes,client端内核实际维护的MSS值为扣除tsopt后的1478。

  • No4-No5:因为我们通过路由设置了到127.0.0.1的mtu为1530bytes,client进行write写入2048bytes的数据的时候,内核会先从应用层复制1478bytes的数据发送出去,server端收到这个数据后回复ACK确认报文。

  • No6-No7:接着client端内核复制剩余的570bytes发送出去,server端进入延迟ACK模式,延迟定时器设置为40ms。

  • client连续write写入数据的时候,第二个write写入的2048bytes数据,内核首先会读入1478bytes的数据,尝试发送出去的时候发现发送对方接收窗口的大小只有1452bytes的空间,因此client端会先把这1478bytes的数据缓存起来,继续读入剩余的数据并缓存起来。

  • No7:延迟ACK定时器超时后回复No7确认报文,可以看到定时器定时40ms,但是大约在37ms后就超时了,定时器精度的问题前面解释过多次了这里不再重复。此处可以看到No7中的window size为1452,小于rcv_mss(1478),但是server端并没有回复window size=0的ACK报文,原因是避免窗口右边沿收缩,可以看到避免窗口右边沿收缩优先级高于SWS避免算法。client端收到这个ACK报文后,发现当前所有发出去的数据都已经被ACK确认了,并且当前尚有待发送数据因为对端接收窗口限制未发送出去,因此启动peisist timer定时器,定时时间为RTO,即208ms。

  • No8-No9:persist timer定时器超时后,强制发送1452bytes的数据填满对端的接收窗口,server端回复ACK确认报文。

  • No10-No22:client端进行window probe过程,并进行指数回退,server端进行无效系列号的ACK回复,这个过程之前文章已经解释过了,不在解释。

  • No23:server端从第15s开始进行read读取操作,但是实际上并不是每次读取数据都会释放server端的接收缓存,server端内核使用一种叫做skb的结构来保存接收到的TCP数据,server端接收到第一个No4报文的时候这个报文对应的skb大小为2358bytes,这个结构中保存的有效TCP数据大小为No4报文的大小即1478。接着server端收到No6报文的时候保存这个报文的skb大小为2304bytes,但是这个skb保存的有效TCP数据仅仅为570bytes。server端收到No8报文的时候对应的skb结构大小为2332,对应的有效TCP数据为1452bytes。可以看到server端总共收到了(1478+570+1452)=3500bytes的数据,这些是对应window size大小的缓存,但是这些数据和保存这些数据的结构总共占用了(2358+2304+2332)=6994bytes的接收缓存。用于接收数据的缓存3500bytes和额外的其他的缓存(6994-3500)=3494bytes两者之间的比例接近1:1。我们通过SO_RCVBUF选项设置server端缓存为3500bytes的时候,内核实际会把接收缓存设置为7000bytes,然后在根据tcp_adv_win_scale参数设定的比例来分配用于接收有效TCP数据的缓存和其他的缓存,本例中tcp_adv_win_scale参数值为1,对应的比例为1:1可以看到与实际情况比较接近。server端在读取数据的时候只有完整的读取出1478bytes的数据后才能释放2358bytes的skb结构增加接收缓存,然后在读取出570bytes的数据后才能释放第二个skb所占用的2304bytes的接收缓存。server端在进行第6次read操作后,总共读取1536bytes的数据,对应的时间点大约为25s,但是可以看到server端在第25s的时候并没有进行window update操作,在随后的No21报文进行window probe的回复No22的时候window size仍然为0,原因就是释放第一个skb后,空闲的接收缓存为(7000-2304-2332)=2364bytes,按照tcp_adv_win_scale参数进行1:1比例分配后用于接收有效TCP数据的缓存(即window size大小)为1182bytes,可以看到1182是小于server端维护的rcv_mss(此时为1478bytes),因此此时继续回复的报文中window size仍然为0。server端读取到第8次的时候,对应的时间点为29s,此时共读取了8*256=2048bytes,第二个skb得以释放此时server端的window size缓存为(7000-2332)/2=2334,这个值已经高于rcv_mss了,因此满足发送window update的条件,server端会将window size取为rcv_mss大的整数倍,即如No23所示,Win=1478。(本段及后面描述中每个skb的大小取自添加的SOCK_DEBUG日志,仅用于说明server端的处理过程,不必纠结具体大小怎么计算的)

  • No24:server端在persist timer定时器超时的时候强制发送1452bytes的No8报文,把一个1478bytes的mss大小的报文拆成了两段,一段是No8的1452bytes,另外一段就对应No24的26bytes。可以看到(1452+26)=1478,正好与发送mss相符合。

  • No25:此处有意思的是server端相比No23并没有读取新的数据,但是收到No24的新数据回复的No25的时候,window size仍然为1478,与No23相同。原因是新收到的No24报文大小为26bytes,合并到了保存No8报文的skb结构中,并没有额外占用新的skb结构,No8对应的skb大小则变为2332+26=2358bytes。此时对应的window size缓存为(7000-2358)/2=2321,可以看到仍然高于一个rcv_mss,因此No25报文中window size继续取为1478。

  • No26-No27:No26在server端对应的skb大小为2358bytes,此时server端window size缓存为(7000-2358-2358)/2=1142, 减掉的两个2358一个对应No26报文的skb,另外一个对应No8和No24报文合并后的skb, 计算出来的1142低于rcv_mss,因此回复的No27报文在没有收缩接收窗右边沿的前提下window size=0。

  • No28-No36:填满对端接收窗口后,继续进行类似的window probe过程,不再细致解释。

  • No37:server端自29s之后,继续进行了6次read操作后对应时间点为41s,共读取了1536bytes,而No8和No26报文实际上共存有1478bytes的有效tcp数据,因此这个skb对应的tcp数据已经被完全读取出来了,这个skb得以释放,此时对应window size的缓存为(7000-2358)/2=2321,可以看到高于一个rcv_mss可以进行window update了,window size取为rcv_mss的整数倍后为1478。

  • No38-No39:client端把最后剩余的1140bytes数据发出后,server端回复ACK确认包,client和server之间的数据传输完成

  • No40:server端一直读取数据后,No40的window update消息触发流程与上面No23和No37类似。

  • No41:No41触发window update的原因是到No61时候,server端已经把所有的数据读取完毕,所有的skb都得以释放,此时计算出来的用于window size的缓存为7000/2=3500,取为rcv_mss的整数倍后为2956,而之前的接收窗口大小为1478,新计算出来的接收窗口满足大于或等于之前接收窗口的两倍的条件,因此触发了window update消息。

2、TLP和延迟ACK的交互

client和server建立连接后,client立即连续写入三次数据,第一次写入2048bytes,第二次写入204bytes,第三次写入2048bytes,server端则先休眠15s,然后每隔2s读取256bytes的数据

作为与上面示例的对比,可以看到在第二次写入204bytes的数据时候,client端立即将这204bytes的数据发出了,原因是这个小数据包没有超过对端接收窗口,同时Nagle算法允许这个小数据包发出,因此这个数据包立即发出对应No7,而上面示例中的No8报文虽然满足MSS大小的条件,但是因为超出了对方接收窗口的大小因此只能等待persist timer定时器超时。

另外一点需要注意的是在低时延下,TLP和延迟ACK的交互会造成如下图No8所示的无效重传,server端在收到No6报文的时候会启动延迟ACK定时器,定时时间为40ms,但是client在发出No7报文的时候会启动TLP定时器,定时时间为max(2*SRTT, 10ms)=10ms,因为TCP模块tick精度问题,最终TLP定时器设置为12ms,client端的TLP定时器大约在0.012s左右超时,而server端的延迟ACK定时器超时时间大约为0.040,因此最终TLP定时器首先超时重传了No7报文,造成了无效重传。

其余报文流程与上面示例类似,不再逐个讲解。

补充说明:

1、实际上linux内部的处理判断条件挺多的,这里只是简单概述了一下,感兴趣的可以继续查看相关的linux代码

接收端read操作读取数据后对于窗口更新的处理tcp_recvmsg、 tcp_cleanup_rbuf、 __tcp_select_window、tcp_send_ack

发送端write写入数据的处理流程tcp_sendmsg、 tcp_push_one、 tcp_push、 __tcp_push_pending_frames、 tcp_write_xmit、 tcp_transmit_skb、 tcp_select_window、 __tcp_select_window

TCP系列33—窗口管理&流控—7、Silly Window Syndrome(SWS)的更多相关文章

  1. TCP系列27—窗口管理&流控—1、概述

    在前面的内容中我们依次介绍了TCP的连接建立和终止过程和TCP的各种重传方式.接着我们在这部分首先关注交互式应用TCP连接相关内容如延迟ACK.Nagle算法.Cork算法等,接着我们引入流控机制(f ...

  2. TCP系列31—窗口管理&流控—5、TCP流控与滑窗

    一.TCP流控 之前我们介绍过TCP是基于窗口的流量控制,在TCP的发送端会维持一个发送窗口,我们假设发送窗口的大小为N比特,网络环回时延为RTT,那么在网络状况良好没有发生拥塞的情况下,发送端每个R ...

  3. TCP系列32—窗口管理&流控—6、TCP zero windows和persist timer

    一.简介 我们之前介绍过,TCP报文中的window size表示发出这个报文的一端准备多少bytes的数据,当TCP的一端一直接收数据,但是应用层没有及时读取的话,数据一直在TCP模块中缓存,最终受 ...

  4. TCP系列36—窗口管理&流控—10、linux下的异常报文系列接收

    在这篇文章中我们看一下server端在接收到异常数据系列时的处理,主要目的是通过wireshark示例对这些异常数据系列的处理有一个直观的认识,感兴趣的自行阅读相关代码和协议,这里不再进行详细介绍 在 ...

  5. TCP系列35—窗口管理&流控—9、紧急机制

    一.概述 我们在最开始介绍TCP头结构的时候,里面有个URG的标志位,还有一个Urgent Pointer的16bits字段.当URG标志位有效的时候,Urgent Poinert用来指示紧急数据的相 ...

  6. TCP系列34—窗口管理&流控—8、缓存自动调整

    一.概述 我们之前介绍过一种具有大的带宽时延乘积(band-delay product.BDP)的网络,这种网络称为长肥网络(LongFatNetwork,即LFN).我们想象一种简单的场景,假设发送 ...

  7. TCP系列30—窗口管理&流控—4、Cork算法

    一.Cork算法概述 Cork算法与Nagle算法类似,也有人把Cork算法称呼为super-Nagle.Nagle算法提出的背景是网络因为大量小包小包而导致利用率低下产生网络拥塞,网络发生拥塞的时候 ...

  8. TCP系列29—窗口管理&流控—3、Nagle算法

    一.Nagle算法概述 之前我们介绍过,有一些交互式应用会传递大量的小包(称呼为tinygrams),这些小包的负载可能只有几个bytes,但是TCP和IP的基本头就有40bytes,如果大量传递这种 ...

  9. TCP系列28—窗口管理&流控—2、延迟ACK(Delayed Acknowledgments)

    一.简介 之前的内容中我们多次提到延迟ACK(Delayed Ack),延迟ACK是在RFC1122协议中定义的,协议指出,一个TCP实现应该实现延迟ACK,但是ACK不能被过度延迟,协议给出延迟AC ...

随机推荐

  1. 【Java】集合遍历--List和Map的多种遍历方式

    1. List的两种遍历方式 package com.nova.test; import java.util.ArrayList; import java.util.Iterator; import ...

  2. CASE WHEN 批量更新

    单个值: UPDATE categories SET display_order = CASE id WHEN 1 THEN 3 WHEN 2 THEN 4 WHEN 3 THEN 5 END WHE ...

  3. Linux字符设备学习,总结

    注册字符驱动的一种老方法: 注册一个字符设备的经典方法是使用:int register_chrdev(unsigned int major, const char *name, structfile_ ...

  4. centos7安装mysql5.7.18笔记

    重装了一下系统,装了centos7,但是centos7下默认没有安装mysql,有MariaDB数据库,网上的解释是: “MariaDB数据库管理系统是MySQL的一个分支,主要由开源社区在维护,采用 ...

  5. Verilog_Day3

    内容为书中第5章 条件语句 条件语句必须在过程块语句中使用.所谓过程块语句是指由 initial 和 always 语句引导的执行语句集合.除这两种块语句引导的begin_end块中可以编写条件语句外 ...

  6. Java设计模式(17)——行为模式之观察者模式(Observer)

    一.概述 概念 UML简图 我们根据一个示例得类图来分析角色 角色 抽象主题:保存观察者聚集(集合),管理(增删)观察者 抽象观察者:定义具体观察者的抽象接口,在得到主题通知后更新自己 具体主题:将有 ...

  7. Lucene第一讲——概述与入门

    一.概述 1.什么是Lucene? Lucene是apache下的一个开源的全文检索引擎工具包. 它为软件开发人员提供一个简单易用的工具包(类库),以方便的在目标系统中实现全文检索的功能. 2.能干什 ...

  8. P1294 高手去散步

    P1294 高手去散步 题目背景 高手最近谈恋爱了.不过是单相思.“即使是单相思,也是完整的爱情”,高手从未放弃对它的追求.今天,这个阳光明媚的早晨,太阳从西边缓缓升起.于是它找到高手,希望在晨读开始 ...

  9. 在线tidb+tipd+tikv扩容,迁移,从UC到阿里云

    集群现状: 共有五个节点,配置为16核32g内存,数据节点为1T ssd盘,非数据节点为100g ssd盘: 角色规划: node1 tidb tipd node2 tidb tipd node3 t ...

  10. 【JUC源码解析】Exchanger

    简介 Exchanger,并发工具类,用于线程间的数据交换. 使用 两个线程,两个缓冲区,一个线程往一个缓冲区里面填数据,另一个线程从另一个缓冲区里面取数据.当填数据的线程将缓冲区填满时,或者取数据的 ...