天天看點

詳解 TCP 逾時與重傳機制——長文預警

上一篇介紹 TCP 的文章「TCP 三次握手,四次揮手和一些細節」回報還不錯,還是蠻開心的,這次接着講一講關于逾時和重傳那一部分。

我們都知道 TCP 協定具有重傳機制,也就是說,如果發送方認為發生了丢包現象,就重發這些資料包。很顯然,我們需要一個方法來「猜測」是否發生了丢包。最簡單的想法就是,接收方每收到一個包,就向發送方傳回一個 ACK,表示自己已經收到了這段資料,反過來,如果發送方一段時間内沒有收到 ACK,就知道很可能是資料包丢失了,緊接着就重發該資料包,直到收到 ACK 為止。

你可能注意到我用的是「猜測」,因為即使是逾時了,這個資料包也可能并沒有丢,它隻是繞了一條遠路,來的很晚而已。畢竟 TCP 協定是位于傳輸層的協定,不可能明确知道資料鍊路層和實體層發生了什麼。但這并不妨礙我們的逾時重傳機制,因為接收方會自動忽略重複的包。

逾時和重傳的概念其實就是這麼簡單,但内部的細節卻是很多,我們最先想到的一個問題就是,到底多長時間才能算逾時呢?

逾時是怎麼确定的?

一刀切的辦法就是,我直接把逾時時間設成一個固定值,比如說 200ms,但這樣肯定是有問題的,我們的電腦和很多伺服器都有互動,這些伺服器位于天南海北,國内國外,延遲差異巨大,打個比方:

  • 我的個人部落格搭在國内,延遲大概 30ms,也就是說正常情況下的資料包,60ms 左右就已經能收到 ACK 了,但是按照我們的方法,200ms 才能确定丢包(正常可能是 90 到 120 ms),這效率實在是有點低。
  • 假設你通路某國外網站,延遲有 130 ms,這就麻煩了,正常的資料包都可能被認為是逾時,導緻大量資料包被重發,可以想象,重發的資料包也很容易被誤判為逾時。。。雪崩效應的感覺

是以設定固定值是很不可靠的,我們要根據網絡延遲,動态調整逾時時間,延遲越大,逾時時間越長。

在這裡先引入兩個概念:

  • RTT(Round Trip Time):往返時延,也就是資料包從發出去到收到對應 ACK 的時間。RTT 是針對連接配接的,每一個連接配接都有各自獨立的 RTT。
  • RTO(Retransmission Time Out):重傳逾時,也就是前面說的逾時時間。

比較标準的 RTT 定義:

Measure the elapsed time between sending a data octet with a particular sequence number and receiving an acknowledgment that covers that sequence number (segments sent do not have to match segments received). This measured elapsed time is the Round Trip Time (RTT).

經典方法

最初的規範「RFC0793」采用了下面的公式來得到平滑的 RTT 估計值(稱作 SRTT):

​ SRTT <- α·SRTT +(1 - α)·RTT

RTT 是指最新的樣本值,這種估算方法叫做「指數權重移動平均」,名字聽起來比較高大上,但整個公式比較好了解,就是利用現存的 SRTT 值和最新測量到的 RTT 值取一個權重平均。

有了 SRTT,就該設定對應的 RTO 的值了,「RFC0793」是這麼算的:

​ RTO = min(ubound, max(lbound, (SRTT)·β))

這裡面的 ubound 是 RTO 的上邊界,lbound 為 RTO 的下邊界,β 稱為時延離散因子,推薦值為 1.3 ~ 2.0。這個計算公式就是将 (SRTT)·β 的值作為 RTO,隻不過另外限制了 RTO 的上下限。

這個計算方法,初看是沒有什麼問題(至少我是這麼感覺的),但是實際應用起來,有兩個缺陷:

There were two known problems with the RTO calculations specified in RFC-793. First, the accurate measurement of RTTs is difficult when there are retransmissions. Second, the algorithm to compute the smoothed round-trip time is inadequate [TCP:7], because it incorrectly assumed that the variance in RTT values would be small and constant. These problems were solved by Karn\'s and Jacobson\'s algorithm, respectively.

這段話摘自「RFC1122」,我來解釋一下:

  • 當出現資料包重傳的情況下,RTT 的計算就會很“麻煩”,我畫了張圖來說明這些情況:
    詳解 TCP 逾時與重傳機制——長文預警
    圖上列了兩種情況,這兩種情況下計算 RTT 的方法是不一樣的(這就是所謂的重傳二義性):
    • 情況一:RTT = t2 - t0
    • 情況二:RTT = t2 - t1
    但是對于用戶端來說,它不知道發生了哪種情況,選錯情況的結果就是 RTT 偏大/偏小,影響到 RTO 的計算。(最簡單粗暴的解決方法就是忽略有重傳的資料包,隻計算那些沒重傳過的,但這樣會導緻其他問題。。詳見 Karn\'s algorithm)
  • 另一個問題是,這個算法假設 RTT 波動比較小,因為這個權重平均的算法又叫低通濾波器,對突然的網絡波動不敏感。如果網絡時延突然增大導緻實際 RTT 值遠大于估計值,會導緻不必要的重傳,增大網絡負擔。( RTT 增大已經表明網絡出現了過載,這些不必要的重傳會進一步加重網絡負擔)。

标準方法

說實話這個标準方法比較,,,麻煩,我就直接貼公式了:

​ SRTT <- (1 - α)·SRTT + α·RTT //跟基本方法一樣,求 SRTT 的權重平均

​ rttvar <- (1 - h)·rttvar + h·(|RTT - SRTT |) //計算 SRTT 與真實值的差距(稱之為絕對誤差|Err|),同樣用到權重平均

​ RTO = SRTT + 4·rttvar //估算出來的新的 RTO,rttvar 的系數 4 是調參調出來的

這個算法的整體思想就是結合平均值(就是基本方法)和平均偏差來進行估算,一波玄學調參得到不錯的效果。如果想更深入了解這個算法,參考「RFC6298」。

重傳——TCP的重要事件

基于計時器的重傳

這種機制下,每個資料包都有相應的計時器,一旦超過 RTO 而沒有收到 ACK,就重發該資料包。沒收到 ACK 的資料包都會存在重傳緩沖區裡,等到 ACK 後,就從緩沖區裡删除。

首先明确一點,對 TCP 來說,逾時重傳是相當重要的事件(RTO 往往大于兩倍的 RTT,逾時往往意味着擁塞),一旦發生這種情況,TCP 不僅會重傳對應資料段,還會降低目前的資料發送速率,因為TCP 會認為目前網絡發生了擁塞。

簡單的逾時重傳機制往往比較低效,如下面這種情況:

詳解 TCP 逾時與重傳機制——長文預警

假設資料包5丢失,資料包 6,7,8,9 都已經到達接收方,這個時候用戶端就隻能等伺服器發送 ACK,注意對于包 6,7,8,9,伺服器都不能發送 ACK,這是滑動視窗機制決定的,是以對于用戶端來說,他完全不知道丢了幾個包,可能就悲觀的認為,5 後面的資料包也都丢了,就重傳這 5 個資料包,這就比較浪費了。

快速重傳

快速重傳機制「RFC5681」基于接收端的回報資訊來引發重傳,而非重傳計時器逾時。

剛剛提到過,基于計時器的重傳往往要等待很長時間,而快速重傳使用了很巧妙的方法來解決這個問題:伺服器如果收到亂序的包,也給用戶端回複 ACK,隻不過是重複的 ACK。就拿剛剛的例子來說,收到亂序的包 6,7,8,9 時,伺服器全都發 ACK = 5。這樣,用戶端就知道 5 發生了空缺。一般來說,如果用戶端連續三次收到重複的 ACK,就會重傳對應包,而不需要等到計時器逾時。

詳解 TCP 逾時與重傳機制——長文預警

但快速重傳仍然沒有解決第二個問題:到底該重傳多少個包?

帶選擇确認的重傳

改進的方法就是 SACK(Selective Acknowledgment),簡單來講就是在快速重傳的基礎上,傳回最近收到的封包段的序列号範圍,這樣用戶端就知道,哪些資料包已經到達伺服器了。

來幾個簡單的示例:

  • case 1:第一個包丢失,剩下的 7 個包都被收到了。

    當收到 7 個包的任何一個的時候,接收方會傳回一個帶 SACK 選項的 ACK,告知發送方自己收到了哪些亂序包。注:Left Edge,Right Edge 就是這些亂序包的左右邊界。

Triggering    ACK      Left Edge   Right Edge
             Segment

             5000         (lost)
             5500         5000     5500       6000
             6000         5000     5500       6500
             6500         5000     5500       7000
             7000         5000     5500       7500
             7500         5000     5500       8000
             8000         5000     5500       8500
             8500         5000     5500       9000

           
  • case 2:第 2, 4, 6, 8 個資料包丢失。
    • 收到第一個包時,沒有亂序的情況,正常回複 ACK。
    • 收到第 3, 5, 7 個包時,由于出現了亂序包,回複帶 SACK 的 ACK。
    • 因為這種情況下有很多碎片段,是以相應的 Block 段也有很多組,當然,因為選項字段大小限制, Block 也有上限。
Triggering  ACK    First Block   2nd Block     3rd Block
          Segment            Left   Right  Left   Right  Left   Right
                             Edge   Edge   Edge   Edge   Edge   Edge

          5000       5500
          5500       (lost)
          6000       5500    6000   6500
          6500       (lost)
          7000       5500    7000   7500   6000   6500
          7500       (lost)
          8000       5500    8000   8500   7000   7500   6000   6500
          8500       (lost)
           

不過 SACK 的規範「RFC2018」有點坑爹,接收方可能會在提供一個 SACK 告訴發送方這些資訊後,又「食言」,也就是說,接收方可能把這些(亂序的)資料包删除掉,然後再通知發送方。以下摘自「RFC2018」:

Note that the data receiver is permitted to discard data in its queue that has not been acknowledged to the data sender, even if the data has already been reported in a SACK option. Such discarding of SACKed packets is discouraged, but may be used if the receiver runs out of buffer space.

最後一句是說,當接收方緩沖區快被耗盡時,可以采取這種措施,當然并不建議這種行為。。。

由于這個操作,發送方在收到 SACK 以後,也不能直接清空重傳緩沖區裡的資料,一直到接收方發送普通的,ACK 号大于其最大序列号的值的時候才能清除。另外,重傳計時器也收到影響,重傳計時器應該忽略 SACK 的影響,畢竟接收方把資料删了跟丢包沒啥差別。

DSACK 擴充

DSACK,即重複 SACK,這個機制是在 SACK 的基礎上,額外攜帶資訊,告知發送方有哪些資料包自己重複接收了。DSACK 的目的是幫助發送方判斷,是否發生了包失序、ACK 丢失、包重複或僞重傳。讓 TCP 可以更好的做網絡流控。

關于 DSACK,「RFC2883」裡舉了很多例子,有興趣的讀者可以去閱讀一下,我這裡就不講那麼細了。

逾時和重傳的内容大概就是這麼多,希望對你有所幫助。

詳解 TCP 逾時與重傳機制——長文預警