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)。可以看到,这种情况对于轻负载的网络来说,可能还可以接受,但是对于重负载的网络而言,就极有可能承载不了而轻易的发生拥塞瘫痪。
针对上面提到的这个状况,Nagle算法的改进在于:如果发送端欲多次发送包含少量字符的数据包(一般情况下,后面统一称长度小于MSS的数据包为小包,与此相对,称长度等于MSS的数据包为大包,为了某些对比说明,还有中包,即长度比小包长,但又不足一个MSS的包),则发送端会先将第一个小包发送出去,而将后面到达的少量字符数据都缓存起来而不立即发送,直到收到接收端对前一个数据包报文段的ACK确认、或当前字符属于紧急数据,或者积攒到了一定数量的数据(比如缓存的字符数据已经达到数据包报文段的最大长度)等多种情况才将其组成一个较大的数据包发送出去,具体有哪些情况,我们来看看内核实现:
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 RFC413TCP_NODELAY详解.
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:        }
这一段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确认)),那么缓存数据而不立即发送。
TCP_NODELAY详解 
上左图(台式主机图样为发送端,又叫客户端,服务器主机图样为接收端,又叫服务器)是未开启Nagle算法的情况,此时客户端应用层下传的数据包被立即发送到网络上(暂不考虑发送窗口与接收窗口这些固有限制,下同),而不管该数据包的大小如何,因此在网络里就有可能同时存在该连接的多个小包;而如上右图所示上,在未收到服务器对第一个包的ACK确认之前,客户端应用层下传的数据包被缓存了起来,当收到ACK确认之后(图中给的情况是这种,当然还有其他情况,前面已经详细描述过)才发送出去,这样不仅总包数由原来的3个变为2个,网络负载降低,与此同时,客户端和服务器都只需处理两个包,消耗的CPU等资源也减少了。
Nagle算法在一些场景下的确能提高网络利用率、降低包处理(客户端或服务器)主机资源消耗并且工作得很好,但是在某些场景下却又弊大于利,要说清楚这个问题需要引入另一个概念,即延迟确认(Delayed ACK)。延迟确认是提高网络利用率的另一种优化,但它针对的是ACK确认包。我们知道,对于TCP协议而言,正常情况下,接收端会对它收到的每一个数据包向发送端发出一个ACK确认包(如前面图示那样);而一种相对的优化就是把ACK延后处理,即ACK与数据包或窗口更新通知包等一起发送(文档RFC 1122),当然这些数据包都是由接收端发送给发送端(接收端和发送端只是一个相对概念)的:
TCP_NODELAY详解 
上左图是一般情况,上右图(这里只画出了ACK延迟确认机制中的两种情况:通过反向数据携带ACK和超时发送ACK)中,数据包A的ACK是通过接收端发回给发送端的数据包a携带一起过来的,而对应的数据包a的ACK是在等待超时之后再发送的。另外,虽然RFC 1122标准文档上,超时时间最大值是500毫秒,但在实际实现中最大超时时间一般为200毫秒(并不是指每一次超时都要等待200毫秒,因为在收到数据时,定时器可能已经经历一些时间了,在最坏情况的最大值也就是200毫秒,平均等待超时值为100毫秒),比如在linux3.4.4有个TCP_DELACK_MAX的宏标识该超时最大值:
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算法与ACK延迟确认的相互作用,仍然举个例子来讲,如果发送端暂有一段数据要发送给接收端,这段数据的长度不到最大两个包,也就是说,根据Nagle算法,发送端发出去第一个数据包后,剩下的数据不足以组成一个可立即发送的数据包(即剩余数据长度没有大于等于MSS),因此发送端就会等待,直到收到接收端对第一个数据包的ACK确认或者应用层传下更多需要发送的数据等(这里暂只考虑第一个条件,即收到ACK);而在接收端,由于ACK延迟确认机制的作用,它不会立即发送ACK,而是等待,直到(具体情况请参考内核函数tcp_send_delayed_ack(),由于涉及到情况太过复杂,并且与当前内容关系不大,所以略过,我们仅根据RFC 1122来看):1,收到发送端的第二个大数据包;2,等待超时(比如,200毫秒)。当然,如果本身有反向数据包要发送,那么可以携带ACK,但是在最糟的情况下,最终的结果就是发送端的第二个数据包需要等待200毫秒才能被发送到网络上。而在像HTTP这样的应用里,某一时刻的数据基本是单向的,所以出现最糟情况的概率非常的大,而且第二个数据包往往用于标识这一个请求或响应的成功结束,如果请求和响应都要超时等待的话,那么时延就得增大400毫秒。
针对在上面这种场景下Nagle算法缺点改进的详细情况描述在文档:http://tools.ietf.org/id/draft-minshall-nagle-01.txt里,在linux内核里也已经应用了这种改进,也就是前面未曾详细讲解的函数tcp_minshall_check():
1376:        Filename : \linux-3.4.4\net\ipv4\tcp_output.c
1377:        /* Minshall's variant of the Nagle send check. */
1378:        static inline int tcp_minshall_check(const struct tcp_sock *tp)
1379:        {
1380:                return after(tp->snd_sml, tp->snd_una) &&
1381:                        !after(tp->snd_sml, tp->snd_nxt);
1382:        }
函数名是按改进提出者的姓名来命名的,这个函数的实现很简单,但要理解它必须先知道这些字段的含义(RFC 793、RFC 1122):tp->snd_nxt,下一个待发送的字节(序号,后同);tp->snd_una,下一个待确认的字节,如果它的值等于tp->snd_nxt,则表示所有已发数据都已经得到了确认;tp->snd_sml,已经发出去的最近的一个小包的最后一个字节(注意,不一定是已确认)。具体图示如下:
TCP_NODELAY详解 
总结前面所有介绍的内容,Minshall对Nagle算法所做的改进简而言之就是一句话:在判断当前包是否可发送时,只需检查最近的一个小包是否已经确认(其它需要判断的条件,比如包长度是否大于MSS等这些没变,这里假定判断到最后,由此处决定是否发送),如果是,即前面提到的tcp_minshall_check(tp)函数返回值为假,从而函数tcp_nagle_check()返回0,那么表示可以发送(前面图示里的上图),否则延迟等待(前面图示里的下图)。基于的原理很简单,既然发送的小包都已经确认了,也就是说网络上没有当前连接的小包了,所以发送一个即便是比较小的数据包也无关大碍,同时更重要的是,这样做的话,缩短了延迟,提高了带宽利用率。
那么对于前面那个例子,由于第一个数据包是大包,所以不管它所对应的ACK是否已经收到都不影响对是否发送第二个数据包所做的检查与判断,此时因为所有的小包都已经确认(其实是因为本身就没有发送过小包),所以第二个包可以直接发送而无需等待。
传统Nagle算法可以看出是一种包-停-等协议,它在未收到前一个包的确认前不会发送第二个包,除非是“逼不得已”,而改进的Nagle算法是一种折中处理,如果未确认的不是小包,那么第二个包可以发送出去,但是它能保证在同一个RTT内,网络上只有一个当前连接的小包(因为如果前一个小包未被确认,不会发出第二个小包);但是,改进的Nagle算法在某些特殊情况下反而会出现不利,比如下面这种情况(3个数据块相继到达,后面暂时也没有其他数据到达),传统Nagle算法只有一个小包,而改进的Nagle算法会产生2个小包(第二个小包是延迟等待超时产生),但这并没有特别大的影响(所以说是它一种折中处理):
TCP_NODELAY详解 
TCP中的Nagle算法默认是启用的,但是它并不是适合任何情况,对于telnet或rlogin这样的远程登录应用的确比较适合(原本就是为此而设计),但是在某些应用场景下我们却又需要关闭它。在链接:http://www.isi.edu/lsam/publicat ... ractions/node2.html里提到Apache对HTTP持久连接(Keep-Alive,Prsistent-Connection)处理时凸现的奇数包&结束小包问题(The Odd/Short-Final-Segment Problem),这是一个并的关系,即问题是由于已有奇数个包发出,并且还有一个结束小包(在这里,结束小包并不是指带FIN旗标的包,而是指一个HTTP请求或响应的结束包)等待发出而导致的。我们来看看具体的问题详情,以3个包+1个结束小包为例,下图是一种可能发生的发包情况:
TCP_NODELAY详解 
最后一个小包包含了整个响应数据的最后一些数据,所以它是结束小包,如果当前HTTP是非持久连接,那么在连接关闭时,最后这个小包会立即发送出去,这不会出现问题;但是,如果当前HTTP是持久连接(非pipelining处理,pipelining仅HTTP 1.1支持,并且目前有相当一部分陈旧但仍在广泛使用中的浏览器版本尚不支持,nginx目前对pipelining的支持很弱,它必须是前一个请求完全处理完后才能处理后一个请求),即进行连续的Request/Response、Request/Response、…,处理,那么由于最后这个小包受到Nagle算法影响无法及时的发送出去(具体是由于客户端在未结束上一个请求前不会发出新的request数据,导致无法携带ACK而延迟确认,进而导致服务器没收到客户端对上一个小包的的确认导致最后一个小包无法发送出来),导致第n次请求/响应未能结束,从而客户端第n+1次的Request请求数据无法发出。
TCP_NODELAY详解 
正是由于会有这个问题,所以遇到这种情况,nginx就会主动关闭Nagle算法,我们来看nginx代码:
2436:        Filename : \linux-3.4.4\net\ipv4\tcp_output.c
2437:        static void
2438:        ngx_http_set_keepalive(ngx_http_request_t *r)
2439:        {
2440:        …
2623:            if (tcp_nodelay
2624:                && clcf->tcp_nodelay
2625:                && c->tcp_nodelay == NGX_TCP_NODELAY_UNSET)
2626:            {
2627:                ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "tcp_nodelay"TCP_NODELAY详解;
2628:        
2629:                if (setsockopt(c->fd, IPPROTO_TCP, TCP_NODELAY,
2630:                               (const void *) &tcp_nodelay, sizeof(int))
2631:                    == -1)
2632:                {
2633:        …
2646:                c->tcp_nodelay = NGX_TCP_NODELAY_SET;
2647:            }
Nginx执行到这个函数内部,就说明当前连接是持久连接。第2623行的局部变量tcp_nodelay是用于标记TCP_CORK选项的,由配置指令tcp_nopush指定,默认情况下为off,在linux下,nginx把TCP_NODELAY和TCP_CORK这两个选项完全互斥使用(事实上它们可以一起使用,下一节详细描述),禁用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选项),最后的响应数据会被立即发送出去,从而解决了前面提到的可能问题。

http://lenky.info/ebook/

上一篇:DirectShow设置采集帧率码率YUV<转>


下一篇:C# 深入浅出 委托与事件