天天看點

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 RFC413

TCP_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毫秒。

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_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選項),最後的響應資料會被立即發送出去,進而解決了前面提到的可能問題。

<a href="http://lenky.info/ebook/" target="_blank">http://lenky.info/ebook/</a>