序言
很多人在提到Delay ACK的时候,都会认为它既然是一个TCP特性,那就必然存在一个开关,可以随意开启或者关闭,以下是一些显然的想法:
1.系统中有一个开关,比如sysctl或者Windows注册表项,可以开启本机的TCP Delay ACK特性;
2.系统的编程API中提供了socket选项,可以通过setsockopt来开启或者关闭某一条连接的TCP Delay ACK特性。
3.某些操作系统或者某些系统版本可能不支持TCP Delay ACK选项。
持这些想法当然没有错,但是如果从另一个角度去理解Delay ACK话,可能就会有另一种想法了,而Linux正是这另一种想法的体现,当然,这也是本文的主题。
Delay ACK的实质
Delay ACK是什么?这是一个伪问题!解释概念往往非常容易,但对理解问题却是毫无益处。我们应该问:为什么会让一个针对接收到数据的ACK延迟发送?有人认为是为了减少网络上的ACK流量,有人说是为了给发送端一点突发的机会所以要积累ACK再发送,在ACK延迟的这段时间,发送端可能已经积累了足够的数据,而这对于提高长肥管道的吞吐率是有益处的。
...
也许你会认为我马上要长篇大论一通关于Nagle算法以及Write-Write-Read算法的细节了,事实上不!我们只需要知道,Delay ACK以及Nagle算法是针对特定场景的,不光是Delay ACK,不光是Nagle算法,所有的TCP算法,包括那些拥塞控制算法,都是针对特定场景的,没有放之四海而皆准的TCP算法!
在一个TCP连接启动的时候,没有人可以预知该TCP连接后续的交互模式以及数据发送序列(除非你是在做重放实验!!),因此如果你开启了Delay ACK,但是恰恰遇到了并不适合开启Delay ACK的场景,比如遇到了Write-Write-Read,那岂不是会吃亏?那么一个问题摆在了人们面前:
到底是开启Delay ACK好呢?还是关闭它好呢?
正文
请记住上一节序言的最后的那个问题,本文将围绕它展开。本文不会去分析Delay ACK会造成问题(比如Write-Write-Read这种)的各种场景,而仅仅从以下一个场景开始去展开。该场景是我自己构造的,旨在解剖两类经典的Delay ACK的实现机制。
先看场景吧。But,场景前有个声明。
关于Delay ACK的触发声明
一般而言,如果TCP接收端收到了超过一个MSS大小的数据,无论怎样都会立即回复一个ACK,这个2个MSS大小阈值是为了平衡延迟和吞吐,我不知道为什么会选为2个MSS,可能是经验值,也可能是大牛大傻逼拍脑袋拍出来的值。所以,为了简单起见,我接下来的论述中,每次(对于理解Linux内核的而言,就是每个传输的skb)所传输的数据长度均不会大于1000,我所有实验的MTU均为1500,也就是说,每次传输的数据长度均不会大于1个MSS,这样就不用考虑Delay ACK与MSS的关系了。
因此,下文所有情况中,如果按照标准的Delay ACK的理解,所有的传输均会触发接收端Delay ACK!
[root@localhost linux]# time ./Client 1.1.1.1
real 0m4.014s
user 0m0.001s
sys 0m0.005s
[root@localhost linux]# time ./Client 192.168.44.1
real 0m4.427s
user 0m0.002s
sys 0m0.010s
多执行几次,几乎每次结果都是Windows机器完成时间比CentOS多大约0.4秒!Why??
我来通过tcpdump来看个究竟,通过tcpdump输出的时间戳可以看出详细的传输时间分布。我们首先看连接Windows服务的tcpdump输出,因为这个可能更符合大家的预期,也就是本文序言中展示的那3种想法中的其中之一:通过这个tcpdump输出,我们可以很明显看出在数据段传输和被确认之间的200ms延时,这个简单的200ms延迟看似正好符合Delay ACK的预期,即数据段被接收到之后,等待200ms之后再发送ACK。既然Windows已经“完美”呈现了Delay ACK应有的表现,那么我们来看看Linux的行为,看看这相差的0.4秒到底差别在哪里!
以下是连接Linux CentOS服务器的tcpdump输出:同样的代码怎么可能会有两种表现呢?看来我们要深挖!
通过仔细看以上连接CentOS服务器的tcpdump输出,针对收到的数据回复ACK的时间并不像Windows那样持续的持有200ms的延迟,而是断续地持有40ms延迟或者根本没有延迟,这种表现明显与Windows表现不同!!
既然程序一样,表现却不同,我们不得不深挖一下Linux和Windows关于Delay ACK协议栈实现的区别了,如果应用程序和网卡都一样的话,那么不同肯定是两者之间的东西造成的,这就是协议栈。
场景表现的分析
不管你信不信,Delay ACK在Linux系统中是没有一个统一配置的,几年前我曾经以为它是一个sysctl项,可是后来无没有找到,现在我常跟人讲,你没有能力通过一个配置彻底关掉或者开启Linux的Delay ACK。但是我现在知道我可以通过一个socket选项通过setsockopt调用来实现这个,这个选项就是TCP_QUICKACK选项:
TCP_QUICKACK (since Linux 2.4.4)
重要的发现有两个:
- 该选项是不稳定的。也就是说,即便你对一个TCP连接设置了该选项,那么后续的TCP逻辑(特定于Linux实现的Linux协议栈)也会取消你的设置,这意味着你同样没有能力通过一个socket选项彻底关掉或者开启TCP连接的Delay ACK。
- 该选项的不可移植。也就是说,你不能在除了Linux版本(内核高于2.4.4)之外的系统上使用该参数!这句话的言外之意是,这个参数是Linux实现Delay ACK的个性!即Linux实现的Delay ACK与其它系统的实现并无一致。
这也许也许有点令人费解,在解释这个之前,我们再看下Windows的对应解释。在此之前,请注意,不要将Delay ACK与TCP_NODELAY选项进行关联,后者控制的是发送端Nagle算法的行为,而本文说的是接收端行为。
在Windows中,有一个针对网卡的注册表项:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\Tcpip\Parameters\Interfaces{3A41D224-0292-449A-BA2F-96330637DBE8}\TcpAckFrequency
其中3A41D224-0292-449A-BA2F-96330637DBE8是相应网卡的ID值,这个TcpAckFrequency是针对这个网卡的全局配置,它指示了ACK的发送机制,它的值表示回复一个ACK所要收到的最多数据段数。比如TcpAckFrequency的值为2,则表示在一个超时时间段(默认200ms)内收到2个数据段才回复一个ACK,这个2也是一个默认值。如果我想取消掉Delay ACK,就把它设置成1,表示收到1个数据段就回复一个ACK!
设置完之后,禁用再启用网卡,上述注册表项即可重新加载而生效,无需重启系统。注意,如果网卡配置下面没有TcpAckFrequency,那么你手工添加它即可。
此时,如果再次执行测试,就会发现针对Windows服务器和CentOS服务器的测试结果基本一致了,不再有0.4秒的偏差了:
[root@localhost linux]# time ./Client 192.168.44.1
real 0m4.025s
user 0m0.000s
sys 0m0.008s
[root@localhost linux]# time ./Client 192.168.44.1
real 0m4.015s
user 0m0.000s
sys 0m0.003s
此时再看连接Windows服务器的tcpdump输出,便发现再没有Delay ACK现象了:
由是可见,这个TcpAckFrequency注册表选项,就是一个控制网卡范围内的TCP Delay ACK选项,设置成1表示禁用了Delay ACK,而设置成N则表示200ms内收到N个数据段方才回复一个ACK!
通过tcpdump抓包,我们看到,Windows的Delay ACK是网卡范围内全局生效的,即,如果配置TcpAckFrequency为2(默认情况),那么无论如何都是收到2个段才会回复一个ACK,即便这会严重影响响应性时延也毫不变通,一切都由该参数决定。由于我没有精力和必要对Windows协议栈进行进一步的Hack,所以对Windows的Delay ACK分析也就到此为止了。
然而相同的代码在Linux中表现却是另一番景象。它好像并不受某一个参数或者某个socket选项的影响(正如TCP_QUICKACK的manual里所述,其实如果你在服务器端的accept调用后添加设置TCP_QUICKACK选项,情况也是一样的。)。也就是说,Linux中的Delay ACK不是配置参数和socket选项决定的,而是自适应的,在这个自适应机制的范围内,系统依然提供了一个临时的开启/关闭Delay ACK的开关,即TCP_QUICKACK选项!
解析自适应Delay ACK
再次声明,本文仅仅单独解释Delay ACK,无关Nagle算法。
抛开与Nagle算法一起使用时带来问题的那些复杂场景不说,Delay ACK的期待是数据捎带ACK,如果TCP被设计成数据无法捎带ACK,那么Delay ACK的意义就只剩下与Nagle合力提高吞吐率了。幸运的是,TCP可以由数据捎带ACK,在这个意义上,Delay ACK的意义就在于
这个意义如下图所示:我们可以看出,在TCP双向数据传输是一来一回的场景下,Delay ACK可以省去所有的纯ACK段的发送。典型的比如远程终端登录(需要回显的如telnet,ssh之类)。
Linux为这种双向数据传输取了个名字,叫做pingpong。上图描述的就是一个完美的pingpong模式,对于一条TCP连接的任意一端来讲,pingpong模式指的就是“R-W-R-W-R-W-R-W...”(其中R代表Read,W代表Write,简称RW模式)模式,但是事实上,在现实中,这种完美适应Delay ACK的RW模式几乎不存在,如果出现RRW模式恰逢对端启用了Nagle的话,就会出现问题,因此,不管是开启还是关闭Delay ACK,都无法完美平衡ACK开销与传输延迟之间的矛盾,自适应机制势在必行。
Linux实现的其自适应Delay ACK换句话说就是Linux的协议栈可以自动识别当前是否是pingpong(即RW场景或者说完全交互场景)场景,从而依照这个判断来动态开启或者关闭Delay ACK。仍然以上图为模版,我加一些细节,大家也许就可以看到究竟了:道理很简单,如果TCP接收端的协议栈可以在收到数据的“一段时间”内探测到了自身发送了数据,那么pingpong就会设置成1,此时显然就可以走Delay ACK的逻辑了,反之,如果TCP接收端协议栈发现超过“一段时间”都没有数据发送,那么自然会将pingpong设置成0,上述的“一段时间”叫做ATO,至于说如何探测到没有数据发送,很简单,那就是定时器超时。
本质上,TCP之所以使能Delay ACK,其实并不是真的想ACK被Delay,而是期望数据的发送可以把ACK捎带过去,如果没有数据可发送,在ATO时间过后,ACK还是要发送的,毕竟ACK就是TCP的时钟。因此反过来可以下结论,如果ATO之后没有数据发送,而是发送了Delay ACK,那就说明该TCP传输模式此刻并不是pingpong模式,所以说,在Delay ACK定时器超时后,需要禁用pingpong模式,即把pingpong设置成0,以表示后续的ACK不能再Delay了,直到发现该TCP连接重新进入pingpong模式。
一条TCP连接的pingpong最简单状态图如下:以上就是Linux自适应Delay ACK的几乎全部了,说“几乎”是因为它还存在几个细节,但这些细节不是重点,所以说我们只能简要叙述一下。但是有一个细节必须在这里详述,这就是QUICK计数器的细节。
关于QUICK计数器
Linux虽然可以自适应地根据pingpong的取值在Delay ACK与否之间切换,但是pingpong并不是唯一的依据,有的时候,即便pingpong为0证明了此时是非交互模式,也要启用Delay ACK。什么情况呢?
我们知道,即时的ACK发送可以诱发发送端发送更多的数据,然而有的时候,接收端却不希望发送端发送太多的数据,比如接收端接收缓存内存吃紧的时候,这可能是由于应用程序读取数据过慢导致的,并且此时很有可能接收端并没有在发送数据,因此不符合Delay ACK的条件,但是由于接收端希望减缓发送端的发送,这种情况下,依然要Delay ACK。
Linux为TCP的Delay ACK维护了一个计数器QUICK,该计数器表示在非Delay ACK模式下,发送的即时ACK的数量,也就是即时ACK的配额,在pingpong为0的前提下,只有QUICK持有配额(不为0),该即时ACK才可以被发送出去,否则该ACK会被Delay!
QUICK计数器值在什么情况下会增减呢?
- 在连接初始化后,当第一次收到数据的时候
此时会将QUICK配额增加到最多16个段。配备16个段的配额是为了照顾慢启动,保证发送端在慢启动阶段时,接收端有足够的QUIICK配额发送即时的ACK。 - 当自从上一次接收到数据到现在又一次收到数据的时间间隔太久的时候
此时会将QUICK配额增加到最多16个段。此时配备足量的QUICK配额是为了发送即时的ACK以促使发送端尽快发送数据。 - 当接收端窗口缩减的时候
此时会将QUIICK配额清零。这种情况下,内存可以已经吃紧,尽量延缓接收数据是有益的,所以要减缓TCP时钟,延迟ACK的发送。 - 当接收端窗口小于接收端缓存一半的时候
此时会将QUIICK配额清零。请参见上述第4点。
本图考虑到了大多数的情况。我想Linux能做到如此,也算可以了,更细节的东西,随着内核版本不断变化的东西,我也不想细讲了。
到此为止,大家也许会认为,是否Delay ACK是由pingpong值以及QUICK值平等决定的,其各自权力就像古罗马保民官一样,各持一票,一票否决。然则非也!大多数情况下,Linux会将“禁用Delay ACK”作为主线!这就涉及到了Linux TCP Delay ACK的Oneshot特性。
自适应Delay ACK的Oneshot特性
如果Linux在pingpong为1时,启用了Delay ACK,那么待Delay ACK定时器到期时发送真正的Delay ACK的时候,说明pingpong为1是假的,此时就会取消掉pingpong的1值!也就是说,pingpong=1这件事碰到ATO过期,就会将pingpong重置为0!这就是Delay ACK的Oneshot特性,Delay ACK只会触发一次,当它真的被触发了,它也就被清除了!
在这个Oneshot的特性下,我们只需要关注“什么时候开启Delay ACK”即可:
- pingpong为1的时候,说明在交互模式,启用Delay ACK;
- pingpong为0,但QUICK配额不足的时候,说明接收端要主动延迟发送端的数据发送,启用Delay ACK。
自适应Delay ACK的另一些细节
- 慢启动的问题
在连接刚开始的慢启动阶段,数据总是希望被尽可能快的发送,因此ACK的及时性特别重要,除此之外,在对端发现丢包到恢复后的慢启动阶段,依然需要尽快的ACK来诱导数据尽快的发送,这个如何被数据的接收端所感知呢?答案是无法感知!非常遗憾!(尽管QUICK计数器设置为MAX 16是多么的意义重大!) - 分配纯ACK失败后Delay ACK对其的接管
当你无论如何想要发送一个纯ACK的时候,你就需要申请一个skb(当然,本节是针对Linux内核实现的,不要扯DPDK这种忽悠温州皮鞋厂老板的傻逼技术),如何申请失败会怎样呢?Linux的做法非常巧妙,即,如果申请内存失败,那么就把这个ACK作为一个Delay ACK在此后的某个时间(大约ATO)发出!
与Delay ACK相关的几个内核函数
- tcp_enter_quickack_mode
该函数将pingpong设置为0,取消了交互模式,并且给予了QUICK配额 - if (skb_queue_empty(&tp->out_of_order_queue))
当空洞被填补,那么:inet_csk(sk)->icsk_ack.pingpong = 0; - tcp_delack_timer
延迟定时器到期,说明没有数据主动发送,不是交互模式,设置pingpong为0。 - tcp_event_data_sent
当本端主动发送数据时:
if ((u32)(now - icsk->icsk_ack.lrcvtime) < icsk->icsk_ack.ato)
icsk->icsk_ack.pingpong = 1; - tcp_event_data_recv
当第一次收到数据时,会增加QUICK配额,至于pingpong,后续发数据时再判断。 - tcp_event_ack_sent
ACK发送时,不管是立即的ACK还是延迟ACK,都会取消Delay ACK定时器,并减去QUICK的相应配额。
作者:dog250
来源:CSDN
原文:https://blog.csdn.net/dog250/article/details/52664508