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

相關文章
相關標籤/搜索