首页 > 代码库 > TCP系列33—窗口管理&流控—7、Silly Window Syndrome(SWS)

TCP系列33—窗口管理&流控—7、Silly Window Syndrome(SWS)

一、SWS介绍

        前面我们已经通过示例看到如果接收端的应用层一直没有读取数据,那么window size就会慢慢变小最终可能变为0,此时我们假设一种场景,如果应用层读取少量数据(比如十几bytes),接收端TCP有了少量的新的接收缓存后如果立即进行window update把新的window size通告发送端的话,发送端如果立即发送数据,那么接收端缓存可能又会立即耗尽,window size又变为0,接着应用层重复读取少量数据,这个过程重复的话,那么发送端就会频繁的发送大量的小包,这种场景我们就称呼为Silly Window Syndrome(SWS),中文一般翻译为糊涂窗口综合征或者愚蠢窗口综合征。之前我们介绍Nagle算法的时候已经介绍过,大量的小包会降低网络利用率甚至造成网络拥塞,最终降低网络性能。

        从上面的关于SWS的介绍中我们可以看到两个关键点,一个是接收端通告了比较小的window size,另外一个是发送端收到比较小的window size的时候,立即响应发出了对应的小包。因此如果要避免SWS,按照RFC1122协议可以从这两个方面着手:

  • 对于接收端要避免以小的增量来推进接收窗的右边沿(RCV.NXT+RCV.WND),即使接收的数据包都是小包。只有当接收窗口的增量大于min( Fr * RCV.BUFF, Eff.snd.MSS )的时候才通告新的窗口。其中Fr是一个分数因子,协议建议值为1/2,Eff.snd.MSS为对端的发送MSS。

  • 对于发送端来说在不超出对端接收窗口的前提下至少满足下列三个条件中的一个才能发送数据:(1)、一个full-sized的数据包(即大小满足Eff.snd.MSS)可以被发送;(2)、数据包的大小超过对端曾经通告过的window size的一半;(3)、当TCP发送端禁用了Nagle算法或者所有发出的数据都已经被对端ACK确认的话,那么TCP可以发送小数据包;

RFC1122要求TCP的接收端和发送端都是需要实现SWS避免算法的。另外Nagle算法和SWS避免算法是互补的,都是在不同场景下避免发送小数据包。

二、linux实现介绍

在linux实现上对于接收端大体处理如下

1、应用层读取了新的数据时候,如果空闲的接收缓存大于等于总接收缓存的1/16,并且大于等于MSS的估计值的时候,会根据这个空闲缓存计算出一个新的window size,如果这个新的window size大于等于两倍的当前接收窗口才会立即触发window update

2、回复对端window probe或者ACK确认包时候,如果空闲的接收缓存小于总接收缓存的1/16/或者小于MSS的估计值的时候,在不shrink窗口的前提下则回复window size为0的报文。

上面所说的MSS估计值就是rcv_mss和总接收缓存两者之间的最小值,rcv_mss的具体初始化方式和更新方式前面延迟ACK内容中已经介绍过了。

对于发送端实际上我们前面内容已经看到,发送端维护的发送MSS最大也只能为接收端曾经通告过的最大的window size的一半。因此发送端的条件(1)、(2)可以合并,条件3中的Nagle算法我们之前已经介绍过了相关内容。

三、wireshark示例

测试之前我们先进行如下设置

  1. sudo ip route change local 127.0.0.1 dev lo  src 127.0.0.2  mtu 1530

这个命令的含义是,当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






来自为知笔记(Wiz)


TCP系列33—窗口管理&流控—7、Silly Window Syndrome(SWS)