TCP之Nagle算法&&延迟ACK

 

糊涂窗口综合症和Nagle算法

 

 

 

  TCP/IP详解系列,关于tcp拥塞控制和数据流的地方讲的不细致,或许是涉及概念/算法太多,作者略去了一些对初学者来说比较陌生的细节吧。比如SWS未说明是什么就开始介绍其避免方法,还和nagle扯在了一起,直觉告诉我二者一定有猫腻,边搜索一下,果然很有收获。今天贴在这里,分享给大家。 

  关键字:糊涂窗口综合症  nagle算法  延迟ACK/clark算法   CORK选项

第一部分 糊涂窗口综合症

  当发送端应用进程产生数据很慢、或接收端应用进程处理接收缓冲区数据很慢,或二者兼而有之;就会使应用进程间传送的报文段很小,特别是有效载荷很小。 极端情况下,有效载荷可能只有1个字节;而传输开销有40字节(20字节的IP头+20字节的TCP头) 这种现象就叫糊涂窗口综合症。

发送端求解(negle)

  如果发送端为产生数据很慢的应用程序服务(典型的有telnet应用),例如,一次产生一个字节。这个应用程序一次将一个字节的数据写入发送端的TCP的缓存。如果发送端的TCP没有特定的指令,它就产生只包括一个字节数据的报文段。结果有很多41字节的IP数据报就在互连网中传来传去。解决的方法是防止发送端的TCP逐个字节地发送数据。必须强迫发送端的TCP收集数据,然后用一个更大的数据块来发送。发送端的TCP要等待多长时间呢?如果它等待过长,它就会使整个的过程产生较长的时延。如果它的等待时间不够长,它就可能发送较小的报文段,于是,Nagle找到了一个很好的解决方法,发明了Nagle算法。而他选择的等待时间是一个RTT,即下个ACK来到时。

接收端求解(delay-ack)

  接收端的TCP可能产生糊涂窗口综合症,如果它为消耗数据很慢的应用程序服务,例如,一次消耗一个字节。假定发送应用程序产生了1000字节的数据块,但接收应用程序每次只吸收1字节的数据。再假定接收端的TCP的输入缓存为4000字节。发送端先发送第一个4000字节的数据。接收端将它存储在其缓存中。现在缓存满了。它通知窗口大小为零,这表示发送端必须停止发送数据。接收应用程序从接收端的TCP的输入缓存中读取第一个字节的数据。在入缓存中现在有了1字节的空间。接收端的TCP宣布其窗口大小为1字节,这表示正渴望等待发送数据的发送端的TCP会把这个宣布当作一个好消息,并发送只包括一个字节数据的报文段。这样的过程一直继续下去。一个字节的数据被消耗掉,然后发送只包含一个字节数据的报文段。

  对于这种糊涂窗口综合症,即应用程序消耗数据比到达的慢,有两种建议的解决方法:
  1) Clark解决方法

      Clark解决方法是只要有数据到达就发送确认,但宣布的窗口大小为零,直到或者缓存空间已能放入具有最大长度的报文段,或者缓存空间的一半已经空了。
  2 )延迟确认ACK 

      这表示当一个报文段到达时并不立即发送确认。接收端在确认收到的报文段之前一直等待,直到入缓存有足够的空间为止。延迟的确认防止了发送端的TCP滑动其窗口。当发送端的TCP发送完其数据后,它就停下来了。这样就防止了这种症状。迟延的确认还有另一个优点:它减少了通信量。接收端不需要确认每一个报文段。但它也有一个缺点,就是迟延的确认有可能迫使发送端重传其未被确认的报文段。可以用协议来平衡这个优点和缺点,例如现在定义了确认的延迟不能超过500毫秒。

第二部分:治疗办法

   2.1 nagle算法

        TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。(一个连接会设置MSS参数,因此,TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据)。Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。
        Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。
        从Nagle算法生效情况的补集,可以侧面理解其本质,参考tcp_output.c文件里tcp_nagle_check函数:

      (1)如果包长度达到MSS,则允许发送;

      (2)如果该包含有FIN,则允许发送;

      (3)设置了TCP_NODELAY选项(意在禁止nagle),则允许发送;

      (4)未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送; 

      (5)上述条件都未满足,但发生了超时(一般设置延迟ACK,一般为200ms),则立即发送。

   通常我们比较关注的是情况(1)和(5)。

         Nagle算法只允许一个未被ACK的包存在于网络,它并不管包的大小,因此它事实上就是一个扩展的停-等协议,只不过它是基于包停-等的,而不是基于字节停-等的。Nagle算法完全由TCP协议的ACK机制决定,这会带来一些问题,比如如果对端ACK回复很快的话,Nagle事实上不会拼接太多的数据包,虽然避免了网络拥塞,网络总体的利用率依然很低。另外,他是一个自适应的方法,读者可以自己按上述规则试验一下。 

        Nagle算法是silly window syndrome(SWS)预防算法的一个半集,预防SWS不止nagle算法一个途径。SWS算法预防发送少量的数据,Nagle算法是其在发送方的实现,而接收方要做的时不要通告缓冲空间的很小增长,不通知小窗口,除非缓冲区空间有显著的增长。这里显著的增长定义为完全大小的段(MSS)或增长到大于最大窗口的一半。

注意:BSD的实现是允许在空闲链接上发送大的写操作剩下的最后的小段,也就是说,当超过1个MSS数据发送时,内核先依次发送完n个MSS的数据包,然后再发送尾部的小数据包,其间不再延时等待。(假设网络不阻塞且接收窗口足够大)

    TCP_NODELAY 选项

        默认情况下,发送数据采用Nagle 算法。这样虽然提高了网络吞吐量,但是实时性却降低了,在一些交互性很强的应用程序来说是不允许的,使用TCP_NODELAY选项可以禁止Negale 算法。此时,应用程序向内核递交的每个数据包都会立即发送出去。需要注意的是,虽然禁止了Negale 算法,但网络的传输仍然受到TCP确认延迟机制的影响。


 相关:linux环境下的TCP_CORK 选项 

        所谓的CORK就是塞子的意思,形象地理解就是用CORK将连接塞住,使得数据先不发出去,等到拔去塞子后再发出去。设置该选项后,内核会尽力把小数据包拼接成一个大的数据包(一个MTU)再发送出去,当然若一定时间后(一般为200ms,该值尚待确认),内核仍然没有组合成一个MTU时也必须发送现有的数据(不可能让数据一直等待吧)。
        然而,TCP_CORK的实现可能并不像你想象的那么完美,CORK并不会将连接完全塞住。内核其实并不知道应用层到底什么时候会发送第二批数据用于和第一批数据拼接以达到MTU的大小,因此内核会给出一个时间限制,在该时间内没有拼接成一个大包(努力接近MTU)的话,内核就会无条件发送。也就是说若应用层程序发送小包数据的间隔不够短时,TCP_CORK就没有一点作用,反而失去了数据的实时性(每个小包数据都会延时一定时间再发送)。

Nagle算法与CORK算法区别

  Nagle算法的初衷:避免发送大量的小包,防止小包泛滥于网络,理想情况下,对于一个TCP连接而言,网络上每次只能一个小包存在。它更多的是端到端意义上的优化。
  CORK算法的初衷:提高网络利用率,理想情况下,完全避免发送小包,仅仅发送满包以及不得不发的小包。

  很多人都把Nagle算法的目的理解为“提高网络利用率”,事实上,Nagle算法所谓的“提高网络利用率”只是它的一个副作用(Side effect...),Nagle算法的主旨在于“避免发送‘大量’的小包”。Nagle算法并没有阻止发送小包,它只是阻止了发送大量的小包!诚然,发送大量的小包是降低了网络利用率,但是,发送少量必须发送的小包也是对网络利用率的降低,想彻底提高网络利用率,为嘛不直接阻止小包发送呢?不管是大量小包还是少量小包,甚至一个小包也不让发送,这才是提高网络利用率的正解!是的,TCP_CORK就是做这个的。所以有人说,CORK选项是Nagle的增强,而实际上,它们是完全不同的两回事,初衷不同。

  Nagle算法和CORK算法着眼点不一样,Nagle算法主要避免网络因为太多的小包(协议头的比例非常之大)而拥塞,而CORK算法则是为了提高网络的利用率,使得总体上协议头占用的比例尽可能的小。如此看来这二者在避免发送小包上是一致的,在用户控制的层面上,Nagle算法完全不受用户socket的控制,你只能简单的设置TCP_NODELAY而禁用它,CORK算法同样也是通过设置或者清除TCP_CORK使能或者禁用之,然而Nagle算法关心的是网络拥塞问题,只要所有的ACK回来则发包,而CORK算法却可以关心内容,在前后数据包发送间隔很短的前提下(很重要,否则内核会帮你将分散的包发出),即使你是分散发送多个小数据包,你也可以通过使能CORK算法将这些内容拼接在一个包内,如果此时用Nagle算法的话,则可能做不到这一点。

    2.2 延迟ACK

        这里要说明一下,Nagle算法和延迟ACK作用在方向相反的数据包和针对该数据包的确认包上,因此它们的作用力会相悖,结果就是谁也不能发包。就像一根绳子上拴两只青蛙一样,被对方牵制谁也跑不了!关键点在于,小包的发送依赖于ACK,然而延迟ACK阻止了ACK的即时发送,形成了僵持状态。本来只是为了减少网络上小包的数量(再次强调Nagle算法以及延迟ACK的目的,注意,糊涂窗口综合症只是网络上小包泛滥的原因之一!),却人为引入了大量的延迟!

参考文献

再次谈谈TCP的Nagle算法与TCP_CORK选项

《延迟问题实测》

《tcp/ip详解卷2》  24章 TCP 传输控制协议

连续发送多份小数据时40ms延迟问题

 《so_linger选项关闭timewait》http://www.xuebuyuan.com/1783143.html

  《nginx实现的应用层linger选项》http://blog.csdn.net/wangpengqi/article/details/17245889 以及http://blog.csdn.net/fangru/article/details/9024759

 
 

 

1. Nagle算法:

是为了减少广域网的小分组数目,从而减小网络拥塞的出现;

该算法要求一个tcp连接上最多只能有一个未被确认的未完成的小分组,在该分组ack到达之前不能发送其他的小分组,tcp需要收集这些少量的分组,并在ack到来时以一个分组的方式发送出去;其中小分组的定义是小于MSS的任何分组;

该算法的优越之处在于它是自适应的,确认到达的越快,数据也就发哦送的越快;而在希望减少微小分组数目的低速广域网上,则会发送更少的分组;

2. 延迟ACK:

如果tcp对每个数据包都发送一个ack确认,那么只是一个单独的数据包为了发送一个ack代价比较高,所以tcp会延迟一段时间,如果这段时间内有数据发送到对端,则捎带发送ack,如果在延迟ack定时器触发时候,发现ack尚未发送,则立即单独发送;

延迟ACK好处:

(1) 避免糊涂窗口综合症;

(2) 发送数据的时候将ack捎带发送,不必单独发送ack;

(3) 如果延迟时间内有多个数据段到达,那么允许协议栈发送一个ack确认多个报文段;

3. 当Nagle遇上延迟ACK:

试想如下典型操作,写-写-读,即通过多个写小片数据向对端发送单个逻辑的操作,两次写数据长度小于MSS,当第一次写数据到达对端后,对端延迟ack,不发送ack,而本端因为要发送的数据长度小于MSS,所以nagle算法起作用,数据并不会立即发送,而是等待对端发送的第一次数据确认ack;这样的情况下,需要等待对端超时发送ack,然后本段才能发送第二次写的数据,从而造成延迟;

4. 关闭Nagle算法:

使用TCP套接字选项TCP_NODELAY可以关闭套接字选项;

如下场景考虑关闭Nagle算法:

(1) 对端不向本端发送数据,并且对延时比较敏感的操作;这种操作没法捎带ack;

(2) 如上写-写-读操作;对于此种情况,优先使用其他方式,而不是关闭Nagle算法:

--使用writev,而不是两次调用write,单个writev调用会使tcp输出一次而不是两次,只产生一个tcp分节,这是首选方法;

--把两次写操作的数据复制到单个缓冲区,然后对缓冲区调用一次write;

--关闭Nagle算法,调用write两次;有损于网络,通常不考虑;

5. 禁止Nagle和开启Nagle算法发送数据与确认示意图:

 
 
原文地址:https://www.cnblogs.com/williamjie/p/9390308.html