TCP重傳機制
在錯綜復雜的網絡,並不一定所有的數據能正常的數據傳輸,萬一數據在傳輸過程中丟失了呢?
TCP要保證所有的數據包都可以到達,所以,必需要有重傳機制。
常見的重傳機制:
- 超時重傳
- 快速重傳
- SACK
- D-SACK
而所有重傳的機制都需要依賴通過序列號Seq與確認應答ACK。
在 TCP 中,當發送端的數據到達接收主機時,接收端主機會返回一個確認應答消息,表示已收到消息。
超時重傳
發送端發了1,2,3,4,5一共五份數據,接收端收到了1,2,於是回ack 3,然后收到了4(注意此時3還沒收到),此時的TCP會怎么辦?我們要知道,因為正如前面所說的,SeqNum和Ack是以字節數為單位,所以ack的時候,不能跳着確認,只能確認最大的連續收到的包,不然,發送端就以為之前的都收到了。
超時重傳的處理方式:接收端不再回ack(直到收到數據3),發送端死等ack 3,當發送端發現收不到3的ack超時后,會重傳3。一旦接收端收到3后,會ack 回 4——意味着3收到了,期待下一個數據4。
但是,這種方式會有比較嚴重的問題,那就是因為要死等3,所以會導致4和5即便已經收到了,而發送端也完全不知道發生了什么事,因為沒有收到Ack,所以,發送方可能會悲觀地認為也丟了,所以有可能也會導致4和5的重傳。
對此有兩種選擇:
- 一種是僅重傳timeout的包。也就是第3份數據。
- 另一種是重傳timeout后所有的數據,也就是第3,4,5這三份數據。
TCP 會在以下兩種情況發生超時重傳:
- 數據包丟失
- 確認應答丟失
超時時間
我們先來了解一下什么是 RTT
(Round-Trip Time 往返時延),從下圖我們就可以知道:
RTT
就是數據從網絡一端傳送到另一端所需的時間,也就是包的往返時間。
超時重傳時間是以 RTO
(Retransmission Timeout 超時重傳時間)表示。
假設在重傳的情況下,超時時間 RTO
「較長或較短」時,會發生什么事情呢?
上圖中有兩種超時時間不同的情況:
- 當超時時間 RTO 較大時,重發就慢,丟了老半天才重發,沒有效率,性能差;
- 當超時時間 RTO 較小時,會導致可能並沒有丟就重發,於是重發的就快,會增加網絡擁塞,導致更多的超時,更多的超時導致更多的重發。
精確的測量超時時間 RTO
的值是非常重要的,這可讓我們的重傳機制更高效。
根據上述的兩種情況,我們可以得知,超時重傳時間 RTO 的值應該略大於報文往返 RTT 的值。
至此,可能大家覺得超時重傳時間 RTO
的值計算,也不是很復雜嘛。
好像就是在發送端發包時記下 t0
,然后接收端再把這個 ack
回來時再記一個 t1
,於是 RTT = t1 – t0
。沒那么簡單,這只是一個采樣,不能代表普遍情況。
實際上「報文往返 RTT 的值」是經常變化的,因為我們的網絡也是時常變化的。也就因為「報文往返 RTT 的值」 是經常波動變化的,所以「超時重傳時間 RTO 的值」應該是一個動態變化的值。
我們來看看 Linux 是如何計算 RTO
的呢?
估計往返時間,通常需要采樣以下兩個:
- 需要 TCP 通過采樣 RTT 的時間,然后進行加權平均,算出一個平滑 RTT 的值,而且這個值還是要不斷變化的,因為網絡狀況不斷地變化。
- 除了采樣 RTT,還要采樣 RTT 的波動范圍,這樣就避免如果 RTT 有一個大的波動的話,很難被發現的情況。
RFC6289 建議使用以下的公式計算 RTO:
其中 SRTT
是計算平滑的RTT ,DevRTR
是計算平滑的RTT 與 最新 RTT 的差距。
在 Linux 下,α = 0.125,β = 0.25, μ = 1,∂ = 4。別問怎么來的,問就是大量實驗中調出來的。
如果超時重發的數據,再次超時的時候,又需要重傳的時候,TCP 的策略是超時間隔加倍。
也就是每當遇到一次超時重傳的時候,都會將下一次超時時間間隔設為先前值的兩倍。兩次超時,就說明網絡環境差,不宜頻繁反復發送。
超時觸發重傳存在的問題是,超時周期可能相對較長。那是不是可以有更快的方式呢?
於是就可以用「快速重傳」機制來解決超時重發的時間等待。
快速重傳
不以時間驅動,而以數據驅動重傳。
接收端如果沒有收到期望的數據,而收到后續亂序的包,也給客戶端回復 ACK,只不過是重復的 ACk,回復相同的ACK三次以后觸發快速重傳。
也就是說,如果,包沒有連續到達,就ack最后那個可能被丟了的包,如果發送方連續收到3次相同的ack,就重傳。Fast Retransmit的好處是不用等timeout了再重傳。
比如:如果發送方發出了1,2,3,4,5份數據,第一份先到送了,於是就ack回2,結果2因為某些原因沒收到,3到達了,於是還是ack回2,后面的4和5都到了,但是還是ack回2,因為2還是沒有收到,於是發送端收到了三個ack=2的確認,知道了2還沒有到,於是就馬上重轉2。然后,接收端收到了2,此時因為3,4,5都收到了,於是ack回6。示意圖如下:
Fast Retransmit只解決了一個問題,就是timeout的問題,它依然面臨一個艱難的選擇,就是,是重傳之前的一個還是重傳所有的問題。對於上面的示例來說,是重傳#2呢還是重傳#2,#3,#4,#5呢?因為發送端並不清楚這連續的3個ack(2)是誰傳回來的?也許發送端發了20份數據,是#6,#10,#20傳來的呢。這樣,發送端很有可能要重傳從2到20的這堆數據(這就是某些TCP的實際的實現)。
SACK
SACK(Selective Acknowledgment),在快速重傳的基礎上,返回最近收到的報文段的序列號范圍,這樣客戶端就知道,哪些數據包已經到達服務器了。
如下圖,發送方收到了三次同樣的 ACK 確認報文,於是就會觸發快速重發機制,通過 SACK
信息發現只有 200~299
這段數據丟失,則重發時,就只選擇了這個 TCP 段進行重復。
如果要支持 SACK
,必須雙方都要支持。在 Linux 下,可以通過 net.ipv4.tcp_sack
參數打開這個功能(Linux 2.4 后默認打開)。
Duplicate SACK
DSACK,即重復 SACK,這個機制是在 SACK 的基礎上,額外攜帶信息,告知發送方有哪些數據包自己重復接收了。DSACK 的目的是幫助發送方判斷,是否發生了包失序、ACK 丟失、包重復或偽重傳。讓 TCP 可以更好的做網絡流控。
栗子一號:ACK 丟包
- 「接收方」發給「發送方」的兩個 ACK 確認應答都丟失了,所以發送方超時后,重傳第一個數據包(3000 ~ 3499)
- 於是「接收方」發現數據是重復收到的,於是回了一個 SACK = 3000~3500,告訴「發送方」 3000~3500 的數據早已被接收了,因為 ACK 都到了 4000 了,已經意味着 4000 之前的所有數據都已收到,所以這個 SACK 就代表着
D-SACK
。 - 這樣「發送方」就知道了,數據沒有丟,是「接收方」的 ACK 確認報文丟了。
栗子二號:網絡延時
- 數據包(1000~1499) 被網絡延遲了,導致「發送方」沒有收到 Ack 1500 的確認報文。
- 而后面報文到達的三個相同的 ACK 確認報文,就觸發了快速重傳機制,但是在重傳后,被延遲的數據包(1000~1499)又到了「接收方」;
- 所以「接收方」回了一個 SACK=1000~1500,因為 ACK 已經到了 3000,所以這個 SACK 是 D-SACK,表示收到了重復的包。
- 這樣發送方就知道快速重傳觸發的原因不是發出去的包丟了,也不是因為回應的 ACK 包丟了,而是因為網絡延遲了。
可見,D-SACK
有這么幾個好處:
- 可以讓「發送方」知道,是發出去的包丟了,還是接收方回應的 ACK 包丟了;
- 可以知道是不是「發送方」的數據包被網絡延遲了;
- 可以知道網絡中是不是把「發送方」的數據包給復制了;
在 Linux 下可以通過 net.ipv4.tcp_dsack
參數開啟/關閉這個功能(Linux 2.4 后默認打開)。
筆記整理: