TCP重傳的的兩種方式
沒有永遠不出錯誤的通信,這句話表明著不管外部條件多么完備,永遠都會有出錯的可能。所以,在 TCP 的正常通信過程中,也會出現錯誤,這種錯誤可能是由于數據包丟失引起的,也可能是由于數據包重復引起的,甚至可能是由于數據包失序引起的。
TCP 的通信過程中,會由 TCP 的接收端返回一系列的確認信息來判斷是否出現錯誤,一旦出現丟包等情況,TCP 就會啟動重傳操作,重傳尚未確認的數據。
TCP 的重傳有兩種方式,一種是基于時間,一種是基于確認信息,一般通過確認信息要比通過時間更加高效。
所以從這點就可以看出,TCP 的確認和重傳,都是基于數據包是否被確認為前提的。
TCP 在發送數據時會設置一個定時器,如果在定時器指定的時間內未收到確認信息,那么就會觸發相應的超時或者基于計時器的重傳操作,計時器超時通常被稱為重傳超時(RTO)。
但是有另外一種不會引起延遲的方式,這就是快速重傳。
TCP 在每次重傳一次報文后,其重傳時間都會加倍,這種"間隔時間加倍"被稱為二進制指數補償(binary exponential backoff) 。等到間隔時間加倍到 15.5 min 后,客戶端會顯示
- Connection closed by foreign host.
TCP 擁有兩個閾值來決定如何重傳一個報文段,這兩個閾值被定義在 RFC[RCF1122] 中,第一個閾值是 R1,它表示愿意嘗試重傳的次數,閾值 R2 表示 TCP 應該放棄連接的時間。R1 和 R2 應至少設為三次重傳和 100 秒放棄 TCP 連接。
這里需要注意下,對連接建立報文 SYN 來說,它的 R2 至少應該設置為 3 分鐘,但是在不同的系統中,R1 和 R2 值的設置方式也不同。
在 Linux 系統中,R1 和 R2 的值可以通過應用程序來設置,或者是修改 net.ipv4.tcp_retries1 和 net.ipv4.tcp_retries2 的值來設置。變量值就是重傳次數。
tcp_retries2 的默認值是 15,這個重試次數的耗時大約是 13 - 30 分鐘,這只是一個大概值,最終耗時時間還要取決于 RTO ,也就是重傳超時時間。tcp_retries1 的默認值是 3 。
對于 SYN 段來說,net.ipv4.tcp_syn_retries 和 net.ipv4.tcp_synack_retries 這兩個值限制了 SYN 的重傳次數,默認是 5,大約是 180 秒。
Windows 操作系統下也有 R1 和 R2 變量,它們的值被定義在下方的注冊表中
- HKLM\System\CurrentControlSet\Services\Tcpip\Parameters
- HKLM\System\CurrentControlSet\Services\Tcpip6\Parameters
其中有一個非常重要的變量就是 TcpMaxDataRetransmissions,這個 TcpMaxDataRetransmissions 對應 Linux 中的 tcp_retries2 變量,默認值是 5。這個值的意思表示的是 TCP 在現有連接上未確認數據段的次數。
快速重傳
我們上面提到了快速重傳,實際上快速重傳機制是基于接收端的反饋信息來觸發的,它并不受重傳計時器的影響。所以與超時重傳相比,快速重傳能夠有效的修復丟包情況。當 TCP 連接的過程中接收端出現亂序的報文(比如 2 - 4 - 3)到達時,TCP 需要立刻生成確認消息,這種確認消息也被稱為重復 ACK。
當失序報文到達時,重復 ACK 要做到立刻返回,不允許延遲發送,此舉的目的是要告訴發送方某段報文失序到達了,希望發送方指出失序報文段的序列號。
還有一種情況也會導致重復 ACK 發給發送方,那就是當前報文段的后續報文發送至接收端,由此可以判斷當前發送方的報文段丟失或者延遲到達。因為這兩種情況導致的后果都是接收方沒有收到報文,但是我們卻無法判斷到底是報文段丟失還是報文段沒有送達。因此 TCP 發送端會等待一定數目的重復 ACK 被接受來決定數據是否丟失并觸發快速重傳。一般這個判斷的數量是 3,這段文字表述可能無法清晰理解,我們舉個例子。
如上圖所示,報文段 1 成功接收并被確認為 ACK 2,接收端的期待序號為 2,當報文段 2 丟失后,報文段 3。失序到達,但是與接收端的期望不匹配,所以接收端會重復發送冗余 ACK 2。
這樣,在超時重傳定時器到期之前,接收收到連續三個相同的 ACK 后,發送端就知道哪個報文段丟失了,于是發送方會重發這個丟失的報文段,這樣就不用等待重傳定時器的到期,大大提高了效率。
SACK
在標準的 TCP 確認機制中,如果發送方發送了 0 - 10000 序號之間的數據,但是接收方只接收到了 0 -1000, 3000 - 10000 之間的數據,而 1000 - 3000 之間的數據沒有到達接收端,此時發送方會重傳 1000 - 10000 之間的數據,實際上這是沒有必要的,因為 3000 后面的數據已經被接收了。但是發送方無法感知這種情況的存在。
如何避免或者說解決這種問題呢?
為了優化這種情況,我們有必要讓客戶端知道更多的消息,在 TCP 報文段中,有一個 SACK 選項字段,這個字段是一種選擇性確認(selective acknowledgment)機制,這個機制能告訴 TCP 客戶端,用我們的俗語來解釋就是:“我這里最多允許接收 1000 之后的報文段,但是我卻收到了 3000 - 10000 的報文段,請給我 1000 - 3000 之間的報文段”。
但是,這個選擇性確認機制的是否開啟還受一個字段的影響,這個字段就是 SACK 允許選項字段,通信雙方在 SYN 段或者 SYN + ACK 段中添加 SACK 允許選項字段來通知對端主機是否支持 SACK,如果雙方都支持的話,后續在 SYN 段中就可以使用 SACK 選項了。
這里需要注意下:SACK 選項字段只能出現在 SYN 段中。
偽超時和重傳
在某些情況下,即使沒有出現報文段的丟失也可能會引發報文重傳。這種重傳行為被稱為偽重傳(spurious retransmission) ,這種重傳是沒有必要的,造成這種情況的因素可能是由于偽超時(spurious timeout),偽超時的意思就是過早的判定超時發生。造成偽超時的因素有很多,比如報文段失序到達,報文段重復,ACK 丟失等情況。
檢測和處理偽超時的方法有很多,這些方法統稱為檢測算法和響應算法。檢測算法用于判斷是否出現了超時現象或出現了計時器的重傳現象。一旦出現了超時或者重傳的情況,就會執行響應算法撤銷或者減輕超時帶來的影響,下面是幾種算法,此篇文章暫不深入這些實現細節:
- 重復 SACK 擴展- DSACK
- Eifel 檢測算法
- 前移 RTO 恢復 - F-RTO
- Eifel 響應算法
包失序和包重復
上面我們討論的都是 TCP 如何處理丟包的問題,我們下面來討論一下包失序和包重復的問題。
包失序
數據包的失序到達是互聯網中極其容易出現的一種情況,由于 IP 層并不能保證數據包的有序性,每個數據包的發送都可能會選擇當前情況傳輸速度最快的鏈路,所以很有可能出現發送了 A - > B -> C 的三個數據包,到達接收端的數據包順序是 C -> A -> B 或者 B -> C -> A 等等。這就是包失序的一種現象。
在包傳輸中,主要分為兩種鏈路:正向鏈路(SYN)和反向鏈路(ACK)。
如果失序發生在正向鏈路,TCP 是無法正確判斷數據包是否丟失的,數據的丟失和失序都會導致接收端收到無序的數據包,造成數據之間的空缺。如果這種空缺不夠大的話,這種情況影響不大;但是如果空缺比較大的話,可能會導致偽重傳。
如果失序發生在反向鏈路,就會使 TCP 的窗口前移,然后收到重復而應該被丟棄的 ACK,導致發送端出現不必要的流量突發,影響可用網絡帶寬。
回到我們上面討論的快速重傳,由于快速重傳是根據重復 ACK 推斷出現丟包而啟動的,它不用等到重傳計時器超時。由于 TCP 接收端會對接收到的失序報文立刻返回 ACK,所以網絡中任何一個失序到達的報文都可能會造成重復 ACK。假設一旦收到 ACK,就會啟動快速重傳機制,當 ACK 數量激增,就會導致大量不必要的重傳發生,所以快速重傳應該達到重復閾值(dupthresh)再觸發。但是在互聯網中,嚴重的失序并不常見,因此 dupthresh 的值可以設置的盡量小,一般來說 3 就能處理絕大部分情況。
包重復
包重復也是互聯網中出現很少的一種情況,它指的是在網絡傳輸過程中,包可能會出現傳輸多次的情況,當重傳生成時,TCP 可能會出現混淆。
包的重復可以使接收端生成一系列的重復 ACK,這種情況可以使用 SACK 協商來解決。