TCP要點有四,一曰有連線,二曰可靠傳輸,三曰資料按照到達,四曰端到端流量控制。注意,TCP被設計時只保證這四點,此時它雖然也有些問題,然而很簡單,然而更大的問題很快呈現出來,使之不得不考慮和IP網路相關的東西,比如公平性,效率,因此增加了擁塞控制,這樣TCP就成了現在這個樣子。
為什麼要進行擁塞控制
要回答這個問題,首先必須知道什麼時候TCP
會出現擁塞。TCP
作為一個端到端的傳輸層協議,它並不關心連線雙方在物理鏈路上會經過多少路由器交換機以及報文傳輸的路徑和下一條,這是IP
層該考慮的事。然而,在現實網路應用中,TCP
連線的兩端可能相隔千山萬水,報文也需要由多個路由器交換機進行轉發。交換裝置的效能不是無限的!, 當多個入介面的報文都要從相同的出介面轉發時,如果出介面轉發速率達到極限,報文就會開始在交換裝置的入介面快取佇列堆積。但這個佇列長度也是有限的,當佇列塞滿後,後續輸入的報文就只能被丟棄掉了。對於TCP
的傳送端來說,看到的就是傳送超時丟包了。
網路資源是各個連線共享的,為了大家都能完成資料傳輸。所以,TCP
需要當它感知到傳輸發生擁塞時,需要降低自己的傳送速率,等待擁塞解除。
如何進行擁塞控制
擁塞視窗 cwnd
首先需要明確的是,TCP
是在傳送端
進行擁塞控制的。TCP為每條連線準備了一個記錄擁塞視窗大小的變數cwnd
1
,它限制了本端TCP
可以傳送到網路中的最大報文數量2
。顯然,這個值越大,連線的吞吐量越高,但也更容易導致網路擁塞。所以,TCP的擁塞控制本質上就是根據丟包情況調整cwnd
,使得傳輸的吞吐率儘可能地大!而不同的擁塞控制演算法就是調整cwnd
的方式不同!
注1
: 本文中的cwnd
以傳送端的最大報文段長度SMSS
為單位的
注2
: 這個數量也受對端通告的視窗大小限制
Linux 使用者可以使用 ss --tcp --info 檢視連結的cwnd
值
擁塞控制演算法
TCP從誕生至今,已經有了多種的擁塞控制演算法,直到現在還有新的在被提出!其中TCP Tahoe
(1988)和TCP Reno
(1990)是最初的兩個演算法。雖然看上去年代很就遠了,但 Reno
演算法直到現在還在廣泛地使用。
-
Tahoe
提出了1)慢啟動,2)擁塞避免,3)快速重傳 -
Reno
在Tahoe
的基礎上增加了4)快速恢復
Tahoe
演算法的基本思想是
- 首選設定一個符合情理的初始視窗值
- 當沒有出現丟包時,慢慢地增加視窗大小,逐漸逼近吞吐量的上界
- 當出現丟包時,快速地減小視窗大小,等待阻塞消除
Tahoe 擁塞避免 (congestion-avoidance)
當我們在理解擁塞控制演算法時,可以假想傳送端是一下子將整個擁塞視窗大小的報文傳送出去,然後等待回應。
Tahoe
採用的是加性增乘性減(Additive Increase, Multiplicative Decrease, AIMD)方式來完成緩慢增加和快速減小擁塞視窗:
傳送端傳送整窗
的資料:
- 如果沒有丟包,則
cwnd = cwnd + 1
- 如果出現丟包了,則
cwnd = cwnd / 2
為什麼丟包後是除以2
呢, 這裡的2
實際上是一個折中值!用下面的例子來解釋!
物理傳輸路徑都會有延時,這個延時也讓傳輸鏈路有了傳輸容量(transit capacity
)這樣一個概念,同時我們把交換裝置的佇列快取稱為佇列容量(queue capacity
).比如下面這樣一個連線,傳輸容量是M
,佇列容量是N
.
當cwnd
小於M
時,不會使用R
的佇列,此時不會有擁塞發生;當cwnd
繼續增大時,開始使用R
的佇列,此時實際上已經有阻塞了!但是A
感知不到,因為沒有丟包! 當cwnd
繼續增大到M + N
時,如果再增大cwnd
,就會出現丟包。此時擁塞控制演算法需要減小cwnd
,那麼減小到多少合適呢? 當然是減少到不使用R
的快取,或者說使得cwnd = M
,這樣可以快速解除阻塞。
這是理想的情況! 實際情況是A
並不知道M
和N
有多大(或者說有什麼關係),它只知道當cwnd
超過M + N
時會出丟包!於是我們折中地假定M = N
,所以當cwnd = 2N
時是丟包的臨界點,為了解除阻塞,讓cwnd = cwnd / 2 = N
就可以解除阻塞3
!
所以,cwnd
的變化趨勢就像上面這樣,圖中上方的紅色曲線表示出現丟包。這樣的穩定狀態也稱為擁塞避免階段
(congestion-avoidance phase
)
現實演算法實現中,擁塞避免階段的cwnd
是在收到每個ACK時更新的:cwnd += 1/cwnd
,如果認真算,會發現這比整窗更新cwnd += 1
要稍微少一點!
注3
:後面會提到,Tahoe
在此時會將cwnd
先設定為1
,然後再迅速恢復到cwnd / 2
Tahoe 慢啟動 (Slow Start)
Tahoe
需要為選定一個cwnd
初始值,但是傳送端並不知道多大的cwnd
才合適。所以只能從1開始4
,如果這個時候就開始加性增,那就太慢了,比如假設一個連線會傳送5050個MSS
大小的報文,按照加性增加,需要100個RTT
才能傳輸完成(1+2+3+...+100=5050)。因此,Tahoe
和Reno
使用一種稱為慢啟動的演算法迅速提高cwnd
。也就是隻要沒有丟包,每傳送一個整窗的資料,cwnd = 2 X cwnd
。換句話說,在慢啟動階段
(slow-start phase
),當傳送端每收到一個ACK
時,就讓cwnd = cwnd + 1
注4
RFC 2581 已經允許cwnd
的初始值最大為2, RFC 3390 已經允許cwnd
的初始值最大為4, RFC 6928已經允許cwnd
的初始值最大為10
那麼,慢啟動階段何時停止?或者說什麼時候進入前面的擁塞避免階段 ? Tahoe
演算法定義了一個慢啟動閾值
(slow-start threshold
)變數,在cwnd < ssthresh
時,TCP
處於慢啟動階段,在cwnd > ssthresh
後,TCP
處於擁塞避免階段。
ssthreshold
的初始值一個非常大的值。連線建立後cwnd
以指數增加,直到出現丟包後, 慢啟動閾值將被設定為 cwnd / 2
。同時cwnd
被設定為1
,重新開始慢啟動過程。這個過程如下圖所示, 可以看到,慢啟動可是一點也不慢。
Tahoe 快速重傳 (Fast Retransmit)
現實的網路網路環境拓撲可能十分複雜,即使是同一個TCP
連線的報文,也有可能由於諸如等價路由等因素被路由器轉發到不同的路徑,於是,在接收端就可能出現報文的亂序到達,甚至丟包!舉個例子,傳送端傳送了資料DATA[1]、DATA[2]、……、DATA[8],但由於某些因素,DATA[2]在傳輸過程中被丟了,接收端只收到另外7個報文,它會連續回覆多次 ACK[1](請求傳送端傳送DATA[2])。這個時候,傳送端還需要等待DATA[2]的回覆超時(2個RTT)嗎?
快速重傳的策略是,不等了!擋傳送端收到第3個重複的ACK[1]時(也就是第4個ACK[1]),它要馬上重傳DATA[2],然後進入慢啟動階段,設定ssthresh = cwnd / 2
, cwnd = 1
.
如上圖所示,其中cwnd
的初始值為8,當傳送端收到第3個重複的ACK[1]時,迅速進入慢啟動階段,之後當再收到ACK[1]時,由於cwnd = 1
只有1,因此並不會傳送新的報文
Reno 快速恢復(Fast Recovery)
在快速重傳中,當出現報文亂序丟包後,擁塞視窗cwnd
變為1,由於該限制,在丟失的資料包被應答之前,沒有辦法傳送新的資料包。這樣大大降低了網路的吞吐量。針對這個問題,TCP Reno
在TCP Tahoe
的基礎上增加了快速恢復(Fast Recovery
)。
快速恢復的策略是當收到第3個重複的ACK後,快速重傳丟失的包,然後
- 設定
sshthresh = cwnd / 2
- 設定
cwnd = cwnd /2
還是以上面的例子為例
與快速重傳中不同的是,傳送端在收到第3個重複的ACK後,cwnd
變為5,EFS
設定為7
這裡EFS
表示傳送端認為的正在向對端傳送的包(Estimated FlightSize),或者說正在鏈路上(in flight)的包。一般情況下,EFS
是與cwnd
相等的。但在快速恢復的時候,就不同了。假設擁塞避免階段時cwnd = EFS = N
,在啟動快速恢復時,收到了3個重複的ACK,注意,這3個ACK是不會佔用網路資源的(因為它們已經被對端收到了),所以EFS = N - 3
,而既然是出發了快速恢復,那麼一定是有一個包沒有到達,所以EFS = N - 4
,然後,本端會快速重傳一個報文,EFS = N - 3
,這就是上面EFS
設定為7的來源。
其他部分沒什麼好說的,傳送端會在EFS < cwnd
時傳送信的資料,而同時,這又會使得EFS = cwnd
TCP New Reno
根據Reno
的描述,TCP
傳送端會在收到3個重複的ACK時進行快速重傳和快速恢復,但還有有一個問題,重複的ACK背後可能不僅僅是一個包丟了!如果是多個包丟了,即使傳送端快速重傳了丟失的第一個包,進入快速恢復,那麼後面也會收到接收端發出的多個請求其他丟失的包的重複ACK!這個時候?傳送端需要再累計到3個重複的ACK才能重傳!
問題出在哪裡?傳送端不能重收到的重複ACK中獲得更多的丟包資訊!它只知道第一個被丟棄的報文,後面還有多少被丟棄了?完全不知道!也許使用SACK
(參考RFC6675)這就不是問題,但這需要兩端都支援SACK
。對於不支援SACK
的場景,TCP
需要更靈活!
RFC6582中描述的New Reno
演算法,在Reno
中的基礎上,引入了一個新的變數recover
,當進入快速恢復狀態時(收到3個重複的ACK[a]),將recover
設定為已經傳送的最後的報文的序號。如果之後收到的新的ACK[b]序號b不超過recover
,就說明這還是一個丟包引起的ACK !這種ACK在標準中也稱之為部分應答
(partial acknowledgments
), 這時傳送端就不等了,立即重傳丟失的報文。
還有後文!