Socket选项系列之TCP_NODELAY

在网络拥塞控制领域,咱们知道有一个很是有名的算法叫作Nagle算法(Nagle algorithm),这是使用它的发明人John Nagle的名字来命名的,John Nagle在1984年首次用这个算法来尝试解决福特汽车公司的网络拥塞问题(RFC 896),该问题的具体描述是:若是咱们的应用程序一次产生1个字节的数据,而这个1个字节数据又以网络数据包的形式发送到远端服务器,那么就很容易致使网络因为太多的数据包而过载。好比,当用户使用Telnet链接到远程服务器时,每一次击键操做就会产生1个字节数据,进而发送出去一个数据包,因此,在典型状况下,传送一个只拥有1个字节有效数据的数据包,却要发费40个字节长包头(即ip头20字节+tcp头20字节)的额外开销,这种有效载荷(payload)利用率极其低下的状况被统称之为愚蠢窗口症候群(Silly Window Syndrome)。能够看到,这种状况对于轻负载的网络来讲,可能还能够接受,可是对于重负载的网络而言,就极有可能承载不了而轻易的发生拥塞瘫痪。html

针对上面提到的这个情况,Nagle算法的改进在于:若是发送端欲屡次发送包含少许字符的数据包(通常状况下,后面统一称长度小于MSS的数据包为小包,与此相对,称长度等于MSS的数据包为大包,为了某些对比说明,还有中包,即长度比小包长,但又不足一个MSS的包;MSS,TCP最大分段大小,以太网下通常就是1460字节。),则发送端会先将第一个小包发送出去,而将后面到达的少许字符数据都缓存起来而不当即发送,直到收到接收端对前一个数据包报文段的ACK确认、或当前字符属于紧急数据,或者积攒到了必定数量的数据(好比缓存的字符数据已经达到数据包报文段的最大长度)等多种状况才将其组成一个较大的数据包发送出去,具体有哪些状况,咱们来看看内核(以linux-3.4.4为例,后同)实现:java

1383:   Filename : \linux-3.4.4\net\ipv4\tcp_output.c
1384:   /* Return 0, if packet can be sent now without violation Nagle's rules:
1385:    * 1. It is full sized.
1386:    * 2. Or it contains FIN. (already checked by caller)
1387:    * 3. Or TCP_CORK is not set, and TCP_NODELAY is set.
1388:    * 4. Or TCP_CORK is not set, and all sent packets are ACKed.
1389:    *    With Minshall's modification: all sent small packets are ACKed.
1390:    */
1391:   static inline int tcp_nagle_check(const struct tcp_sock *tp,
1392:                     const struct sk_buff *skb,
1393:                     unsigned mss_now, int nonagle)
1394:   {
1395:       return skb->len < mss_now &&
1396:           ((nonagle & TCP_NAGLE_CORK) ||
1397:            (!nonagle && tp->packets_out && tcp_minshall_check(tp)));
1398:   }
1399:   
1400:   /* Return non-zero if the Nagle test allows this packet to be
1401:    * sent now.
1402:    */
1403:   static inline int tcp_nagle_test(const struct tcp_sock *tp, const struct sk_buff *skb,
1404:                    unsigned int cur_mss, int nonagle)
1405:   {
1406:       /* Nagle rule does not apply to frames, which sit in the middle of the
1407:        * write_queue (they have no chances to get new data).
1408:        *
1409:        * This is implemented in the callers, where they modify the 'nonagle'
1410:        * argument based upon the location of SKB in the send queue.
1411:        */
1412:       if (nonagle & TCP_NAGLE_PUSH)
1413:           return 1;
1414:   
1415:       /* Don't use the nagle rule for urgent data (or for the final FIN).
1416:        * Nagle can be ignored during F-RTO too (see RFC4138).
1417:        */
1418:       if (tcp_urg_mode(tp) || (tp->frto_counter == 2) ||
1419:           (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN))
1420:           return 1;
1421:   
1422:       if (!tcp_nagle_check(tp, skb, cur_mss, nonagle))
1423:           return 1;
1424:   
1425:       return 0;
1426:   }


上左图(台式主机图样为发送端,又叫客户端,服务器主机图样为接收端,又叫服务器)是未开启Nagle算法的状况,此时客户端应用层下传的数据包被当即发送到网络上(暂不考虑发送窗口与接收窗口这些固有限制,下同),而无论该数据包的大小如何,所以在网络里就有可能同时存在该链接的多个小包;而如上右图所示上,在未收到服务器对第一个包的ACK确认以前,客户端应用层下传的数据包被缓存了起来,当收到ACK确认以后(图中给的状况是这种,固然还有其余状况,前面已经详细描述过)才发送出去,这样不只总包数由原来的3个变为2个,网络负载下降,与此同时,客户端和服务器都只需处理两个包,消耗的CPU等资源也减小了。这一段Linux内核代码很是容易看,由于注释代码足够的多。从函数tcp_nagle_test()看起,第1412行是直接进行参数判断,若是在外部(也就是调用者)主动设置了TCP_NAGLE_PUSH旗标,好比主动禁止Nagle算法或主动拔走塞子(下一节TCP_CORK内容)或明确是链接最后一个包(好比链接close()前发出的数据包),此时固然是返回1从而把数据包当即发送出去;第1418-1420行代码处理的是特殊包,也就是紧急数据包、带FIN旗标的结束包以及带F-RTO旗标的包;第1422行进入到tcp_nagle_check()函数进行判断,该函数的头注释有点混乱而不太清楚,我再逐句代码解释一下,首先要看明白若是该函数返回1,则表示该数据包不当即发送;再看具体实现就是:skb->len < mss_now为真表示若是包数据长度小于当前MSS;nonagle & TCP_NAGLE_CORK为真表示当前已主动加塞或明确标识当即还会有数据过来(内核表示为MSG_MORE);!nonagle为真表示启用Nagle算法;tp->packets_out为真表示存在有发出去的数据包没有被ACK确认;tcp_minshall_check(tp)是Nagle算法的改进,先直接认为它与前一个判断相同,具体后续再讲。把这些条件按与或组合起来就是:若是包数据长度小于当前MSS &&((加塞 || 有数据立刻过来)||(启用Nagle算法 && 存在有发出去的数据包没有被ACK确认)),那么缓存数据而不当即发送:
node

Nagle算法在一些场景下的确能提升网络利用率、下降包处理(客户端或服务器)主机资源消耗而且工做得很好,可是在某些场景下却又弊大于利,要说清楚这个问题须要引入另外一个概念,即延迟确认(Delayed ACK)。延迟确认是提升网络利用率的另外一种优化,但它针对的是ACK确认包。咱们知道,对于TCP协议而言,正常状况下,接收端会对它收到的每个数据包向发送端发出一个ACK确认包(如前面图示那样);而一种相对的优化就是把ACK延后处理,即ACK与数据包或窗口更新通知包等一块儿发送(文档RFC 1122),固然这些数据包都是由接收端发送给发送端(接收端和发送端只是一个相对概念)的:linux

上左图是通常状况,上右图(这里只画出了ACK延迟确认机制中的两种状况:经过反向数据携带ACK和超时发送ACK)中,数据包A的ACK是经过接收端发回给发送端的数据包a携带一块儿过来的,而对应的数据包a的ACK是在等待超时以后再发送的。另外,虽然RFC 1122标准文档上,超时时间最大值是500毫秒,但在实际实现中最大超时时间通常为200毫秒(并非指每一次超时都要等待200毫秒,由于在收到数据时,定时器可能已经经历一些时间了,在最坏状况的最大值也就是200毫秒,平均等待超时值为100毫秒),好比在linux3.4.4有个TCP_DELACK_MAX的宏标识该超时最大值:nginx

115:    Filename : \linux-3.4.4\include\net\tcp.h
116:    #define TCP_DELACK_MAX  ((unsigned)(HZ/5))  /* maximal time to delay before sending an ACK */


针对在上面这种场景下Nagle算法缺点改进的详细状况描述在文档:http://tools.ietf.org/id/draft-minshall-nagle-01.txt里,在linux内核里也已经应用了这种改进,也就是前面不曾详细讲解的函数tcp_minshall_check():回过头来看Nagle算法与ACK延迟确认的相互做用,仍然举个例子来说,若是发送端暂有一段数据要发送给接收端,这段数据的长度不到最大两个包,也就是说,根据Nagle算法,发送端发出去第一个数据包后,剩下的数据不足以组成一个可当即发送的数据包(即剩余数据长度没有大于等于MSS),所以发送端就会等待,直到收到接收端对第一个数据包的ACK确认或者应用层传下更多须要发送的数据等(这里暂只考虑第一个条件,即收到ACK);而在接收端,因为ACK延迟确认机制的做用,它不会当即发送ACK,而是等待,直到(具体状况请参考内核函数tcp_send_delayed_ack(),因为涉及到状况太过复杂,而且与当前内容关系不大,因此略过,咱们仅根据RFC 1122来看):1,收到发送端的第二个大数据包;2,等待超时(好比,200毫秒)。固然,若是自己有反向数据包要发送,那么能够携带ACK,可是在最糟的状况下,最终的结果就是发送端的第二个数据包须要等待200毫秒才能被发送到网络上。而在像HTTP这样的应用里,某一时刻的数据基本是单向的,因此出现最糟状况的几率很是的大,并且第二个数据包每每用于标识这一个请求或响应的成功结束,若是请求和响应都要超时等待的话,那么时延就得增大400毫秒。算法


函数名是按改进提出者的姓名来命名的,这个函数的实现很简单,但要理解它必须先知道这些字段的含义(RFC 79三、RFC 1122):tp->snd_nxt,下一个待发送的字节(序号,后同);tp->snd_una,下一个待确认的字节,若是它的值等于tp->snd_nxt,则表示全部已发数据都已经获得了确认;tp->snd_sml,已经发出去的最近的一个小包的最后一个字节(注意,不必定是已确认)。具体图示以下:浏览器

总结前面全部介绍的内容,Minshall对Nagle算法所作的改进简而言之就是一句话:在判断当前包是否可发送时,只需检查最近的一个小包是否已经确认(其它须要判断的条件,好比包长度是否大于MSS等这些没变,这里假定判断到最后,由此处决定是否发送),若是是,即前面提到的tcp_minshall_check(tp)函数返回值为假,从而函数tcp_nagle_check()返回0,那么表示能够发送(前面图示里的上图),不然延迟等待(前面图示里的下图)。基于的原理很简单,既然发送的小包都已经确认了,也就是说网络上没有当前链接的小包了,因此发送一个即使是比较小的数据包也无关大碍,同时更重要的是,这样作的话,缩短了延迟,提升了带宽利用率。
那么对于前面那个例子,因为第一个数据包是大包,因此无论它所对应的ACK是否已经收到都不影响对是否发送第二个数据包所作的检查与判断,此时由于全部的小包都已经确认(实际上是由于自己就没有发送太小包),因此第二个包能够直接发送而无需等待。
传统Nagle算法能够看出是一种包-停-等协议,它在未收到前一个包的确认前不会发送第二个包,除非是“无可奈何”,而改进的Nagle算法是一种折中处理,若是未确认的不是小包,那么第二个包能够发送出去,可是它能保证在同一个RTT内,网络上只有一个当前链接的小包(由于若是前一个小包未被确认,不会发出第二个小包);可是,改进的Nagle算法在某些特殊状况下反而会出现不利,好比下面这种状况(3个数据块相继到达,后面暂时也没有其余数据到达),传统Nagle算法只有一个小包,而改进的Nagle算法会产生2个小包(第二个小包是延迟等待超时产生),但这并无特别大的影响(因此说是它一种折中处理):缓存

TCP中的Nagle算法默认是启用的,可是它并非适合任何状况,对于telnet或rlogin这样的远程登陆应用的确比较适合(本来就是为此而设计),可是在某些应用场景下咱们却又须要关闭它。在连接:http://www.isi.edu/lsam/publications/phttp_tcp_interactions/node2.html里提到Apache对HTTP持久链接(Keep-Alive,Prsistent-Connection)处理时凸现的奇数包&结束小包问题(The Odd/Short-Final-Segment Problem),这是一个并的关系,即问题是因为已有奇数个包发出,而且还有一个结束小包(在这里,结束小包并非指带FIN旗标的包,而是指一个HTTP请求或响应的结束包)等待发出而致使的。咱们来看看具体的问题详情,以3个包+1个结束小包为例,下图是一种可能发生的发包状况:服务器

最后一个小包包含了整个响应数据的最后一些数据,因此它是结束小包,若是当前HTTP是非持久链接,那么在链接关闭时,最后这个小包会当即发送出去,这不会出现问题;可是,若是当前HTTP是持久链接(非pipelining处理,pipelining仅HTTP 1.1支持,而且目前有至关一部分陈旧但仍在普遍使用中的浏览器版本尚不支持,nginx目前对pipelining的支持很弱,它必须是前一个请求彻底处理完后才能处理后一个请求),即进行连续的Request/Response、Request/Response、…,处理,那么因为最后这个小包受到Nagle算法影响没法及时的发送出去(具体是因为客户端在未结束上一个请求前不会发出新的request数据,致使没法携带ACK而延迟确认,进而致使服务器没收到客户端对上一个小包的的确认致使最后一个小包没法发送出来),致使第n次请求/响应未能结束,从而客户端第n+1次的Request请求数据没法发出:网络

正是因为有这个问题,因此若是可能会遇到这种状况,nginx就事前主动关闭Nagle算法.

Nginx执行到这个函数内部,就说明当前链接是持久链接。第2623行的局部变量tcp_nodelay是用于标记TCP_CORK选项的,由配置指令tcp_nopush指定,默认状况下为off,在linux下,nginx把TCP_NODELAY和TCP_CORK这两个选项彻底互斥使用(事实上,从内核版本2.5.71之后,它们就能够相互接合使用),禁用TCP_CORK选项时,局部变量tcp_nodelay值为1(从该变量能够看到,nginx对这两个选项的使用,TCP_CORK优先级别高于TCP_NODELAY);clcf->tcp_nodelay对应TCP_NODELAY选项的配置指令tcp_nodelay的配置值,默认状况下为1;c->tcp_nodelay用于标记当前是否已经对该套接口设置了TCP_NODELAY选项,第一次执行到这里时,值通常状况下也就是NGX_TCP_NODELAY_UNSET(除非不是IP协议等),由于只有此处一个地方设置TCP_NODELAY选项。因此,总体来看,若是此判断为真,因而第2629行对套接口设置TCP_NODELAY禁止Nagle算法(字段c->tcp_nodelay被赋值为NGX_TCP_NODELAY_SET,表示当前已经对该套接口设置了TCP_NODELAY选项),最后的响应数据会被当即发送出去,从而解决了前面提到的可能问题。

相关文章
相关标签/搜索