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.
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確認)),那麼緩存數據而不立即發送。
image002.jpg
2012-08-25 16:38 上傳
下載附件 (19.94 KB)

上左圖(臺式主機圖樣爲發送端,又叫客戶端,服務器主機圖樣爲接收端,又叫服務器)是未開啓Nagle算法的情況,此時客戶端應用層下傳的數據包被立即發送到網絡上(暫不考慮發送窗口與接收窗口這些固有限制,下同),而不管該數據包的大小如何,因此在網絡裏就有可能同時存在該連接的多個小包;而如上右圖所示上,在未收到服務器對第一個包的ACK確認之前,客戶端應用層下傳的數據包被緩存了起來,當收到ACK確認之後(圖中給的情況是這種,當然還有其他情況,前面已經詳細描述過)才發送出去,這樣不僅總包數由原來的3個變爲2個,網絡負載降低,與此同時,客戶端和服務器都只需處理兩個包,消耗的CPU等資源也減少了。
Nagle算法在一些場景下的確能提高網絡利用率、降低包處理(客戶端或服務器)主機資源消耗並且工作得很好,但是在某些場景下卻又弊大於利,要說清楚這個問題需要引入另一個概念,即延遲確認(Delayed ACK)。延遲確認是提高網絡利用率的另一種優化,但它針對的是ACK確認包。我們知道,對於TCP協議而言,正常情況下,接收端會對它收到的每一個數據包向發送端發出一個ACK確認包(如前面圖示那樣);而一種相對的優化就是把ACK延後處理,即ACK與數據包或窗口更新通知包等一起發送(文檔RFC 1122),當然這些數據包都是由接收端發送給發送端(接收端和發送端只是一個相對概念)的:
image004.jpg
2012-08-25 16:38 上傳
下載附件 (14.18 KB)

上左圖是一般情況,上右圖(這裏只畫出了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,已經發出去的最近的一個小包的最後一個字節(注意,不一定是已確認)。具體圖示如下:
image006.jpg
2012-08-25 16:39 上傳
下載附件 (17.58 KB)

總結前面所有介紹的內容,Minshall對Nagle算法所做的改進簡而言之就是一句話:在判斷當前包是否可發送時,只需檢查最近的一個小包是否已經確認(其它需要判斷的條件,比如包長度是否大於MSS等這些沒變,這裏假定判斷到最後,由此處決定是否發送),如果是,即前面提到的tcp_minshall_check(tp)函數返回值爲假,從而函數tcp_nagle_check()返回0,那麼表示可以發送(前面圖示裏的上圖),否則延遲等待(前面圖示裏的下圖)。基於的原理很簡單,既然發送的小包都已經確認了,也就是說網絡上沒有當前連接的小包了,所以發送一個即便是比較小的數據包也無關大礙,同時更重要的是,這樣做的話,縮短了延遲,提高了帶寬利用率。
那麼對於前面那個例子,由於第一個數據包是大包,所以不管它所對應的ACK是否已經收到都不影響對是否發送第二個數據包所做的檢查與判斷,此時因爲所有的小包都已經確認(其實是因爲本身就沒有發送過小包),所以第二個包可以直接發送而無需等待。
傳統Nagle算法可以看出是一種包-停-等協議,它在未收到前一個包的確認前不會發送第二個包,除非是“逼不得已”,而改進的Nagle算法是一種折中處理,如果未確認的不是小包,那麼第二個包可以發送出去,但是它能保證在同一個RTT內,網絡上只有一個當前連接的小包(因爲如果前一個小包未被確認,不會發出第二個小包);但是,改進的Nagle算法在某些特殊情況下反而會出現不利,比如下面這種情況(3個數據塊相繼到達,後面暫時也沒有其他數據到達),傳統Nagle算法只有一個小包,而改進的Nagle算法會產生2個小包(第二個小包是延遲等待超時產生),但這並沒有特別大的影響(所以說是它一種折中處理):
image008.jpg
2012-08-25 16:39 上傳
下載附件 (9.75 KB)

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個結束小包爲例,下圖是一種可能發生的發包情況:
10.PNG
2012-08-25 16:57 上傳
下載附件 (31.46 KB)

最後一個小包包含了整個響應數據的最後一些數據,所以它是結束小包,如果當前HTTP是非持久連接,那麼在連接關閉時,最後這個小包會立即發送出去,這不會出現問題;但是,如果當前HTTP是持久連接(非pipelining處理,pipelining僅HTTP 1.1支持,並且目前有相當一部分陳舊但仍在廣泛使用中的瀏覽器版本尚不支持,nginx目前對pipelining的支持很弱,它必須是前一個請求完全處理完後才能處理後一個請求),即進行連續的Request/Response、Request/Response、…,處理,那麼由於最後這個小包受到Nagle算法影響無法及時的發送出去(具體是由於客戶端在未結束上一個請求前不會發出新的request數據,導致無法攜帶ACK而延遲確認,進而導致服務器沒收到客戶端對上一個小包的的確認導致最後一個小包無法發送出來),導致第n次請求/響應未能結束,從而客戶端第n+1次的Request請求數據無法發出。
image012.jpg
2012-08-25 16:40 上傳
下載附件 (18 KB)

正是由於會有這個問題,所以遇到這種情況,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";
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選項),最後的響應數據會被立即發送出去,從而解決了前面提到的可能問題。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章