再次談談TCP的Nagle算法與TCP_CORK選項

原文地址:http://blog.csdn.net/dog250/article/details/21303679


事件回放

使用OpenVPN傳輸虛擬桌面流量,終端上有明顯逐幀刷屏現象,網絡環境爲百兆局域網。

分析

1.首先將OpenVPN改爲TCP模式,因爲局域網環境下TCP和UDP差別不大,不會引起重傳疊加問題。TCP的好處在於可以任意蹂躪分析,因爲它的算法巨複雜,如果換UDP,太簡單了,沒啥好調整的;
2.分析過程不是本文的目的,直接給結果吧。減小發送/接收緩衝區到MTU的2倍的樣子,減小MTU到1000以下,OpenVPN加上tcp-nodelay選項,結果好了一些,但是還是會刷屏,繼續解決,最終確認這個問題搞不定,因爲虛擬桌面所在虛擬機沒裝顯卡驅動...(TMD我最煩那些銷售眼中的上帝告訴我讓我可以解決一些不屬於我範疇的問題,比如在連信號都TMD沒有的情況下可以保證手機暢通)

TCP啊TCP

我懂TCP是因爲我煩它!如果根本不懂就沒有資格煩。TCP有超級多的參數可以調整,直接折騰就是了,我一向認爲,抓包工具是用來分析數據的,而不是用來分析TCP行爲的,你能抓個包告訴我有沒有啓用Nagle算法嗎?你能在客戶端抓包分析出來TCP timestamps導致的連接被拒絕嗎?學習協議還是要理解設計原理和該協議被設計和修改的歷史,這樣你才能知道它爲什麼會這樣以及當時是什麼原因導致了它這樣,說實話,有時看看代碼也是有好處的,但是看代碼會讓人容易鑽進死衚衕,比如一些非標準的東西一旦進入代碼就會讓人誤以爲是標準的東西,然後再看其它與之不同的實現的時候,先入爲主的印象就會印象你的認知...
       總之,TCP很難,也很令人討厭,一直覺得TCP設計的很好,但是發展的很亂。

網絡利用率與大量小包

很多人都把Nagle算法的目的理解爲“提高網絡利用率”,事實上,Nagle算法所謂的“提高網絡利用率”只是它的一個副作用(Side effect...),Nagle算法的主旨在於“避免發送‘大量’的小包”。Nagle算法並沒有阻止發送小包,它只是阻止了發送大量的小包!誠然,發送大量的小包是降低了網絡利用率,但是,發送少量必須發送的小包也是對網絡利用率的降低,想徹底提高網絡利用率,爲嘛不直接阻止小包發送呢?不管是大量小包還是少量小包,甚至一個小包也不讓發送,這纔是提高網絡利用率的正解!是的,TCP_CORK就是做這個的。所以有人說,CORK選項是Nagle的增強,而實際上,它們是完全不同的兩回事,初衷不同。
Nagle算法的初衷:避免發送大量的小包,防止小包氾濫於網絡,理想情況下,對於一個TCP連接而言,網絡上每次只能一個小包存在。它更多的是端到端意義上的優化。
CORK算法的初衷:提高網絡利用率,理想情況下,完全避免發送小包,僅僅發送滿包以及不得不發的小包

關於交互式應用

一直以來,人們有個誤區,那就是Nagle算法會爲交互式應用引入延遲,建議交互式應用關閉Nagle算法。事實上,正是交互式應用引入了大量的小包,Nagle算法所作用的正是交互式應用!引入一些延遲是必然的,畢竟任何事都要有所代價,但是它更多的是解決了交互式應用產生大量小包的問題,不能將那麼一點點爲解決問題所付出的代價作爲新的問題而忽視了真正的問題本身!

TCP的Nagle算法和延遲ACK

Nagle算法的操作過程請參看Wiki,它減少了大量小包的發送,實際上就是基於小包的停-等協議。在等待已經發出的包被確認之前,發送端利用這段時間可以積累應用下來的數據,使其大小趨向於增加。這是避免糊塗窗口綜合症的一種有效方法,請注意,糊塗窗口指的是接收端的糊塗,而不是發送端的糊塗,接收端不管三七二十一得通告自己的接收窗口大小,絲毫不管這會在發送端產生大量小包。然而發送端可以不糊塗,你通告你的,我就是不發,你糊塗我不糊塗,你不斷通告很小的數值,我不予理睬,我有自己的方法,直到收到已經發出包的ACK纔會繼續發送,這就是Nagle算法的糊塗抵制方案
       治療糊塗窗口綜合症有兩種方式,一種是“你糊塗我不糊塗”的方式,即上述的Nagle算法的方式,另外一種是“治療接收端的糊塗”的方式,其中一種機制是延遲ACK(還有其它機制,比如不發送小窗口通告等)。可以看出,這兩種方式中都在試圖減少包的發送量,二者殊途同歸的解決了同一問題,對於發送方而言,不理會接收端的小窗口通告等於說不馬上發送小包,小包得以有時間積累成大包,對於接收方而言,延遲ACK可以拖延ACK發送時間,進而延遲窗口通告,在這段時間內,接收窗口有機會進一步(由於應用程序處理)放大。單獨理解這兩種方式都是簡單的,但是一旦它們混在一起使用,情況就會非常不幸!因爲...
       Nagle算法和延遲ACK作用在方向相反的數據包和針對該數據包的確認包上,因此它們的作用力會相悖,結果就是誰也不能發包。就像一根繩子上拴兩隻青蛙一樣,被對方牽制誰也跑不了!關鍵點在於,小包的發送依賴於ACK,然而延遲ACK阻止了ACK的即時發送,形成了僵持狀態。本來只是爲了減少網絡上小包的數量(再次強調Nagle算法以及延遲ACK的目的,注意,糊塗窗口綜合症只是網絡上小包氾濫的原因之一!),卻人爲引入了大量的延遲!
       此處有一個通用的解釋,Nagle算法的小包發送依賴於接收端對小包得快速確認,因此接收端對待ACK而言,應該朝着延遲ACK相反俄方向用力,即快速ACK;相對的,如果在接收端啓用了延遲ACK,發送端就應該不斷髮送數據包,不管是大包還是小包(不考慮稍帶ACK的影響),因爲發送端已經不能指望接收端正常ACK數據包了,即發送端應該禁掉Nagle算法。以上解釋背後的思想就是數據包和ACK包是相關的,力應該往一個方向使,一邊拉另一邊就要推,如果兩邊都拉,力就會抵消掉,陷入僵持。不幸點或者說悲哀的地方在於,Nagle算法和延遲ACK機制都是“拉方案”!划船的人都知道,划槳手有兩種座位佈局,要麼超同一個方向,要麼面朝不同的方向,後者和TCP數據包和ACK很類似,要想船往前走,必須朝一個方向劃,雖然他們面朝相反的方向。

編程模型的影響

在Nagle算法的Wiki主頁,有這麼一段話:
In general, since Nagle's algorithm is only a defense against careless applications, it will not benefit a carefully written application that takes proper care of buffering; the algorithm has either no effect, or negative effect on the application.
可見編程模型對“減少網絡上小包數量”的影響,言外之意,Nagle算法是個有針對性的優化-針對交互式應用,不是放之四海而皆準的標準,要想有一個比較好的方案,別指望它了,還是應用程序自己搞定纔是正解!要想Nagle算法真的能夠減少網絡上小包數量而又不引入明顯延遲,對TCP數據的產生方式是有要求的,交互式應用是其初始針對的對象,,Nagle算法要求數據必須是“乒乓型”的,也就是說,數據流有明確的邊界且一來一回,類似人機交互的那種,比如telnet這種遠程終端登錄程序,數據是人從鍵盤敲入的,邊界基本上就是擊鍵,一來一回就是輸入回顯和處理回顯。Nagle算法在上面的場景中保證了下一個小包發送之前,所有發出的包已經得到了確認,再次我們看到,Nagle算法並沒有阻止發送小包,它只是阻止了發送大量的小包。
       換句話說,所謂的“乒乓型”模式就是“write-read-write-read”模式-人機交互模式,但是對於Wiki中指出的“write-write-read”(很多的request/response模式俄C/S服務就是這樣的,比如HTTP)-程序交互模式,Nagle算法和延遲ACK拔河的惡果就會被放大,在“TCP的Nagle算法和延遲ACK”一節中我已經說了,這二者不能混用,這個我們已經知道了,但那只是在TCP本身的原理上給與說明,本節中說的是同一個問題,但是側重於編程層面,然而卻不是僅僅用setsocket將TCP_NODELAY設置一下或者關掉延遲ACK那麼簡單。
       有一篇很好的文章《TCP_CORK: More than you ever wanted to know》,文章說,Nagle算法對於數據來自於user input的那種應用是有效的,但是對於數據generated  by applications using stream oriented protocols,Nagle算法純粹引入了延遲,這個觀點我非常贊同,因爲對於人而言,TCP登錄俄遠程計算機就是一個處理機,人希望自己的操作馬上展示結果,其模式就是write-read-write-read的,但是對於程序而言,其數據產生邏輯就不像人機交互那麼固定,因此你就不能假定程序依照任何序列進行網絡IO,而Nagle算法是和數據IO的序列相關的。實際上就算接收端沒有啓用延遲ACK,Nagle算法應用於write-write-read序列也是有問題的,作者的意思是,平白無故地引入了額外的延遲。
       難道真的有這麼複雜嗎?作者沒有提出如何靠編程把問題解決,但是Nagle算法的Wiki頁面上提到了”儘量編寫好的代碼而不要依賴TCP內置的所謂的算法“來優化TCP的行爲。再次引用Nagle算法Wiki上的那句話:

另一件事情

In general, since Nagle's algorithm is only a defense against careless applications, it will not benefit a carefully written application that takes proper care of buffering; the algorithm has either no effect, or negative effect on the application.
這句話引起了我對另外一件事的思考,這件事就發生在上週,由於公司搬家一切處於混亂中,無心工作,一個網絡處理加速卡供應商來訪,對方聲稱他們的協議棧實現在用戶態以提高性能,我起初對此觀點嗤之以鼻,但是結合我的以上分析以及後續的思考,我發現這真的是一個突破:
當我們什麼事情都依賴內核的時候,可能我們真的錯了!內核不可控,爲何不在用戶態自己搞定呢。內核提供的是基礎設施上的基本服務,提供高端私人訂製服務會把整個內核拖垮!如果協議棧在內核實現(宏內核的風格),那麼就在用戶態管理TCP buffer,解除對複雜TCP算法的依賴,如果協議棧在用戶態實現,那就實現一個更好的類Nagle算法!
這件事情我準備再寫一篇文章,現在回到《
TCP_CORK: More than you ever wanted to know》

回到《TCP_CORK: More than you ever wanted to know》

作者提到了一個觀點,即write的語義,這背後的思想就是”一切皆文件“。既然一切皆文件,那就要爲所有的IO都定義一套文件IO的操作集。我們知道,UNIX最初將文件IO分爲了兩種,即塊設備IO和字符設備IO,這個觀點或者說哲學觀念是具有劃時代意義的,它帶來了編程的簡潔性,爲程序員帶來了一絲清爽之意,那個時候不管是TCP/IP標準還是BSD socket都還沒有被提出,當網路協議棧成爲標準的時候,作爲一種IO類型,網絡IO該屬於塊設備IO還是字符設備IO呢?當時有兩種觀點,其中之一就是將網絡IO從文件IO中獨立成一個平級的IO類型,但是最終還是BSD socket獲得了勝利,即網絡IO也是一種設備IO。然而問題還是在,網絡IO到底是屬於塊設備IO呢,還是該屬於字符設備IO?
       在回答這個問題之前,首先要爲網絡IO定義一種設備模型,沒有設備模型,何談設備分類?最終,網絡IO的設備模型就定義爲了packet,正如作者所說,網絡IO對應的設備就是網絡數據包(packet),用戶應用程序通過標準的文件IO接口write將網絡數據寫入一個設備,該設備就是packet!現在剩下的問題不是這個packet何時真正flush到wire的問題,而是如何生成這個packet的問題,如果數據可以持續積累在一個buffer,然後生成一個packet,那麼它就是塊設備,如果數據來一個就生成一個packet,那就是字符設備。遺憾的是不管是BSD socket還是協議棧,都沒有規定到底有沒有這個buffer,因此,網絡IO作爲一個被另類的類型,獨立於塊設備IO和字符設備IO之外,成爲了第三種IO方式。因此我們可以說,網絡IO或者說INET族的socket IO,可以定義一個buffer作爲socket文件IO接口到packet(作爲一種設備)的緩衝,也可以不定義。定義與否取決於什麼?取決於你如何來解釋網絡!
John Nagle從”小包氾濫會導致網絡癱瘓“這個視角出發,發明了Nagle算法,而另一些人從”buffer packet會引入延遲“這個視角出發,抵制Nagle算法,這個爭鬥其實是沒有意義的,其關鍵導火索在於”一切皆文件“思想遇到了網絡IO時的尷尬。
       那麼我們是否可以回答這一問題呢?網絡IO到底應不應該在數據和packet之間有一個buffer?這還是應該取決於packet到底是一個字符設備還是一個塊設備。這個問題的答案取決於網絡情況和數據接收端情況,對於TCP而言,長肥管道的理想情況下,packet就是一個字符設備,而對於在慢速網絡上使用telnet登錄的程序而言,packet就是一個塊設備,buffer存在與否取決於很多的因素而不是一個因素,存在即是合理的,Nagle算法存在,但是不是讓人濫用的。舉個例子結束本節,滾滾長江東逝水,順江而下可行船,攔江截斷儲水可發電,到底應不應該在長江上構建一個buffer,也是有爭議的....

Linux的優化

正如建立大壩存在爭議一樣,是否緩衝網絡數據也存在爭議,因此Nagle算法的實現並不存在一個哪怕是事實上的標準,更別提什麼工業標準了。因此各家操作系統都有自己的實現,實現中存在有自身的風格。Linux當然也不例外,在數據發送的時候,會有一個tcp_nagle_check函數來判斷是否應該將數據buffer起來,我們看一下注釋就明白了其實現要旨:
/* Return 0, if packet can be sent now without violation Nagle's rules:
* 1. It is full sized
* 2. Or it contains FIN. (already checked by caller) 
* 3. Or TCP_NODELAY was set.
* 4. Or TCP_CORK is not set, and all sent packets are ACKed.
*    With Minshall's modification: all sent small packets are ACKed.
*/

值得一提的是TCP_CORK選項和最後的Minshall's modification,我們先看TCP_CORK,說起這個選項,很多人會把它和Nagle算法聯繫起來,實際上它們的關係並不是十分明確,嚴格說來TCP_CORK是爲了提高數據包的載荷率而引入的,而我們知道,小包的載荷率非常小,所以感覺上好像是TCP_CORK和Nagle算法相關。實際上,CORK選項提高了網絡的利用率,因爲它直接禁止了小包的發送(再次強調,Nagle算法沒有禁止小包發送,只是禁止了大量小包的發送)
       CORK選項在實現上還是和文檔中所說的有一些出入,比如,在超時情況TCP發送窗口探測包的時候,會將buffer的包發送過去,我們看下其註釋:
/* A window probe timeout has occurred. If window is not closed send
* a partial packet else a zero probe.
*/

背後的思想很簡單,反正也是要發送探測包,跑不了了,平白只是發送一個探測包,載荷率太低,是不是能稍帶一些東西過去呢?允許的情況下,把buffer的packet帶過去吧,雖然它們還沒有buffer到一個full size的packet。如果說有些同學只是死摳full size的話就會很糾結,事實上,CORK的真正初衷是提高載荷率,提高網絡利用率。關於CORK,最後要說的是,它只是Linux實現的一個選項。
       最後我們來看一下所謂的
Minshall's modification,這傢伙對Nagle算法做了什麼修正呢?很簡單,雖然Nagle算法要求發送出去的packet被確認之後才能發送小包,但是根據Nagle算法的初衷,它僅僅是避免發送大量小包,那麼爲何不直接忽略大包的確認呢?也就是說,只要求發出去的小包被確認就可以繼續發送小包了,這確實是一種優化,但是也會帶來一些額外的效果,Minshall算法保證了只有一個小包在網絡上,但是Minshall算法也可能會像機關槍一樣突突突的持續發送小包(只要ACK持續到大),按照減少小包數量的初衷,它確實每次只發送一個小包,但是發送間隔卻短了...

後記

TCP太複雜了,任何人的任何分析都是片面的,你都能找出漏洞,因爲對於TCP而言,其任何一個特性的參數調整都會對其它的特性,長肥管道是理想情況,但在某些情況下卻並非最好的情況,Nagle算法有時需要關閉有時需要開啓...唉,TCP啊TCP
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章