TCP 擁塞控制演算法
最近花了些時間在學習TCP/IP協議上,首要原因是由於本人長期以來對TCP/IP的認識就只限於三次握手四次分手上,所以希望深入瞭解一下。再者,TCP/IP和Linux系統層級的很多設計都可以用於中介軟體系統架構上,比如說TCP 擁塞控制演算法也可以用於以響應時間來限流的中介軟體。更深一層,像TCP/IP協議這種基礎知識和原理性的技術,都是經過長時間的考驗的,都是前人智慧的結晶,可以給大家很多啟示和幫助。
本文中會出現一些縮寫,因為篇幅問題,無法每個都進行解釋,如果你不明白它的含義,請自己去搜索瞭解,做一個主動尋求知識的人。
TCP協議有兩個比較重要的控制演算法,一個是流量控制,另一個就是阻塞控制。
TCP協議通過滑動視窗來進行流量控制,它是控制傳送方的傳送速度從而使接受者來得及接收並處理。而擁塞控制是作用於網路,它是防止過多的包被髮送到網路中,避免出現網路負載過大,網路擁塞的情況。
擁塞演算法需要掌握其狀態機和四種演算法。擁塞控制狀態機的狀態有五種,分別是Open,Disorder,CWR,Recovery和Loss狀態。四個演算法為慢啟動,擁塞避免,擁塞發生時演算法和快速恢復。
Congestion Control State Machine
和TCP一樣,擁塞控制演算法也有其狀態機。當傳送方收到一個Ack時,Linux TCP通過狀態機(state)來決定其接下來的行為,是應該降低擁塞視窗cwnd大小,或者保持cwnd不變,還是繼續增加cwnd。如果處理不當,可能會導致丟包或者超時。

狀態機示意圖
1 Open狀態
Open狀態是擁塞控制狀態機的預設狀態。這種狀態下,當ACK到達時,傳送方根據擁塞視窗cwnd(Congestion Window)是小於還是大於慢啟動閾值ssthresh(slow start threshold),來按照慢啟動或者擁塞避免演算法來調整擁塞視窗。
2 Disorder狀態
當傳送方檢測到DACK(重複確認)或者SACK(選擇性確認)時,狀態機將轉變為Disorder狀態。在此狀態下,傳送方遵循飛行(in-flight)包守恆原則,即一個新包只有在一個老包離開網路後才傳送,也就是傳送方收到老包的ACK後,才會再發送一個新包。
3 CWR狀態
傳送方接收到一個擁塞通知時,並不會立刻減少擁塞視窗cwnd,而是每收到兩個ACK就減少一個段,直到視窗的大小減半為止。當cwnd正在減小並且網路中有沒有重傳包時,這個狀態就叫CWR(Congestion Window Reduced,擁塞視窗減少)狀態。CWR狀態可以轉變成Recovery或者Loss狀態。
4 Recovery狀態
當傳送方接收到足夠(推薦為三個)的DACK(重複確認)後,進入該狀態。在該狀態下,擁塞視窗cnwd每收到兩個ACK就減少一個段(segment),直到cwnd等於慢啟動閾值ssthresh,也就是剛進入Recover狀態時cwnd的一半大小。
傳送方保持 Recovery 狀態直到所有進入 Recovery狀態時正在傳送的資料段都成功地被確認,然後傳送方恢復成Open狀態,重傳超時有可能中斷 Recovery 狀態,進入Loss狀態。
5 Loss狀態
當一個RTO(重傳超時時間)到期後,傳送方進入Loss狀態。所有正在傳送的資料標記為丟失,擁塞視窗cwnd設定為一個段(segment),傳送方再次以慢啟動演算法增大擁塞視窗cwnd。
Loss 和 Recovery 狀態的區別是:Loss狀態下,擁塞視窗在傳送方設定為一個段後增大,而 Recovery 狀態下,擁塞視窗只能被減小。Loss 狀態不能被其他的狀態中斷,因此,傳送方只有在所有 Loss 開始時正在傳輸的資料都得到成功確認後,才能退到 Open 狀態。
四大演算法
擁塞控制主要是四個演算法:1)慢啟動,2)擁塞避免,3)擁塞發生,4)快速恢復。這四個演算法不是一天都搞出來的,這個四演算法的發展經歷了很多時間,到今天都還在優化中。

示意圖
慢熱啟動演算法 – Slow Start
所謂慢啟動,也就是TCP連線剛建立,一點一點地提速,試探一下網路的承受能力,以免直接擾亂了網路通道的秩序。
慢啟動演算法:
- 連線建好的開始先初始化擁塞視窗cwnd大小為1,表明可以傳一個MSS大小的資料。
- 每當收到一個ACK,cwnd大小加一,呈線性上升。
- 每當過了一個往返延遲時間RTT(Round-Trip Time),cwnd大小直接翻倍,乘以2,呈指數讓升。
- 還有一個ssthresh(slow start threshold),是一個上限,當cwnd >= ssthresh時,就會進入“擁塞避免演算法”(後面會說這個演算法)
擁塞避免演算法 – Congestion Avoidance
如同前邊說的,當擁塞視窗大小cwnd大於等於慢啟動閾值ssthresh後,就進入擁塞避免演算法。演算法如下:
- 收到一個ACK,則cwnd = cwnd + 1 / cwnd
- 每當過了一個往返延遲時間RTT,cwnd大小加一。
過了慢啟動閾值後,擁塞避免演算法可以避免視窗增長過快導致視窗擁塞,而是緩慢的增加調整到網路的最佳值。
擁塞狀態時的演算法
一般來說,TCP擁塞控制預設認為網路丟包是由於網路擁塞導致的,所以一般的TCP擁塞控制演算法以丟包為網路進入擁塞狀態的訊號。對於丟包有兩種判定方式,一種是超時重傳RTO[Retransmission Timeout]超時,另一個是收到三個重複確認ACK。
超時重傳是TCP協議保證資料可靠性的一個重要機制,其原理是在傳送一個數據以後就開啟一個計時器,在一定時間內如果沒有得到傳送資料報的ACK報文,那麼就重新發送資料,直到傳送成功為止。
但是如果傳送端接收到3個以上的重複ACK,TCP就意識到資料發生丟失,需要重傳。這個機制不需要等到重傳定時器超時,所以叫
做快速重傳,而快速重傳後沒有使用慢啟動演算法,而是擁塞避免演算法,所以這又叫做快速恢復演算法。
超時重傳RTO[Retransmission Timeout]超時,TCP會重傳資料包。TCP認為這種情況比較糟糕,反應也比較強烈:
- 由於發生丟包,將慢啟動閾值ssthresh設定為當前cwnd的一半,即ssthresh = cwnd / 2.
- cwnd重置為1
- 進入慢啟動過程
最為早期的TCP Tahoe演算法就只使用上述處理辦法,但是由於一丟包就一切重來,導致cwnd又重置為1,十分不利於網路資料的穩定傳遞。
所以,TCP Reno演算法進行了優化。當收到三個重複確認ACK時,TCP開啟快速重傳Fast Retransmit演算法,而不用等到RTO超時再進行重傳:
- cwnd大小縮小為當前的一半
- ssthresh設定為縮小後的cwnd大小
- 然後進入快速恢復演算法Fast Recovery。

cwnd曲線示意圖
快速恢復演算法 – Fast Recovery
TCP Tahoe是早期的演算法,所以沒有快速恢復演算法,而Reno演算法有。在進入快速恢復之前,cwnd和ssthresh已經被更改為原有cwnd的一半。快速恢復演算法的邏輯如下:
- cwnd = cwnd + 3 * MSS,加3 * MSS的原因是因為收到3個重複的ACK。
- 重傳DACKs指定的資料包。
- 如果再收到DACKs,那麼cwnd大小增加一。
- 如果收到新的ACK,表明重傳的包成功了,那麼退出快速恢復演算法。將cwnd設定為ssthresh,然後進入擁塞避免演算法。

快速重傳示意圖
如圖所示,第五個包發生了丟失,所以導致接收方接收到三次重複ACK,也就是ACK5。所以將ssthresh設定噹噹時cwnd的一半,也就是6/2 = 3,cwnd設定為3 + 3 = 6。然後重傳第五個包。當收到新的ACK時,也就是ACK11,則退出快速恢復階段,將cwnd重新設定為當前的ssthresh,也就是3,然後進入擁塞避免演算法階段。
後記
本文為大家大致描述了TCP擁塞控制的一些機制,但是這些擁塞控制還是有很多缺陷和待優化的地方,業界也在不斷推出新的擁塞控制演算法,比如說谷歌的BBR。這些我們後續也會繼續探討,請大家繼續關注。
