天天看點

為什麼說TCP是可靠的網絡傳輸協定?

作者:linux技術棧

我們知道TCP是流式協定,通過位元組流的形式在各個網絡裝置間流動,當這些位元組流入到目标機器之後,由目标機器的網絡協定棧通過每個資料段裡的标記(TCP頭)還原資料。不難想到,基于流式的傳輸至少有兩大問題需要解決。第一,假如某個資料段丢了怎麼辦?第二,傳輸過程中發生擁堵怎麼辦?當然,實際的TCP協定要解決的遠不止這兩個問題。

這裡說明一下,很多時候我們把鍊路層、IP層、TCP層發送的資料都稱為資料包,其實是不準确的,每一層都有自己的叫法,鍊路層叫資料幀(Frame),IP層叫資料包(Packet),TCP層叫資料段(Segment)。

MSS

上面我們說TCP協定中傳輸的資料叫資料段,而負責确定資料段大小上限的便是MSS(Max Segment Size)最大資料段,這個大小指的是TCP實際傳輸的資料,不包含IP和TCP頭。

MSS的存在,一是為了一次盡可能多的傳輸資料,二是為了避免IP層MTU拆包。關于MSS和MTU可以參見為什麼MSS比MTU小?

我們先來看第一個問題,假如某個資料段丢了怎麼辦?

為什麼說TCP是可靠的網絡傳輸協定?

重傳與确認

比較容易想到的是,每發一個資料段,都等待接收端的一個信号,同時啟動一個定時器,在定時器觸發之前如果收到了接收端的信号就說明資料發送成功了,如果在規定時間内沒有收到信号就觸發定時器重新發送一次,直至收到接收端的信号,然後才能發送下一個資料段,這就是TCP協定最初的ACK機制,如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

上面這種方式,雖然可以保證資料不丢,但是效率非常低,一次隻能發一個資料段,并且隻能收到接收端的ACK之後才能發下一個資料段,效率低下。

是以,人們又想,發送端是不是不用等到接收端的ACK,連續發送多個資料段,目标機器接收到資料之後,目标機器對收到的資料段依次發送ACK,表示這個資料段我收到了,如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

上面這種方式,雖然提升了網絡效率,但還是存在很多問題。這裡先忘掉TCP的頭、MSS等各種協定内容。假如現在發送一串字元串"hello world",過程如下:

為什麼說TCP是可靠的網絡傳輸協定?

一共分三次發出去,第一次"hel",第二次"lo wor",第三次"ld"。當目标機器收到資料的時候,如何還原資料呢?實際上目标機器隻需要知道每次發送的資料相對于原資料的開始結束位置就可以還原出原資料了。

比如上面的例子,我們把"hello world"看成一個長度為11的字元數組,将"hel"發送出去的時候告訴目标機器這部分資料在0-2的位置,"lo wor"發出去告訴目标機器這部分資料在3-8的位置,同理"ld"在9-10的位置。這樣,接收端收到這些資料之後,通過所在的位置就可以把資料還原了。如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

上面的的例子中,需要有一個前提,每一次發的資料段是有序的。比如"ld",不能是"dl"。這樣,目标機器接收到的每個資料段永遠都是有序的資料。但是段與段并不要求是有序的,"ld"、"lo wor"、"hel"可以是無序的,比如:

為什麼說TCP是可靠的網絡傳輸協定?

由于每次發送的資料段裡面都包含了原始資料的起始結束位置,是以就算不是按順序到達目标機器,目标機器也可以還原出資料。

相關視訊推薦

tcp/ip,accept,11個狀态,細枝末節的秘密,還有哪些你不知道?

100行代碼實作tcp/ip協定棧,自行準備好Linux系統

面試 從網卡 聊到tcp/ip協定棧,再到應用程式

需要C/C++ Linux伺服器架構師學習資料加qun812855908擷取(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等),免費分享

為什麼說TCP是可靠的網絡傳輸協定?

正常情況下,上面的方案看起來很完美,但是由于網絡天生的複雜環境可能會出現各種異常情況。比如,資料段是有可能丢失的,例如,上面例子中第二個資料段丢了,這個時候目标機器就沒法還原資料了,怎麼辦?一種辦法是它可以向發送端索要,比如接收端收到了"hel",但是"lo wor"一直沒收到,它就告訴發送端下一個資料段你要從3這個位置發,用戶端收到之後就知道從3開始的這個資料段丢了,然後就可以重發這個資料段。

其實上面的例子可以再簡化一下,我們讓發送端隻攜帶一個相對原資料的偏移值,比如:

為什麼說TCP是可靠的網絡傳輸協定?

上圖中,每次發送的資料段都攜帶了這個資料段結尾相對于原資料的相對位置。當目标機器收到資料的時候,就可以從第一塊開始進行組裝。假如第二個資料塊發生了丢包,那接收端就問發送端要3~8這些位置的資料。并且就算發生丢包,接收端還是可以根據資料段的大小和偏移位置将後面的資料填充到對應的位置,如下:

為什麼說TCP是可靠的網絡傳輸協定?

上面的這個過程,就是TCP的序列号機制。

很多文章裡對TCP的序列号僅僅停留在随機數、累加就完了,但我覺得序列号才是TCP協定的精華所在,是值得花時間搞明白的。

TCP協定序列号機制

在TCP頭中,有兩個32位的序列号,一個是發送序列号,另一個是确認序列号。發送序列号表示發送端要從哪裡開始發,确認序列号表示這之前的資料目标機器都收到了。

我們可以進入Wireshark的 Statistic>Flow Graph 檢視資料段的流轉圖,下面是一個http請求的過程。

為什麼說TCP是可靠的網絡傳輸協定?

上面是一個Wireshark抓的資料包,經過3次握手之後雙方的序列号和确認号都是1,這裡要說明的是,TCP協定的序列号并不是從0開始的,而是每次都生成一個随機數,Wireshark為了檢視友善使用了相對序列号,也就是在随機的序列号上加上一個偏移值。

第4個包,用戶端發送了361位元組資料,此時用戶端的序列号和确認号都還是1。

第5個包,服務端收到361位元組資料,在發送端的序列号基礎上加上361位元組,是以确認号是362位元組,此時服務端的序列号還是1。

第6個包開始,服務端連續發送了6個1350位元組大小的資料,此時序列号分别是1、1351、2701、4051、5401、6751,确認号是362。

第12個包,用戶端一次性确認6個資料段,确認号是1350*6 + 1 = 8101,這期間用戶端并沒有發送資料,是以序列号還是362。

可以看到,通過序列号可以在連續發送的情況下保證資料的順序性。

通過上面的例子可以發現,序列号是越來越大的,那是不是可以一直增長下去呢?

實際上,TCP的序列号最大為2^32,也就是4G位元組,超過之後會從0開始重新計算,如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

序列号的這個特點會導緻一個問題,就是序列号的回繞,比如下面這個例子:

為什麼說TCP是可靠的網絡傳輸協定?

上面例子中,時間點B發送的1G:2G的資料包丢失了,如果在F時刻重發,網絡協定棧就沒法區分到底是B時刻的資料段還是F時刻的資料段了。

為了解決這個問題,TCP協定在每個資料段的頭裡面加了一個timestamp的Option。其kind等于8,長度為10位元組,如下:

為什麼說TCP是可靠的網絡傳輸協定?

有了時間戳之後通過判斷時間戳大小就可以解決上面的序列号回繞的問題了。

RTO和RTT

TCP的重傳機制離不開兩個變量,RTO和RTT。下面我們先來搞明白RTO和RTT是什麼,它們之間又有什麼樣的關系?

RTT可以認為是資料段從發送到收到ACK之間消耗的總時長,如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

在實際的網絡環境中,可能有各種各樣的情況,如果隻是通過發送出去的時間和收到ACK的時間來計算RTT,有可能是不準确的。比如,考慮下面這種情況:

為什麼說TCP是可靠的網絡傳輸協定?

圖中,(a)中發生了重傳,這個時候我們是以第一次為準?還是第二次發送時間為準呢?(b)在發生了重傳之後,又收到上一次發送的ACK,這個時候又以哪個為準呢?

為了搞明白這個問題,我們先來看RTO,RTO是TCP的定時重傳計時器,當資料段發送出去之後都會啟動一個RTO,當在規定的時間内沒有收到ACK,就重發資料段。

我們可以考慮一下,這個RTO設定成多少比較合适呢?假如我們設定成1s,如果資料段都能在1s之内發送完就不會觸發RTO重傳。但是,如果我們網絡環境不好的情況下,可能出現非常多的資料段發送超過1s,這個時候就會觸發大量的重傳,顯然這會急劇降低網絡傳輸效率。

你可能會說,那就設定成2s嘛,或者1分鐘。好,假設我們設定成2s。不巧的是,中間出現了丢包,這個時候要等2s才能觸發重傳。顯然,RTO也不是越長越好。

比較好的方式是,我們知道每次傳輸所花費的時間,也就是RTT,将RTO設定成比RTT稍大,這樣就即可以減少重傳同時也可以在出現丢包時盡快的重傳。但實際情況中RTT可能會遇到各種各樣的問題,比如下面這樣:

為什麼說TCP是可靠的網絡傳輸協定?

圖中,a)由于RTT比實際要小,是以導緻RTO也比較小,最終就會導緻大量無效的重傳。b)RTT如果取RTT1又太大,取RTT2又可能太小,似乎怎麼取都不太合适。是以,要計算RTO就要先有一個比較準确的RTT。

TCP協定解決這個問題的方式也很簡單,我們上面提到說,TCP的頭部包含了一個時間戳的資訊,再回顧一下,這個時間戳Option實際是包含了兩個時間戳。一個接收時的時間戳,一個是回顯時間戳,也就是發送時的時間戳。通過這兩個時間戳就可以計算出準确的RTT了。這也是目前主流作業系統TCP計算RTT的方式。

但是,仔細考慮一下,由于網絡傳輸環境時刻都在變化。例如,一個RTT計算好了,但是當發送資料段的時候實際的RTT又變長了,這個時候原本目标機器可以正常收到資料段,但因為實際RTT變長了,就有可能導緻頻繁的重傳。是以,這種方案似乎并不完美。

更平滑的RTO

在文檔RFC793中提出一種計算平滑RTO的方案,引入了兩個定量α和β,RTO的計算公式如下:

SRTT = ( α * SRTT ) + ((1 - α) * RTT)

RTO = min(UBOUND, max(LBOUND, (β * SRTT) ))

RFC793

其中,SRTT是上一次計算出的RTT,第一次為0。α是一個常量,推薦值0.9,β在推薦是1.3~2之間,值越大越平滑。UBOUND是指測試時間最大值,LBOUND指測試時間最小值,比如測量時間最大1分鐘,最小1秒鐘。

但是,這種計算方式,在RTT波動特别大的時候并不好使,是以目前很多作業系統并沒有使用這種方式,而是基于RTT方差計算RTO。

基于RTT方差計算RTO

在文檔RFC6298中提出了使用RTT方差計算RTO,而實作原理也很簡單,首次計算,R是第一次測出的RTT

SRTT = R

RTTVAR = R/2

RTO = SRTT + max (G, K*RTTVAR)

RFC6298

後續的計算

SRTT = (1 - α) * SRTT + α * R’

RTTVAR = (1 - β) * RTTVAR + β * |SRTT - R’|

RTO = SRTT + max (G, K*RTTVAR)

RFC6298

其中,α = 1/8, β = 1/4,K = 4,G 為最小時間顆粒,它們的取值并沒有特别的依據,都是根據曆史大量資料統計得到的一個公認最好的一組值。

通過RTT方差即使在網絡波動比較大的情況下也可以計算出一個相對平滑的RTO。

滑動視窗

我們試想一種情況,當接收端已經非常繁忙了,但發送端很稱職,還在不斷的發送資料。此時,接收端由于處理不過來,資料包就會丢失,進而觸發重傳。這樣,網絡中會出現大量的重傳資料包,傳輸效率會變得非常低,甚至出現當機。

為了解決這個問題,TCP引入了滑動視窗的概念,通信雙方都有一個發送視窗和接收視窗,如下:

為什麼說TCP是可靠的網絡傳輸協定?

0~31位元組表示已經收到ACK的資料,32~45位元組的資料表示已經發送出去了,但還沒收到ACK,46~51位元組表示還可以發送的位元組數。51之後的位元組表示超出了接收方處理的範圍。

上面的例子中,32~51位元組就是發送視窗,可以簡單認為和對端的接收視窗大小是一樣的,但由于網絡因素實際情況是在同一時刻可能并不一樣。

當Category #3為0的時候也就表示對端已經不能繼續處理了。直到,已發送未确認的部分收到對端的确認ACK之後,将視窗向右移動。如下:

為什麼說TCP是可靠的網絡傳輸協定?

有了滑動視窗之後,雙方就可以不斷的協商視窗的大小,進而進行流控。比如在3次握手階段就會告訴對方自己的接收視窗大小,比如:

為什麼說TCP是可靠的網絡傳輸協定?

第一次,用戶端發送的SYN包裡告訴對方自己的接收視窗大小是65535位元組,服務端回複ACK的時候告訴用戶端自己的接收視窗是28960位元組。通過三次握手之後,服務端的發送視窗就是65535位元組,用戶端的發送視窗就是28960位元組,這樣就完成了發送視窗的協商。

實際上,這個視窗在真正發送資料的過程中,每次都會帶到TCP頭中,這樣對端就可以通過和目前自己的發送視窗比較,進而決定是擴充還是收縮視窗。

下面通過一個例子來進行說明,我們假設MSS固定不變,視窗不變,我們将發送視窗分為三部分,如下:

為什麼說TCP是可靠的網絡傳輸協定?

各個部分如下:

  • SND.WND 表示發送視窗總大小
  • SND.UNA 表示發送視窗第一個位元組所在位置
  • SND.NXT 下一個可發送的位元組所在位置

接收視窗如下:

為什麼說TCP是可靠的網絡傳輸協定?

各個部分如下:

  • RCV.WND 表示接收視窗大小
  • RCV.NXT 表示下一個要處理的位元組所在位置

然後我們來看下面這個具體的例子,這裡假設MSS和視窗不發生變化,如下表:

為什麼說TCP是可靠的網絡傳輸協定?

下面是用戶端處理過程:

為什麼說TCP是可靠的網絡傳輸協定?

對于用戶端,一開始SND.UNA和SND.NXT都是1,SND.WND=360, RCV.NXT=241, RCV.WND=200,接着,用戶端發送了140位元組資料,此時序列号是1,用戶端的發送視窗SND.UNA=1,SND.NXT=141,發送視窗還可以發送220位元組。

服務端對140位元組資料确認并發送了80位元組資料,用戶端收到ACK之後發送視窗右移140位元組,SND.WND又回到了300位元組大小。

服務端的視窗變遷過程:

為什麼說TCP是可靠的網絡傳輸協定?

一開始,Server端的SND.NXT=241,SND.UNA=241,第一次發送了80位元組資料之後,SND.NXT變成了321(241+80),此時發送視窗可用空間為120位元組。

在收到Client端對80位元組的ACK之前,Server端又發了120位元組資料。此時,SND.NXT=441(421+120),發送視窗就占滿了,可發送空間為0。

直到,收到Client端對80位元組資料的ACK,Server端的發送視窗向右移動80位元組。此時,SND.UNA變成了321,發送視窗可用空間為80位元組。

然後Server端再次收到Client端的對120位元組的ACK,SND.UNA變成441,發送視窗可用空間又回到200位元組。

Server端再發送160位元組,SND.NXT=601(441+160),發送視窗可用空間變為40位元組。

最後,Server端收到160位元組的ACK,SND.UNA變成601,發送視窗可用空間又回到200位元組。

滑動視窗與網絡協定棧緩存的關系

滑動視窗在作業系統中實際上就是配置設定了一塊固定大小的記憶體,而視窗的調整也會導緻緩存的收縮,兩台裝置之間的通信,如果機器的配置不一樣,會怎麼影響實際的網絡傳輸呢?下面我們就來看一下常見的幾種情況。

1. 應用層沒有及時讀取緩存

為什麼說TCP是可靠的網絡傳輸協定?

上圖中,到第9步的時候,由于應用層沒有及時從網絡協定棧的緩存讀取資料,導緻接收視窗被填滿了,這個時候,發送視窗和接收視窗都為0。當發送視窗為0的情況下,發送端将停止發送資料。

但是,上面的這種情況會有一個問題,當發送端停止發送資料之後,如果接收端不主動通知發送端,就無法更新視窗大小,這個連接配接就卡在這裡了。

是以,在TCP協定中,出現這種情況之後都會有一定時器,定期發送視窗通告。進而有機會恢複資料的傳輸。

2. 收縮視窗導緻的丢包

為什麼說TCP是可靠的網絡傳輸協定?

上圖中,第2步服務端給到用戶端一個視窗通告,視窗大小為100位元組,但此時客戶的發送視窗的緩存上還儲存着180位元組的資料,這就會導緻剩下的80位元組資料丢失。

是以,現代作業系統一般都是先收縮視窗再調整緩存大小。在視窗關閉後定時探測視窗大小。

飛行中封包的适合數量

為什麼說TCP是可靠的網絡傳輸協定?

調整接收視窗與應用緩存

net.ipv4.tcp_adv_win_scale = 1

應用緩存 = buffer / (2^tcp_adv_win_scale)

Linux中對TCP緩沖區的調整方式

• net.ipv4.tcp_rmem = 4096 87380 6291456

• 讀緩存最小值、預設值、最大值,機關位元組,覆寫 net.core.rmem_max

• net.ipv4.tcp_wmem = 4096 16384 4194304

• 寫緩存最小值、預設值、最大值,機關位元組,覆寫net.core.wmem_max

• net.ipv4.tcp_mem = 1541646 2055528 3083292

• 系統無記憶體壓力、啟動壓力模式閥值、最大值,機關為頁的數量

• net.ipv4.tcp_moderate_rcvbuf = 1

• 開啟自動調整緩存模式

糊塗視窗綜合症

糊塗視窗綜合症指的是,由于某些原因(比如服務端非常繁忙的情況下)導緻非常小的視窗通告,導緻的傳輸效率下降的問題,如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

一開始Client端的發送視窗是360位元組,并一次性發送了360位元組到Server端。Server端收到之後,對這360位元組進行了ACK,同時給Client端發送了一個120位元組大小的視窗通告。

Client端收到視窗通告之後将發送視窗設定為了120位元組,又發了120位元組資料到Server端。Server端因為非常繁忙處理不過來,将視窗又改為了80位元組。

由于視窗變小,每次發送的有效資料變小,網絡效率也會變得非常低。是以,要提升網絡傳輸效率就應該盡量避免小視窗。

SWS避免算法

SWS應該說是一種解決方案,發送端和接收端通過不同的算法實作。

在接收端使用David D Clard算法,這個算法的原理是通過每次視窗移動的大小來決定發送視窗通告的大小,具體實作也很簡單,當視窗邊界移動值小于min(MSS, 緩存大小/2)的時候,視窗大小為0。

在發送端,使用Nagle算法,其原理可以總結如下:

  • 不存在已發送未确認的封包段時,立刻發送資料
  • 存在未确認封包段時,滿足下面兩個條件時再發送
    • a.沒有已發送未确認封包段,
    • b.資料長度達到MSS大小

Nagle算法的示例如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

延遲确認

積極的确認會導緻大量沒有攜帶有效資料的資料段(比如隻包含了TCP頭)的ACK,然後發送端還要一條條處理對端過來的ACK。

我們想象一下,當發送端發送一連串的資料段之後,其實接收端可以收了這一批資料之後再統一回ACK,或者等有資料發送給發送端的時候一并回複ACK,這就是TCP的延遲确認。它的核心邏輯如下:

  • 當有響應資料(發送給對端的資料)要發送時,ACK會随着響應資料立即發送給對方
  • 如果沒有響應資料,ACK的發送将會有一個延遲,以等待看是否有響應資料可以一起發送
  • 如果在等待發送ACK期間,對方的第二個資料段又到達了,這時要立即發送ACK

下圖中右邊是一個延遲确認的例子:

為什麼說TCP是可靠的網絡傳輸協定?

可以看到,右圖中,當"H"發送出去之後,接收端沒有響應資料,此時第二個資料段還沒機會發送。是以,"H"的ACK在500ms之後才被發送出去。

當Nagle遇上延遲确認

上面Nagle算法解決小資料段問題,延遲确認可以減少接收端積極的ACK導緻的網絡性能問題,但是當Nagle和延遲确認這兩種機制組合在一起又會産生一些問題,下面我們來具體看,如圖:

為什麼說TCP是可靠的網絡傳輸協定?

可以看到,W1發送之後,根據Nagle算法的規則,此時存在一個已發送未确認的資料段,資料段長度也沒達到MSS的長度。是以,後面的資料暫時不會被發送。而此時接收端沒有要響應的資料,也沒有第二個未确認的ACK,然後就進入到延遲确認的計時器,并在200ms之後發送ACK。

可以看到,整個過程中,由于觸發了延遲确認,整個傳輸過程增加了不必要的耗時。

現代作業系統的網絡程式設計接口都可以通過設定套接字選項來選擇性的關閉延遲确認和Nagle算法。設定項分别為:

延遲确認:TCP_QUICKACK

Nagle:TCP_NODELAY

更加激進的Nagle: TCP_CORK

在Linux中還有一種更激進的Nagle實作,其原理是結合Sendfile零拷貝技術,就是不需要将資料先拷貝到使用者态再拷貝到核心态,而是可以直接将發送的檔案資料拷貝到核心态的記憶體上。

擁塞控制

在整個網絡世界裡,數以億計的網絡裝置參與其中,一個資料包可能要經過許多中間網裝置才能最終到達目的地。而中間所經過裝置的性能和繁忙程度各不一樣。要想讓網絡達到最佳的傳輸效率就需要考慮中間所經過的網絡裝置。而不能隻考慮發送端和接收端。

而擁塞控制就是為了解決在整個傳輸鍊路上根據各個網絡裝置的實作情況将傳輸性能最大化。

為什麼說TCP是可靠的網絡傳輸協定?

擁塞控制前後經曆了4個版本,也可以說是4種不同的解決方案,分别是:

  • 在RFC6582中提出的 Reno & New Reno
  • 可以參考http://intronetworks.cs.luc.edu/current/html/reno.html#tcp-reno-and-fast-recovery
  • BIC算法,在Linux核心2.6.8-2.6.18有實作
  • BIC算法實際上是基于ACK驅動的,檢測丢包來實作擁塞視窗的調整。但是基于ACK就相當于和RTT強相關,而網絡中因為各種原因都會導緻RTT的波動。BIC算法的實作原理可以參考:https://blog.csdn.net/dog250/article/details/53013410
  • 在RFC8312文檔中提出了CUBIC算法并在Linux2.6.19中實作
  • CUBIC算法可以不依賴RTT進行擁塞視窗的調整,可以參考:https://blog.csdn.net/dog250/article/details/53013410
  • B B R算法在Linux4.9開始支援
  • B B R算法是由Google提出的一個擁塞控制算法。

假如我們把網絡傳輸比喻成一根水管,最理想的情況是水管裡面的水量 = 水管直經 * 長度。對于同一條傳輸路徑來講,這條網絡路徑的容量 = 帶寬 * RTT。這裡的帶寬表示機關時間能傳輸的資料大小,而RTT我們在前面已經詳細講過了。

水管在現實世界中有大有小,有長有短。在網絡中也是一樣,帶寬有大有小,RTT有快有慢。

前面提到了滑動視窗,主要是用來解決發送端與接收端之間的視窗協商,但無法感覺到中間裝置的情況。而擁塞控制就是考慮了整個鍊路發送過快或者過慢的問題。為了實作擁塞控制又引入了擁塞視窗(Congestion Window)簡稱cwnd。

擁塞視窗的引入使得TCP協定變得更加複雜。實際發送視窗大小 = min(擁塞視窗, 發送視窗 ),你可以停下來想想為什麼實際發送視窗要取擁塞視窗和發送視窗的最小值。

擁塞控制一般都是配合慢啟動來實作,如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

每收到一個ACK,cwnd(擁塞視窗)擴充一倍,但是第一次啟動視窗大小如何确定的呢?慢啟動的初始視窗IW(Initial Window)的變遷分為3個版本:

  1. 在1997年的RFC2001中規定,慢啟動初始視窗為SMSS大小。
  2. 在1998年的RFC2414檔案中規定如下:

IW = min(4*SMSS, max(2*SMSS, 4380 bytes))

3. 在2013年的RFC6928文檔中規定如下:

IW = min(10*MSS, max(2*MSS, 14600))

為什麼說TCP是可靠的網絡傳輸協定?

擁塞避免

顯然,每次翻倍很快就會達到網絡鍊路中某個裝置的極限,為了避免因為慢啟動頻繁導緻的擁塞問題,我們就需要避免頻繁觸發的擁塞問題,這便是擁塞避免算法。

擁塞避免使用一個慢啟動門檻值ssthresh(slow start threshold)來控制視窗大小,其原理是當擁塞視窗達到ssthresh 後,以線性方式增加擁塞視窗大小,而不是每次翻倍,cwnd += SMSS * SMSS/cwnd,如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

通過擁塞避免算法,就可以避免頻繁的擁塞問題,如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

可以看到,通過引入擁塞控制的門檻值ssthresh之後,當觸發了擁塞控制算法就可以在接下來有效的避免每次因為擁塞視窗增長過快頻繁導緻的擁塞問題。如果不了解擁塞控制算法解決了什麼問題,可以回顧一下滑動視窗那一小節。

失序資料段

前面不止一次提到資料段丢失的問題,而資料段丢失一般情況下都會出現失序資料段。比如封包丢失産生的連續失序ACK段,網絡路徑與裝置導緻資料段失序也會産生少量失序ACK段,若封包重複也會産生少量失序ACK段。

為什麼說TCP是可靠的網絡傳輸協定?

快速重傳

為了解決失序資料段的問題,RFC2581文檔引入了快速重傳算法,它的運作邏輯如下:

對于接收方來講

  1. 當收到一個失序資料段時,立刻發送它所期待的缺口ACK序列号。
  2. 當接收到填充失序缺口的資料段時,立刻發送它所期待的下一個ACK序列号。

對于發送方來講:

當接收到3個重複的失序ACK段(4個相同的失序ACK段)時,不再等待重傳定時器的觸發,立刻基于快速重傳機制重發封包段

下圖描述了快速重傳機制的工作過程。

為什麼說TCP是可靠的網絡傳輸協定?

逾時不會導緻快速重傳

為什麼說TCP是可靠的網絡傳輸協定?

快速重傳一定要進入慢啟動嗎?我們知道慢啟動會突然減少資料流,是以對于快速重傳來講,我們并不希望進入慢啟動流程。而是直接通過ssthresh門檻值計算得到一個擁塞視窗,如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

快速恢複

在RFC2581文檔中提出了快速恢複算法 ,它的觸發時機是啟動快速重傳且正常未失序ACK段到達前觸發快速恢複,這句話啥意思呢?簡單來說就是出現了失序資料段并且觸發了快速重傳到恢複正常不再有失序資料段的這段時間内會觸發快速恢複,再簡單點說就是快速恢複是由快速重傳觸發的,它的運作邏輯如下:

  1. 将ssthresh設定為目前擁塞視窗的一半,設目前cwnd為ssthresh加上3*MSS
  2. 每收到一個重複ACK, cwnd增加1個MSS
  3. 當新資料ACK到達後,設定cwnd為ssthresh
為什麼說TCP是可靠的網絡傳輸協定?

SACK與選擇性重傳算法

上面分析了TCP的序列号機制,為了確定資料的有序及安全,接收端會針對最後收到的連續有序的資料段發送ACK,告訴發送端自己期望下次收到的資料段的序列号。但仔細想想其實還可以進一步優化,提升傳輸效率。下圖中,丢失了序列号201的資料段。那麼下次期望對端發送的序列号就是201,直到收到序列号201的資料段。

為什麼說TCP是可靠的網絡傳輸協定?

在這種機制下,發送端可以積極悲觀的重傳所有的資料段,也可以樂觀的僅重傳丢失的資料段。但這兩種方式都有各自的問題,重傳所有資料段顯然會有大量重複的資料包發送,造成帶寬浪費,同時效率也不高。僅重傳丢失資料段在大量丢包的情況下由于需要依賴對端的ACK又會導緻傳輸效率低下。如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

假如發送端連續發送了三個資料段p1、p2、p3,序列号分别為s1, s2, s3,接收端先收到了s3,此時由于p1和p2還沒收到,是以給發送端ACK中期望的序列号還是s1。此時,發送端就有可能觸發p1、p2、p3的重傳,顯然p3的重傳是重複的。基于此,TCP協定引入了選擇性重傳算法,還是回到剛剛的場景,當接收端先收到p3的時候,在ACK中依然期望從s1開始重傳,但同時告訴對端s2-s3已經收到了,不用再重傳了。這就是SACK算法 。

SACK(TCP Selective Acknowledgement)算法

SACK算法可以參考RFC2018文檔,其原理是當出現失序資料包時在ACK封包中同時給出已經收到的失序的資料段。可以通過設定Option選項來激活SACK算法。

  1. Option kind = 4 表示支援SACK選擇性确認中間封包段功能
  2. Option kind = 5 表示确認封包段,選擇性确認視窗中間的Segments封包段

通常SACK算法都是配合選擇性确認算法一起使用,如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

上圖中,當序列号201的資料段丢了之後,在收到下一個資料段(序列号361)之後,回給發送端的ACK除了期望的序列号201還帶上了一個SACK[361, 500],告訴發送端這個範圍的資料段已經收到本地網絡協定棧已經在處理了,可以先不傳。最後,發送端隻發送了序列号201的資料段,當接收端本地網絡協定棧處理完序列号361的資料之後又會發送一個ACK告訴發送端期望下個收到的資料包序列号為501。

上面還有一種情況,假如接收端本地網絡協定棧對361序列号的資料段處理失敗了,那麼下次的ACK裡期望的序列号就是361。

下面是激活SACK算法之後的資料包,可以看到,Options Kind 為5,選擇性重傳的資料段分别是74031-78111、59071-72671、45599-58121三個範圍内的資料段。

為什麼說TCP是可靠的網絡傳輸協定?

從丢包到測量驅動的擁塞控制算法

前面我們說擁塞控制是從整個網絡鍊路的宏觀角度去考慮的,這裡面要考慮參與網絡傳輸的所有中間裝置,而網絡傳輸效率的上限其實也取決于整個網絡鍊路中效率最差的那個裝置。

當發送端和接收端的收發效率都很高的情況下,假如中間裝置效率低下,就會發生丢包,産生丢包的原因可能是中間裝置記憶體不足也有可能是因為負載過高導緻的逾時。如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

實際上,目前的擁塞控制基本上還是基于丢包來實作的。但是簡單通過丢包來觸發擁塞控制會産生一些問題。這是因為擁塞控制會觸發慢啟動,當達到上限之後又會觸發擁塞控制進而觸發慢啟動,不斷重複這個過程,這就是傳統的基于丢包的擁塞控制算法,很明顯這會影響傳輸的效率。如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

你可能會說,我們固定一個大小不就可以避免慢啟動了嗎?但仔細想想是有問題的。比如,當某一時刻擁塞視窗縮小到了20,但是過了一會網絡環境變好了,整個鍊路中可以發送的最大資料段變成了1000。此時,如果還一直使用20的擁塞視窗顯然是不合理的。

在上面的基礎上,CUBIC算法在上面的基礎上做了優化,可以讓每次觸發擁塞控制的慢啟動過程中發送資料段的大小變動的範圍更小,下圖中紅色線條是使用CUBIC擁塞控制算法的效果。

為什麼說TCP是可靠的網絡傳輸協定?

上面提到的基于丢包的擁塞控制會有一些問題,由于沒法提前預判控制點,會出現大量的丢包,而且随着記憶體越來越便宜,各個中間裝置的記憶體容量越來越大,延時也越來越長。

那麼,怎麼樣才能找到一個合适的控制點呢?我們看下面這張圖:

為什麼說TCP是可靠的網絡傳輸協定?

圖中,上面圖中的Y軸是RTT,下面圖中的Y軸是帶寬,當帶寬增長到一定程度的時候,網絡協定棧的緩沖區開始堆積,此時RTT是無法進行準确測量的。顯然,上圖中的最佳控制點應該是在最大帶寬、最小延時和最低丢包率。但這個控制點由于RTT和帶寬沒法在同一時間準确測量,是以要找到它并不容易。這裡,你可以想一下,為啥RTT和帶寬不能在同一時刻準确測量呢?

從網絡協定棧緩沖區的角度來看,什麼情況下傳輸效率最高呢?我們先看一張圖,如下:

為什麼說TCP是可靠的網絡傳輸協定?

圖中,State1可以認為緩沖區是空的,有資料過來直接就可以處理,不會産生堆積。State2中緩沖區開始堆積,資料得不到及時的處理,是以會出現延時。State3中緩沖區填滿了,如果繼續發送資料就會出現丢包。是以,效率最高的情況應該是每次發送的資料剛好可以被處理掉也就是上圖中State1的情況。

B B R擁塞控制算法

Google在2016年釋出了B B R(TCP Bottleneck Bandwidth and Round-trip propagation time)擁塞控制算法,并在Linux4.9核心引入,最新的QUIC協定也支援B B R算法。下圖中綠色線條是使用B B R之後的效果。

為什麼說TCP是可靠的網絡傳輸協定?

引入B B R之後的傳輸效率有了非常大的提升,如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

上面,我們說RTT和帶寬在同一時刻要準确測量是很困難的,比如RTT變高,但帶寬不變的情況下,到底是某個裝置負載變高了?還是中間鍊路發生了變化?這是很難搞清楚的。而B B R的核心就是為了找到上面我們提到的那個最佳控制點。

首先,要解決的一個問題就是排除掉RTT裡的噪聲,比如ACK的延遲确認以及網絡裝置緩沖區都會産生RTT噪聲。去除RTT噪聲之後得到的應該是一個實體屬性,也就是經過各個實體裝置的時間總和,中間不包括由于TCP協定棧的程式所産生的時延,比如ACK延遲确認。這個實體屬性叫作RTprop。如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

那如何測量出RTprop呢?下面是計算RTprop的公式:

為什麼說TCP是可靠的網絡傳輸協定?
為什麼說TCP是可靠的網絡傳輸協定?

這個公式看起來挺唬人,實際上它做的事情很簡單,通過不斷測量得到一個平均的噪聲值,最終得到一個近似的RTprop的值。

有了RTprop之後剩下的就是測量帶寬BtlBw了,公式如下:

為什麼說TCP是可靠的網絡傳輸協定?

原理也是類似,反複取多次發送速率,取最大值。

上面是對同一個鍊路進行計算,但網絡傳輸的鍊路時刻都在發生變化,當傳輸鍊路發生變化時,B B R算法會基于pacing_gain調整,pacing_gain的原理是周期性的增加并減少傳輸速率,進而使原來的RTprop和RtlBw失效并重新計算RTprop和RTlBw,如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

下面我們來看一下,當線路發生變化的時候,pacing_gain是如何重新計算得到最佳控制點的。如下圖:

為什麼說TCP是可靠的網絡傳輸協定?

上圖中,最上面表示在鍊路發生變化之後速度變大的情況,在20秒的時候速率提升到了20M,此時RTT并沒變大,是以繼續提升速率,直到21秒多的時候RTT增大,此時就可以計算得到線路發生變化之後的最佳控制點。

再來看圖中下面的部分,表示的是鍊路發生變化後,速率變小的情況。在40秒的時候開始增加發送速率,但是RTT也随之變大,根據pacing_gain的規則增加速率之後又會降低速率,是以在42秒的時候又降到了20M的速率直到達到最佳控制點。這就是pacing_gain的運作原理。

下面通過代碼來直覺感受一下B B R算法的實作。根據B B R的原理不難推斷出,B B R算法是重度依賴ACK的,通過ACK攜帶的資訊來計算最佳的控制點。下面是B B R算法處理ACK的過程的代碼實作。

function onAck(packet)
  // 計算rtt
  rtt = now - packet.sendtime
  // 找到最小的rtt更新到RTprop
  update_min_filter(RTpropFilter, rtt)
  delivered += packet.size
  delivered_time = now
  // 計算速率
  deliveredRate = (delivered-packet.delivered)/(delivered_time-packet.delivered_time)
  if (deliveryRate > BtlBwFilter.currentMax || !packet.app_limited)
    // 更新BtlBw
    update_max_filter(BtlBwFilter, deliveryRate)
  if (app_limited_until > 0) 
    app_limited_until = app_limited_until - packet.size           

B B R在發送資料時的處理過程代碼如下:

function send(packet)
  bdp = BtlBwFilter.currentMax * RTpropFilter.currentMin
  // 如果發送速率超出了發送速率就等一等再發送
  if (inflight >= cwnd_gain * bdp)
    return
  if (now >= nextSendTime)
    packet = nextPacketToSend()
    if (!packate) 
      app_limited_until = inflight
      return
    packet.app_limited = (app_limited_until > 0)
    packet.sendtime = now
    packet.delivered = delivered
    packet.delivered_time = delivered_time
    ship(packet)
    // pacing_gain周期性的探測
    nextSendTime = now + packet.size / (pacing_gain * BtlBwFilter.currentMax)
  timerCallbackAt(send, nextSendTime)           

pacing_gain探測的速率增長規律是5/4, 3/4, 1,1,1,1,1,1,也就是1.25、0.75、1、1、1、1、1、1。

關于B B R更詳細的内容可以參考:https://dl.acm.org/doi/pdf/10.1145/3012426.3022184

到這裡,TCP是如何保證它的可靠性就講完了,其中涉及到非常多的實際細節,如果有什麼纰漏還希望大家可以幫忙指出來。