連續發送多份小數據時40ms延遲問題


連續發送多份小數據時40ms延遲問題

 

以及TCP_NODELAY、TCP_CORK失效問題的定位與解決

Pyramid tandai@baidu.com

提到TCP_NODELAY和TCP_CORK,相信很多人都很熟悉。然而由於Linux實現上的問題,這兩個參數在實際使用中,並不像書里介紹的那么簡單。最近DTS在解決一個TCP超時問題時,對這兩個參數和它們背后所隱藏的問題有了比較深刻的認識,在此與同學們分享一下我們的經驗和教訓。

 

問題描述

和許多經典的分布式程序類似,DTS使用TCP長連接用於client和server的數據交互:client發送請求給server,然后等待server回應。有時候出於數據結構上的考慮,client需要先連續發送多份數據,再等待server的回應。測試發現這種情況下,server端有時會出現接收數據延遲。比如說某個case里,client會先發送275個字節,接着發送24個字節,然后再發送292字節數據等等;此時如果該TCP連接被復用過,則server端在收取24字節這批數據時會很容易出現40ms延遲。

由於client每次發送的數據都很小,很自然想到是nagle算法延遲了client端的數據發送,於是在client端和server端都設置了TCP_NODELAY。然而測試發現,此時server雖然順利接受了24字節數據,卻在接受隨后292字節數據時依然出現了40ms延遲。難道是數據太多導致TCP_NODELAY失效?因此又在client端添加了TCP_CORK選項:即如果client需要連續發送多次數據,則先關閉TCP_NODELAY,打開TCP_CORK;所有數據write完后,再關閉TCP_CORK,打開TCP_NODELAY。按照設想,client應該會把所有數據打包在一起發送,但測試結果依然和以前一樣,server還是在收取第三份數據時出現了40ms的延遲。

不得已使用tcpdump進行分析,結果如下:

18:18:01.640134 IP jx-dp-wk11.jx.baidu.com.36989 > tc-dpf-rd-in.tc.baidu.com.licensedaemon: P 551:826(275) ack 141 win 1460 <nop,nop,timestamp 2551499424 1712127318>
18:18:01.640151 IP jx-dp-wk11.jx.baidu.com.36989 > tc-dpf-rd-in.tc.baidu.com.licensedaemon: P 826:850(24) ack 141 win 1460 <nop,nop,timestamp 2551499424 1712127318>
18:18:01.680812 IP tc-dpf-rd-in.tc.baidu.com.licensedaemon > jx-dp-wk11.jx.baidu.com.36989: . ack 850 win 2252 <nop,nop,timestamp 1712127359 2551499424>
18:18:01.680818 IP jx-dp-wk11.jx.baidu.com.36989 > tc-dpf-rd-in.tc.baidu.com.licensedaemon: P 850:1142(292) ack 141 win 1460 <nop,nop,timestamp 2551499465 1712127359>

注意紅色的部分,可見client並沒有將所有數據打成一個包,每次write的數據還是作為單獨的包發送;此外,client在發送完24字節的數據后,一直等到server告知ack才接着發送剩下的292字節。由於server延遲了40ms才告知ack,因此導致了其接收292字節數據時也出現了40ms延遲。

既然查出了延遲是server端delayed ack的原因,通過設置server端TCP_QUICKACK,40ms延遲的問題得到了解決。

 

原因定位

雖然DTS的延時問題暫時得到了解決,但其內在原因卻使人百思不得其解:為什么TCP_NODELAY會失效?為什么TCP_CORK無作為?…… 在STL同學的幫助下,我們逐漸對這些困惑有了答案。

首先介紹下delayed ack算法:當協議棧接受到TCP數據時,並不一定會立刻發送ACK響應,而是傾向於等待一個超時或者滿足特殊條件時再發送。對於Linux實現,這些特殊條件如下:

1)收到的數據已經超過了full frame size

2)或者處於快速回復模式

3)或者出現了亂序的包

4)或者接收窗口的數據足夠多

如果接收方有數據回寫,則ACK也會搭車一起發送。當以上條件都不滿足時,接收方會延遲40ms再回應ACK。

 

  • 1. 為什么TCP_NODELAY失效

UNIX網絡編程這本書介紹說,TCP_NODELAY同時禁止了nagle算法和delayed ACK算法,因此小塊數據可以直接發送。然而Linux實現中,TCP_NODELAY只禁止了nagle算法。另一方面,協議棧在發送包的時候,不僅受到TCP_NODELAY的影響,還受到協議棧里面擁塞窗口的影響。由於server端delayed ack,client遲遲無法收到ack應答,擁塞窗口堵滿,從而無法繼續發送更多數據;一直到40ms后ack達到,才能繼續發送(題外話: TCP_NODELAY在FREEBSD上性能優於Linux上,因為FREEBSD並不像Linux一樣需要第一個包到達后就響應ACK)。

這也解釋了為什么延時現象在重用過的TCP連接上特別容易出現:目前使用的52bs內核中,連接剛建立時擁塞窗口默認是3,因此可以發送3個數據包,而后擁塞窗口變為2,就會導致第3個292字節的包發不出去。

 

  • 2. 為什么TCP_CORK失效

TCP_CORK會將發送端多份數據打成一個包,待到TCP_CORK關閉后一起發送。Linux Man手冊上也描述了TCP_CORK選項和TCP_NODELAY一起使用的情形。然而根據之前tcpdump的結果,client端設置TCP_CORK后並沒有發揮效果。繼續測試發現,只要設置過TCP_NODELAY選項,即使隨后關閉也會導致TCP_CORK無效;如果從未設置過TCP_NODELAY,則TCP_CORK可以產生效果。

根據STL同學對協議棧代碼的調研,發現這個是Linux實現上的問題。在內核中,設置啟動TCP_NODELAY選項后,內核會為socket增加兩個標志位TCP_NAGLE_OFF和TCP_NAGLE_PUSH,關閉TCP_NODELAY的時候,內核只去掉了TCP_NAGLE_OFF標志位。而在發包的時候判斷的卻恰恰是TCP_NAGLE_PUSH標志位,如果該位置位設置,就直接把包發出去,從而導致TCP_CORK發揮不了作用。這很可能是這一版本Linux內核實現上的bug。

 

  • 3. TCP_QUICKACK的作用和限制

前面介紹delayed ack算法時,講到協議棧迅速回復ack的情形之一就是進入到快速回復模式。而TCP_QUICKACK選項就是向內核建議進入快速回復模式。快速回復ack模式的判斷條件如下:(tp->ack.quick && tp->ack.pingpong),其中設置QUICKACK選項會置pingpong=0。

然而,隨着TCP連接的重用和數據的不斷收發,快速回復模式有可能失效。例如在后續的交互過程當中,pingpong變為1的條件就有:1.收到fin后;2. 發送方發送數據時,發現當前時間與上次接收數據的時間小於40ms。此外,發送方發現數據包帶有ack標志位時,也會減小ack.quick值。這些都會導致快速回復模式的退出。因此,即使每次接受數據前都設置TCP_QUICKACK選項,也不能完全解決delayed ack問題。

 

解決方案

經過上述的測試與分析,可以認識到當連續發送多個小數據時,TCP_NODELAY作用並不明顯,TCP_CORK無法像宣傳的那樣和TCP_NODELAY混合使用,而TCP_QUICKACK也不能完全解決問題。因此,我們最終的解決方案如下:

(1)在client端多次發送數據時,先打開TCP_CORK選項,發送完后再關閉TCP_CORK,將多份小數據打成一個包發送;此外,client端不能設置TCP_NODELAY選項,以避免TCP_CORK失效。

(2)server端開啟TCP_QUICKACK選項,盡量快速回復ack。

通過這個延時問題的解決,可以看到由於Linux實現策略上的問題,TCP_NODELAY和TCP_CORK還是暗藏了不少陷阱。實際應用中,其實也可以繞過這些參數,在應用層將多份數據序列化到一個buffer中,或者使用writev系列函數。然而,這些方法需要額外的內存拷貝,或者讓傳輸對象對外暴露過多的數據結構信息,並不一定容易實現,也會添加代碼重構的代價。

另一方面,考慮到那些使用TCP進行異步請求的應用,由於多個請求需要同時復用一個TCP連接,也很容易出現延時問題;而無論是通過TCP_CORK還是writev哪種方法,都不太適合這種異步場景。最近STL推出的新內核添加了一個禁止delayed ack的系統參數,使用該參數理論上講可以徹底根除40ms的延遲問題。

詳細信息參看http://stl.sys.baidu.com/admin/show.php?page=kernel_5600.html,在此也感謝stl的chenjian和liwen同學在這個問題上的幫忙!

 

反饋建議

 


Comments From sangwenfeng - 01 Jul 2009 
贊,深入,ub框架以前的40ms是不是也有隱患不徹底?

 


Comments From baonenghui - 01 Jul 2009 
ub的模式主要是一次請求一次交互,基本是不會TCP_NODELAY后還出現40ms延時。在TCP_NODELAY后還出現40ms延時一般都是對於連續發送小數據的情況下出現的。最早ub的40ms主要是client端沒有TCP_NODELAY設置產生的。

 


Comments From hongdingkun - 03 Jul 2009 
贊,學習了

 


Comments From baonenghui - 22 Oct 2009 
快速回復ack模式的判斷條件如下:(tp->ack.quick && tp->ack.pingpong) 應該是 (tp->ack.quick && tp->ack.pingpong)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM