TCP系列28—窗口管理&流控—2、延迟ACK(Delayed Acknowledgments)
一、简介
之前的内容中我们多次提到延迟ACK(Delayed Ack),延迟ACK是在RFC1122协议中定义的,协议指出,一个TCP实现应该实现延迟ACK,但是ACK不能被过度延迟,协议给出延迟ACK的最大时间为0.5s。如果发送端连续发送最大的数据报文,那么没两个数据报文就需要回复一次ACK。延迟ACK主要目的是等待接收者应用层接收到数据处理后有可能会发送一个响应,这样ACK报文就可以和这个响应报文一起发送了,这样减少了网络中的数据包的同时,也降低了主机的负载处理压力。
二、linux实现概述
Linux在实现上有个延迟ACK定时器,这个定时器的定时时间保存在一个ato变量,这个ato的默认值为40ms,但是会动态调整,例如对于非交互式应用如果延迟ACK定时器超时的时候,ato变量的值就会倍增。但是延迟ACK定时器的最大定时时间为0.5s。与延迟ACK对应,linux还有一个quick ACK模式,这种quick ack模式下就会对每个数据包都回复一个ACK。在连接初始建立时候、收包间隔大于RTO时、收到不在接收窗内的报文的时候等场景下就会进入quck ack模式,进入quick ack模式的时候,会把quick ack计数器初始化为16(也有可能是小于16的某个值),这就是说随后的16个数据包都不采用延迟ACK。
具体到延迟ACK判断的时候,需要同时满足几个条件,如当前已经收到的但是还没有回复ACK确认包的报文小于接收MSS、当前没有处于quick ACK模式、当前缓存中没有先前接收的乱序报文、当前延迟ACK的超时时间大于ato/4等条件。
对于上面MSS的判断条件,linux采用保守的方法来估计对端的发送MSS(即本端的接收MSS,在linux内核中以rcv_mss变量维护),初始化的时候,首先会在SYN报文中的MSS值、SYN-ACK报文中的MSS值、接收窗口的一半、536bytes中取一个最小值,然后在这个最小值和88bytes中取一个最大值,即为接收MSS值,随后在这个TCP连接上面收到新的数据包的时候,一旦发现这个数据包比之前的接收MSS值大,就会把接收MSS值更新为这个数据包的大小。根据MSS这个判断条件,在TCP高速传输的时候,一般都是以MSS发送数据,因此延迟ACK时候会每两个报文回复一个ACK确认包。
quick ACK模式可以通过socket编程接口TCP_QUICKACK选项进行设置,但是这个设置只是设置了一个quick ACK模式的切换,随后TCP的内部处理还可能会切换quick ACK模式。也可以通过路由表永久设置打开quick ACK 模式,例如下面就设置了127.0.0.1这条路由开启quick ACK。
******@Inspiron:~$ sudo ip route change local 127.0.0.1 dev lo quickack 1
******@Inspiron:~$ ip route show table all| grep 'local 127.0.0.1'
local 127.0.0.1 dev lo table local scope host quickack 1
上面只是根据linux实现代码概述了一下延迟ACK的实现,还有一些偏僻细节并没有提到,感兴趣的自行参考linux的实现代码。下面我们根据上面的描述来看一个延迟ACK的wireshark示例。
三、wireshark示例
我们通过一个综合实例来看一下延迟ACK模式的处理,示例图中高亮的数据包是我标记出来的以方便查看,并非是wireshark自动标记的异常包。在后面介绍其他内容后我们还会穿插延迟ACK的示例。
1、综合示例
首先在连接初始建立的时候,按照上面描述会进入quick ACK模式,quick ACK计数器初始化为16,连接初始建立进入quick ACK的目的主要是加速初始慢启动过程。从下图可以看到,从第三个数据包开始,client没发送一个TCP报文,server端就会立即回复ACK确认包。另外从图中可以看到此时client的SYN包中MSS为62,server端SYN-ACK报文中的MSS为65495,server端的接收窗口大小为43690,此时初始化接收MSS过程为,先计算min(62-12, 65495-12, 43690/2, 576)=62bytes,因为linux内部维护的MSS扣除了TSopt选项的12bytes,所以min计算中需要对两端通告的MSS先减12。接着计算max(62, 88)=88bytes,即此时server端认为接收MSS为88byte。一旦后面进入延迟ACK模式后,如果接收的还没有回复ACK确认的报文总大小超过88bytes的时候就会立即回复ACK报文。
接着我们看到从No36报文开始,server并不会对每个收到的TCP报文立即回复ACK了,即进入了延迟ACK模式。而之前的No4-No35共32个报文,包含16个client发过来的数据报文,16个server端回复的ACK报文,正好与quick ACK的初始值16对应。接着从No36开始,server端收到No36的时候判断已经推出了快速ACK模式同时其他条件也满足需要延迟ACK,启动延迟ACK定时器,定时时间为40ms,但是server端在接收到No38报文的时候发现累计没有回复ACK确认包的总数据量为120bytes超过了接收88bytes的接收MSS,因此立即回复了ACK确认包并取消延迟ACK定时器(根据编译内核时候的配置也可能并不取消延迟ACK定时器,但是延迟ACK定时器超时后会判断当前ACK已经发送而不再重复发送ACK)。随后的No40-No43和No44-No47报文的处理流程与No36-No39类似不在重复描述
接着从No48报文开始,报文长度变为20bytes,这样要超过88bytes的接收MSS就需要5个报文,可以从图中看出,server端正是在接收到No48-No52共五个报文100bytes后才触发了No53的ACK回复。可以看出No53和No48之间大约是12ms,并不是延迟ACK超时触发的No53确认包回复。接着注意在No58处server端收到长度为150byte的数据包,server更新接收MSS为max(当前接收MSS, 150)=150bytes。接着从No60报文开始可以看到需要连续4个40bytes的报文才会立即触发ACK回复(注意与上面接收MSS为88bytes,报文长度同为40bytes时候需要3个报文触发ACK的场景进行对比)。
我们接着看No70-No73四个数据包总长度为140bytes低于150bytes的接收MSS,因此并没能触发ACK回复,最终延迟ACK定时器超时,回复No74确认包。此时ato更新为80ms,但是收到No75数据包的时候,ato又更新为60ms(ato更新这块有点罗嗦,已经超出了本文的范围,所以知道这个东西会更新就行了,想深入的可以去看代码),因此延迟ACK定时器设置为60ms。No75-No78四个数据包总共120bytes没有超过接收MSS,因此最终延迟ACK定时器超时,回复No79确认包,但是我们看到No79和No75之间时间差大约为58ms,与预期的60ms有些差距,原因是ubuntu16.04编译内核默认的宏CONFIG_HZ=250,即1s为250tick,这个tick是TCP模块使用的最小时间单位,即4ms,因此定时的时候有可能最大有4ms的误差。可以从自行添加的内核log中确认此时的ACK延迟定时器为60ms。这个定时器误差在TCP模块中的其他各种定时器中也是可能会出现的。接下来No80-No84五个数据包总共为145bytes,没有满足150bytes的接收MSS,但是从图中可以看到No84仍然立即触发了ACK回复,原因是server端接收到No84数据包后,发现当前ato为40ms,距离触发延迟ACK定时器的时间已经不足ato/4了,因此立即触发了No85的ACK确认包。接着No86和No87直接同样是延迟ACK超时。
接着因为No88与上一个数据包No86之间的时间差超过了RTO的值,server端重新进入quick ACK模式,并把quick ACK计数器初始化为16,对接下来的16个数据包立即回复ACK确认包,如下图所示
2、通过路由表设置quick ACK模式
当执行下面语句设置quick ACK后,重新执行上面的程序,不会再出现延迟ACK的情况,限于篇幅不再附wireshark截图,感兴趣的可以自行去git下载
sudo ip route change local 127.0.0.1 dev lo quickack 1
3、TLP与延迟ACK交互
在低时延场景下,TLP与延迟ACK交互可能会造成无效重传,示例请参考后面SWS介绍文中的wireshark示例。
补充说明:
1、在MAC OS上可以通过设置net.inet.tcp.delayed_ack来配置延迟ACK,设置为0表示禁止延迟ACK,设置为1表示总是延迟ACK,设置为2表示每两个数据包回复一个ACK,设置为3标识系统自动探测回复ACK的时机
2、windows可以通过在HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\IG条目下设置TcpAckFrequency参数来配置延迟ACK,可以通过TcpdelAckTicks来设置延迟ACK定时器的定时时间