1.三個問題以及解決
問題1描述:接收端處理慢,導致接收窗口被填滿
這明顯是速率不匹配引發的問題,然而即使速率不匹配,只要滑動窗口能協調好它們的速率就好,要快都快,要慢都慢,事實上滑動窗口在這一點上做的很好。但是如果我們不得不從效率上來考慮問題的話,事實就不那么樂觀了。考慮此時接收窗口已然被填滿,慢速的應用程序慢騰騰的讀取了一個字節,空出一個位置,然后通告給TCP的發送端,發送端得知空出一個位置,馬上發出一個字節,又將接收端填滿,然后接收應用程序又一次慢騰騰...這就是糊涂窗口綜合癥,一個大多數人都很熟悉的詞。這個問題極大的浪費了網絡帶寬,降低了網絡利用率。好比從大同拉100噸煤到北京需要一輛車,拉1Kg煤到北京也需要一輛車(超級夸張的一個例子,請不要相信),但是一輛車開到北京的開銷是一定的...
問題1解決:窗口通告
對于問題1,很顯然問題出在接收端,我們沒有辦法限制發送端不發送小分段,但是卻可以限制接收端通告小窗口,這是合理的,這并不影響應用程序,此時經典的延遲/吞吐量反比律將不再適用,因為接收窗口是滿的,其空出一半空間表示還有一半空間有數據沒有被應用讀取,和其空出一個字節的空間的效果是一樣的,因此可以限制接收端當窗口為0時,直接通告給發送端以阻止其繼續發送數據,只有當其接收窗口再次達到MSS的一半大小的時候才通告一個不為0的窗口,此前對于所有的發送端的窗口probe分段(用于探測接收端窗口大小的probe分段,由TCP標準規定),全部通告窗口為0,這樣發送端在收到窗口不為0的通告,那么肯定是一個比較大的窗口,因此發送端可以一次性發出一個很大的TCP分段,包含大量數據,也即拉了好幾十噸的煤到北京,而不是只拉了幾公斤。
即,限制窗口通告時機,解決糊涂窗口綜合癥
問題2描述:發送端持續發送小包,導致窗口閑置
這明顯是發送端引起的問題,此時接收端的窗口開得很大,然而發送端卻不積累數據,還是一味的發送小塊數據分段。只要發送了任和的分段,接收端都要無條件接收并且確認,這完全符合TCP規范,因此必然要限制發送端不發送這樣的小分段。
問題2解決:Nagle算法
Nagel算法很簡單,標準的Nagle算法為:
IF 數據的大小和窗口的大小都超過了MSS
Then 發送數據分段
ELSE
IF 還有發出的TCP分段的確認沒有到來
Then 積累數據到發送隊列的末尾的TCP分段
ELSE
發送數據分段
EndIF
EndIF
可是后來,這個算法變了,變得更加靈活了,其中的:
IF 還有發出的TCP分段的確認沒有到來
變成了
IF 還有發出的不足MSS大小的TCP分段的確認沒有到來
這樣如果發出了一個MSS大小的分段還沒有被確認,后面也是可以隨時發送一個小分段的,這個改進降低了算法對延遲時間的影響。這個算法體現了一種自適應的策略,越是確認的快,越是發送的快,雖然Nagle算法看起來在積累數據增加吞吐量的同時也加大的時延,可事實上,如果對于類似交互式的應用,時延并不會增加,因為這類應用回復數據也是很快的,比如Telnet之類的服務必然需要回顯字符,因此能和對端進行自適應協調。
注意,Nagle算法是默認開啟的,但是卻可以關閉。如果在開啟的情況下,那么它就嚴格按照上述的算法來執行。
問題3.確認號(ACK)本身就是不含數據的分段,因此大量的確認號消耗了大量的帶寬
這是TCP為了確保可靠性傳輸的規范,然而大多數情況下,ACK還是可以和數據一起捎帶傳輸的。如果沒有捎帶傳輸,那么就只能單獨回來一個ACK,如果這樣的分段太多,網絡的利用率就會下降。從大同用火車拉到北京100噸煤,為了確認煤已收到,北京需要派一輛同樣的火車空載開到大同去復命,因為沒有別的交通工具,只有火車。如果這位復命者剛開著一列火車走,又從大同來了一車煤,這拉煤的哥們兒又要開一列空車去復命了。
問題3的解決:
RFC 建議了一種延遲的ACK,也就是說,ACK在收到數據后并不馬上回復,而是延遲一段可以接受的時間,延遲一段時間的目的是看能不能和接收方要發給發送方的數據一起回去,因為TCP協議頭中總是包含確認號的,如果能的話,就將ACK一起捎帶回去,這樣網絡利用率就提高了。往大同復命的確認者不必開一輛空載火車回大同了,此時北京正好有一批貨物要送往大同,這位復命者搭著這批貨的火車返回大同。
如果等了一段可以接受的時間,還是沒有數據要發往發送端,此時就需要單獨發送一個ACK了,然而即使如此,這個延遲的ACK雖然沒有等到可以被捎帶的數據分段,也可能等到了后續到來的TCP分段,這樣它們就可以取者一起返回了,要知道,TCP的確認號是收到的按序報文的最后一個字節的后一個字節。最后,RFC建議,延遲的ACK最多等待兩個分段的積累確認。
2.分析三個問題之間的關聯
三個問題導致的結果是相同的,但是要知道它們的原因本質上是不同的,問題1幾乎總是出現在接收端窗口滿的情況下,而問題2幾乎總是發生在窗口閑置的情況下,問題3看起來是最無聊的,然而由于TCP的要求,必須要有確認號,而且一個確認號就需要一個TCP分段,這個分段不含數據,無疑是很小的。
三個問題都導致了網絡利用率的降低。雖然兩個問題導致了同樣的結果,但是必須認識到它們是不同的問題,很自然的將這些問題的解決方案匯總在一起,形成一個全局的解決方案,這就是如今的操作系統中的解決方案。
3.問題的雜糅情況
疑難雜癥11:糊涂窗口解決方案和Nagle算法
糊涂窗口綜合癥患者希望發送端積累TCP分段,而Nagle算法確實保證了一定的TCP分段在發送端的積累,另外在延遲ACK的延遲的那一會時間,發送端會利用這段時間積累數據。然而這卻是三個不同的問題。Nagle算法可以緩解糊涂窗口綜合癥,卻不是治本的良藥。
疑難雜癥12:Nagle算法和延遲ACK
延遲ACK會延長ACK到達發送端的時間,由于標準Nagle算法只允許一個未被確認的TCP分段,那無疑在接收端,這個延遲的ACK是毫無希望等待后續數據到來最終進行積累確認的,如果沒有數據可以捎帶這個ACK,那么這個ACK只有在延遲確認定時器超時的時候才會發出,這樣在等待這個ACK的過程中,發送端又積累了一些數據,因此延遲ACK實際上是在增加延遲的代價下加強了Nagle算法。在延遲ACK加Nagle算法的情況下,接收端只有不斷有數據要發回,才能同時既保證了發送端的分段積累,又保證了延遲不增加,同時還沒有或者很少有空載的ACK。
要知道,延遲ACK和Nagle是兩個問題的解決方案。
疑難雜癥13:到底何時可以發送數據
到底何時才能發送數據呢?如果單從Nagle算法上看,很簡單,然而事實證明,情況還要更復雜些。如果發送端已經排列了3個TCP分段,分段1,分段2,分段3依次被排入,三個分段都是小分段(不符合Nagle算法中立即發送的標準),此時已經有一個分段被發出了,且其確認還沒有到來,請問此時能發送分段1 和2嗎?如果按照Nagle算法,是不能發送的,但實際上它們是可以發送的,因為這兩個分段已經沒有任何機會再積累新的數據了,新的數據肯定都積累在分段 3上了。問題在于,分段還沒有積累到一定大小時,怎么還可以產生新的分段?這是可能的,但這是另一個問題,在此不談。
Linux的TCP實現在這個問題上表現的更加靈活,它是這么判斷能否發送的(在開啟了Nagle的情況下):
IF (沒有超過擁塞窗口大小的數據分段未確認 || 數據分段中包含FIN ) &&
數據分段沒有超越窗口邊界
Then
IF 分段在中間(上述例子中的分段1和2) ||
分段是緊急模式 ||
通過上述的Nagle算法(改進后的Nagle算法)
Then 發送分段
EndIF
EndIF
曾經我也改過Nagle算法,確切的說不是修改Nagle算法,而是修改了“到底何時能發送數據”的策略,以往都是發送端判斷能否發送數據的,可是如果此時有延遲ACK在等待被捎帶,而待發送的數據又由于積累不夠或者其它原因不能發送,因此兩邊都在等,這其實在某些情況下不是很好。我所做的改進中對待何時能發送數據又增加了一種情況,這就是“ACK拉”的情況,一旦有延遲ACK等待發送,判斷一下有沒有數據也在等待發送,如果有的話,看看數據是否大到了一定程度,在此,我選擇的是MSS的一半:
IF (沒有超過擁塞窗口大小的數據分段未確認 || 數據分段中包含FIN ) &&
數據分段沒有超越窗口邊界
Then
IF 分段在中間(上述例子中的分段1和2) ||
分段是緊急模式 ||
通過上述的Nagle算法(改進后的Nagle算法)
Then 發送分段
EndIF
ELSE IF 有延遲ACK等待傳輸 &&
發送隊列中有待發送的TCP分段 &&
發送隊列的頭分段大小大于MSS的一半
Then 發送隊列頭分段且捎帶延遲ACK
EndIF
另外,發送隊列頭分段的大小是可以在統計意義上動態計算的,也不一定非要是MSS大小的一半。我們發現,這種算法對于交互式網路應用是自適應的,你打字越快,特定時間內積累的分段就越長,對端回復的越快(可以捎帶ACK),本端發送的也就越快(以Echo舉例會更好理解)。
疑難雜癥14:《TCP/IP詳解(卷一)》中Nagle算法的例子解讀
這個問題在網上搜了很多的答案,有的說RFC的建議,有的說別的。可是實際上這就是一個典型的“競態問題”:
首先服務器發了兩個分段:
數據段12:ack 14
數據段13:ack 14,54:56
然后客戶端發了兩個分段:
數據段14:ack 54,14:17
數據段15:ack 56,17:18
可以看到數據段14本來應該確認56的,但是確認的卻是54。也就是說,數據段已經移出隊列將要發送但還未發送的時候,數據段13才到來,軟中斷處理程序搶占了數據段14的發送進程,要知道此時只是把數據段14移出了隊列,還沒有更新任何的狀態信息,比如“發出但未被確認的分段數量”,此時軟中斷處理程序順利接收了分段13,然后更新窗口信息,并且檢查看有沒有數據要發送,由于分段14已經移出隊列,下一個接受發送檢查的就是分段15了,由于狀態信息還沒有更新,因此分段15順利通過發送檢測,發送完成。
可以看Linux的源代碼了解相關信息,tcp_write_xmit這個函數在兩個地方會被調用,一個是TCP的發送進程中,另一個就是軟中斷的接收處理中,兩者在調用中的競態就會引起《詳解》中的那種情況。注意,這種不加鎖的發送方式是合理的,也是效的,因此TCP的處理語義會做出判斷,丟棄一切不該接收或者重復接收的分段的。