一、快速重傳介紹
按照TCP協議,RTO超時重傳是一個非常重要的事件,當RTO超時的時候,TCP會同時通過兩種方式非常謹慎的降低發送數據包的速率,一種是基於擁塞控制削減發送窗口的大小,另外一個是通過指數回退增加每次RTO超時的時間(即karn算法的第二部分)。所以RTO超時后有可能會導致網絡容量的利用不足。
最開始我們介紹tcp重傳的時候就介紹過TCP還有另外一種重傳方式--快速重傳。快速重傳是RFC5681定一個的一個過程。快速重傳不依賴定時器的超時,而是依靠ACK確認包來進行重傳。使用快速重傳相比RTO超時重傳通常可以更高效的修復TCP丟包問題。快速重傳是基於一個前提:即按照RFC5681,當TCP收到一個亂序報文的時候應該立即回復ACK確認包,而不會延遲ACK(延遲ACK介紹參考之前文章介紹)確認。另外RFC5681還指出如果接收序列號空間存在洞,新接收的報文完全填充了這個洞或者部分填充了這個洞,TCP也應該立即回復一個ACK確認包以便發送端及時獲取接收端相關的信息。
我們舉個例子假設有5個TCP報文,P1(1-10)、P2(11-20)、P3(21-30)、P4(31-40)、P5(41-50),其中括號中標注的是報文的比特系列號,每個報文的長度都為10bytes。假設發送端依次發送這5個報文,其中P2報文在網絡傳輸過程中丟失,P1、P3、P4、P5報文依次按序到達,接收端收到這P1的時候發送ack=11的確認包(實際上這里可能會延遲發送ACK報文,為了描述簡單我們假設立即發送ACK報文),接收端收到P3的時候發現是亂序的報文則會立即回復ack=11確認包(還記得ACK是累計確認的吧,因為P2丟失了ACK只能累計到11),同樣后面收到P4和P5的時候還是會回復ack=11的確認包。這樣發送端就會連續接收到4個ack=11的確認包,后面三個確認包因為和第一個ack number重復,因為稱呼為duplicate ACK。因此接收端就可以依據dup ACK來推測接收端的接收情況。但是我們之前說過IP層不會向TCP提供有序的數據報文,如果網絡傳輸過程中發生亂序導致接收端接收順序變為P1、P3、P2、P4、P5,這樣的情況下也會產生一個dup ACK。另外還有一種情況是IP層dup了ACK報文。我們通過一個dup ACK並不能可靠的確認是發生了丟包還是發生了亂序傳輸,因此會存在一個門限(duplicate ACK threshold或者叫做dupthresh),當TCP收到的dup ACK數超過這個門限的時候,就會認為發生了丟包,進而初始化一個快速重傳。最初協議中給出的dupthresh這個門限是3,但是RFC 4653給出了一種調整dupthresh的方法。Linux中則可以通過/proc/sys/net/ipv4/tcp_reordering來設置默認值,另外Linux可能還會根據亂序測量的結果來更新實際的dupthresh。dupthresh的范圍最終會在/proc/sys/net/ipv4/tcp_reordering和/proc/sys/net/ipv4/tcp_max_reordering之間。在沒有使能SACK的時候,快速重傳只會重傳一個數據包,在使能SACK時候,SACK可以反映接收端是否存在系列號洞,進而允許發送端根據SACK的情況同時傳輸多個數據包。SACK的內容留到后面介紹。
最后補充一下window update的判斷,一般如果一個TCP報文滿足下面三個條件之一的話,linux就會認定這個報文是window update消息,被認定為window update的確認包是不會統計到dup ACK里面的,后面介紹窗口管理的時候還會進一步介紹一下window update。
1、ack number比之前接收的最大的ack number還要大
2、系列號seq比之前接收到的最大系列號還要大
3、系列號seq與之前接收到的系列號相同,但是TCP頭中的window size字段發生了變化
二、wireshark示例
1、快速重傳與RTO超時
設置/proc/sys/net/ipv4目錄下tcp_retries2=8,tcp_early_retrans=0,tcp_sack=0,tcp_reordering=3,tcp_discard_on_port =9877(該參數為自加參數)。
-
client通過rawsocket與server建立連接,對應No1-No3報文
-
client先發送6bytes的數據,服務器回復ACK確認包,對應No4-No5報文
-
服務器連續發送5個len=8的報文,對應No6-No10
-
client對No6報文回復ACK確認報文,對應No11
-
client丟棄No7報文來模擬傳輸過程中丟包,並對No8-No10報文回復dupACK,對應No12-No14
-
server端在收到三個dup ACK后,認為No7報文已經丟失並立即觸發快速重傳,同時設置RTO超時定時器,對應No15
-
client對之后收到的報文直接丟棄不在回復ACK確認包
-
server端RTO超時后,繼續重傳對應的報文,並進行指數回退過程。最終多次RTO超時重傳失敗后,server端釋放TCP連接,對應No16-No21。
2、快速重傳與window update消息
設置/proc/sys/net/ipv4目錄下tcp_retries2=8,tcp_early_retrans=0,tcp_sack=0,tcp_reordering=3,tcp_discard_on_port =9877(該參數為自加參數)。
-
client通過rawsocket與server建立連接,對應No1-No3報文
-
client先發送6bytes的數據,服務器回復ACK確認包,對應No4-No5報文
-
服務器連續發送5個len=8的報文,對應No6-No10
-
client丟棄No6報文來模擬傳輸過程中丟包,並對No7-No10報文回復dupACK,對應No11-No14
-
從wireshark抓包看,server端在收到四個dup ACK后,才認為No6報文已經丟失並立即觸發快速重傳,同時設置RTO超時定時器,對應No15
-
client對之后收到的報文直接丟棄不在回復ACK確認包
-
server端RTO超時后,繼續重傳對應的報文,並進行指數回退過程。最終多次RTO超時重傳失敗后,server端釋放TCP連接,對應No16-No21。
那么這里問題來了,我們設置的tcp_reordering為3,也就是dupthresh值為3(實際上dupthresh可以動態調整,但是在這次測試中沒有發生調整),那為什么這里快速重傳的觸發需要四個dup ACK呢?原因是這里No11對應的ACK報文,相比於No4的ACK報文雖然ack number都為3741168164,但是兩者的Seq卻發生了變化,因此linux認為No11是一個window update消息,而linux並不把window update消息計入dup ACK,后面收到的No12-No14報文才會被TCP認為是dup ACK,因此直到收到No14報文TCP才會認為dup ACK達到快速重傳門限,觸發快速重傳。這里也反映了wireshark和linux在dup ACK認定上的差異性。
3、快速重傳與recovery point
下面我們在看一個快速重傳后觸發RTO超時重傳然后收到ACK確認包的場景。這個測試過程與上一個示例類似,差異部分在於接收端在收到No18的重傳報文以及之后的報文的時會回復一個ACK確認包。我們可以看到在觸發快速重傳之前,正常傳輸的數據中最高系列號的下一個待發送系列號為No10報文對應的1751769800(即1751769792+8),這個系列號節點稱呼為recovery point,而No19確認包中Ack=1751769768小於之前的1751769800,對於這種ACK確認報文,TCP稱呼為partial ACK。在TCP收到parital ACK報文的時候則會立即觸發快速重傳,如No20、No22、No23、No25所示。截圖中最后一個RST消息則是由於server端應用層沒有讀取server緩存中的數據(No4報文的數據)而直接close,因此會產生RST消息。實際的消息流中還有一個No29消息則是由於client使用raw socket編寫的程序沒有處理server端的RST消息,client關閉的時候簡單的發送了一條RST消息來通知server。
我們同時注意到,在發送端收到No19一個ACK確認包的時候,只是發出去了一個重傳包,而在收到No21確認包的時候,TCP卻發出去了兩個重傳包。這種差異則是由於TCP的擁塞控制造成的,后面我們講到擁塞控制的時候在通過幾個示例來介紹說明。
補充說明:
1、快速重傳代碼點tcp_fastretrans_alert,window update消息和dup ack的判斷在tcp_ack_update_window和tcp_ack中