TCP要點(diǎn)有四,一曰有連接,二曰可靠傳輸,三曰數(shù)據(jù)按照到達(dá),四曰端到端流量控制。注意,TCP被設(shè)計(jì)時(shí)只保證這四點(diǎn),此時(shí)它雖然也有些問(wèn)題,然而很簡(jiǎn)單,然而更大的問(wèn)題很快呈現(xiàn)出來(lái),使之不得不考慮和IP網(wǎng)絡(luò)相關(guān)的東西,比如公平性,效率,因此增加了擁塞控制,這樣TCP就成了現(xiàn)在這個(gè)樣子。
為什么要進(jìn)行擁塞控制
要回答這個(gè)問(wèn)題,首先必須知道什么時(shí)候TCP會(huì)出現(xiàn)擁塞。TCP作為一個(gè)端到端的傳輸層協(xié)議,它并不關(guān)心連接雙方在物理鏈路上會(huì)經(jīng)過(guò)多少路由器交換機(jī)以及報(bào)文傳輸?shù)穆窂胶拖乱粭l,這是IP層該考慮的事。然而,在現(xiàn)實(shí)網(wǎng)絡(luò)應(yīng)用中,TCP連接的兩端可能相隔千山萬(wàn)水,報(bào)文也需要由多個(gè)路由器交換機(jī)進(jìn)行轉(zhuǎn)發(fā)。交換設(shè)備的性能不是無(wú)限的!, 當(dāng)多個(gè)入接口的報(bào)文都要從相同的出接口轉(zhuǎn)發(fā)時(shí),如果出接口轉(zhuǎn)發(fā)速率達(dá)到極限,報(bào)文就會(huì)開(kāi)始在交換設(shè)備的入接口緩存隊(duì)列堆積。但這個(gè)隊(duì)列長(zhǎng)度也是有限的,當(dāng)隊(duì)列塞滿后,后續(xù)輸入的報(bào)文就只能被丟棄掉了。對(duì)于TCP的發(fā)送端來(lái)說(shuō),看到的就是發(fā)送超時(shí)丟包了。
如何進(jìn)行擁塞控制
擁塞窗口 cwnd
首先需要明確的是,TCP是在發(fā)送端進(jìn)行擁塞控制的。TCP為每條連接準(zhǔn)備了一個(gè)記錄擁塞窗口大小的變量cwnd1,它限制了本端TCP可以發(fā)送到網(wǎng)絡(luò)中的最大報(bào)文數(shù)量2。顯然,這個(gè)值越大,連接的吞吐量越高,但也更容易導(dǎo)致網(wǎng)絡(luò)擁塞。所以,TCP的擁塞控制本質(zhì)上就是根據(jù)丟包情況調(diào)整cwnd,使得傳輸?shù)耐掏侣时M可能地大!而不同的擁塞控制算法就是調(diào)整cwnd的方式不同!
注1: 本文中的cwnd以發(fā)送端的最大報(bào)文段長(zhǎng)度SMSS為單位的 注2: 這個(gè)數(shù)量也受對(duì)端通告的窗口大小限制
linux 用戶可以使用 ss --tcp --info 查看鏈接的cwnd值
擁塞控制算法
TCP從誕生至今,已經(jīng)有了多種的擁塞控制算法,直到現(xiàn)在還有新的在被提出!其中TCP Tahoe(1988)和TCP Reno(1990)是最初的兩個(gè)算法。雖然看上去年代很就遠(yuǎn)了,但 Reno算法直到現(xiàn)在還在廣泛地使用。
- Tahoe 提出了1)慢啟動(dòng),2)擁塞避免,3)快速重傳
- Reno 在Tahoe的基礎(chǔ)上增加了4)快速恢復(fù)
Tahoe算法的基本思想是
- 首選設(shè)置一個(gè)符合情理的初始窗口值
- 當(dāng)沒(méi)有出現(xiàn)丟包時(shí),慢慢地增加窗口大小,逐漸逼近吞吐量的上界
- 當(dāng)出現(xiàn)丟包時(shí),快速地減小窗口大小,等待阻塞消除
相關(guān)視頻推薦
LinuxC++零拷貝的實(shí)現(xiàn) 用戶態(tài)協(xié)議棧 ntytcp
90分鐘搞定tcp/ip協(xié)議棧
LinuxC++后臺(tái)服務(wù)器開(kāi)發(fā)架構(gòu)師免費(fèi)學(xué)習(xí)地址:C/C++Linux服務(wù)器開(kāi)發(fā)/后臺(tái)架構(gòu)師【零聲教育】-學(xué)習(xí)視頻教程-騰訊課堂
【文章福利】:小編整理了一些個(gè)人覺(jué)得比較好的學(xué)習(xí)書(shū)籍、視頻資料共享在qun文件里面,有需要的可以自行添加哦!(需要視頻資料后臺(tái)私信“1”自取)
Tahoe 擁塞避免 (congestion-avoidance)
當(dāng)我們?cè)诶斫鈸砣刂扑惴〞r(shí),可以假想發(fā)送端是一下子將整個(gè)擁塞窗口大小的報(bào)文發(fā)送出去,然后等待回應(yīng)。
Tahoe采用的是加性增乘性減(Additive Increase, Multiplicative Decrease, AIMD)方式來(lái)完成緩慢增加和快速減小擁塞窗口:
發(fā)送端發(fā)送整窗的數(shù)據(jù):
- 如果沒(méi)有丟包,則 cwnd = cwnd + 1
- 如果出現(xiàn)丟包了,則 cwnd = cwnd / 2
為什么丟包后是除以2呢, 這里的2實(shí)際上是一個(gè)折中值!用下面的例子來(lái)解釋?zhuān)?/p>
?
物理傳輸路徑都會(huì)有延時(shí),這個(gè)延時(shí)也讓傳輸鏈路有了傳輸容量(transit capacity)這樣一個(gè)概念,同時(shí)我們把交換設(shè)備的隊(duì)列緩存稱(chēng)為隊(duì)列容量(queue capacity).比如下面這樣一個(gè)連接,傳輸容量是M,隊(duì)列容量是N.
當(dāng)cwnd小于M時(shí),不會(huì)使用R的隊(duì)列,此時(shí)不會(huì)有擁塞發(fā)生;當(dāng)cwnd繼續(xù)增大時(shí),開(kāi)始使用R的隊(duì)列,此時(shí)實(shí)際上已經(jīng)有阻塞了!但是A感知不到,因?yàn)闆](méi)有丟包! 當(dāng)cwnd繼續(xù)增大到M + N時(shí),如果再增大cwnd,就會(huì)出現(xiàn)丟包。此時(shí)擁塞控制算法需要減小cwnd,那么減小到多少合適呢? 當(dāng)然是減少到不使用R的緩存,或者說(shuō)使得cwnd = M,這樣可以快速解除阻塞。
這是理想的情況! 實(shí)際情況是A并不知道M和N有多大(或者說(shuō)有什么關(guān)系),它只知道當(dāng)cwnd超過(guò)M + N時(shí)會(huì)出丟包!于是我們折中地假定M = N,所以當(dāng)cwnd = 2N時(shí)是丟包的臨界點(diǎn),為了解除阻塞,讓cwnd = cwnd / 2 = N就可以解除阻塞3!
所以,cwnd的變化趨勢(shì)就像上面這樣,圖中上方的紅色曲線表示出現(xiàn)丟包。這樣的穩(wěn)定狀態(tài)也稱(chēng)為擁塞避免階段(congestion-avoidance phase)
現(xiàn)實(shí)算法實(shí)現(xiàn)中,擁塞避免階段的cwnd是在收到每個(gè)ACK時(shí)更新的:cwnd += 1/cwnd,如果認(rèn)真算,會(huì)發(fā)現(xiàn)這比整窗更新cwnd += 1要稍微少一點(diǎn)!
注3:后面會(huì)提到,Tahoe在此時(shí)會(huì)將cwnd先設(shè)置為1,然后再迅速恢復(fù)到cwnd / 2
Tahoe 慢啟動(dòng) (Slow Start)
Tahoe需要為選定一個(gè)cwnd初始值,但是發(fā)送端并不知道多大的cwnd才合適。所以只能從1開(kāi)始4,如果這個(gè)時(shí)候就開(kāi)始加性增,那就太慢了,比如假設(shè)一個(gè)連接會(huì)發(fā)送5050個(gè)MSS大小的報(bào)文,按照加性增加,需要100個(gè)RTT才能傳輸完成(1+2+3+...+100=5050)。因此,Tahoe和Reno使用一種稱(chēng)為慢啟動(dòng)的算法迅速提高cwnd。也就是只要沒(méi)有丟包,每發(fā)送一個(gè)整窗的數(shù)據(jù),cwnd = 2 X cwnd。換句話說(shuō),在慢啟動(dòng)階段(slow-start phase),當(dāng)發(fā)送端每收到一個(gè)ACK時(shí),就讓cwnd = cwnd + 1
?
注4RFC 2581 已經(jīng)允許cwnd的初始值最大為2, RFC 3390 已經(jīng)允許cwnd的初始值最大為4, RFC 6928已經(jīng)允許cwnd的初始值最大為10
那么,慢啟動(dòng)階段何時(shí)停止?或者說(shuō)什么時(shí)候進(jìn)入前面的擁塞避免階段 ? Tahoe算法定義了一個(gè)慢啟動(dòng)閾值(slow-start threshold)變量,在cwnd < ssthresh時(shí),TCP處于慢啟動(dòng)階段,在cwnd > ssthresh后,TCP處于擁塞避免階段。
ssthreshold的初始值一個(gè)非常大的值。連接建立后cwnd以指數(shù)增加,直到出現(xiàn)丟包后, 慢啟動(dòng)閾值將被設(shè)置為 cwnd / 2。同時(shí)cwnd被設(shè)置為1,重新開(kāi)始慢啟動(dòng)過(guò)程。這個(gè)過(guò)程如下圖所示, 可以看到,慢啟動(dòng)可是一點(diǎn)也不慢。
?
Tahoe 快速重傳 (Fast Retransmit)
現(xiàn)實(shí)的網(wǎng)絡(luò)網(wǎng)絡(luò)環(huán)境拓?fù)淇赡苁謴?fù)雜,即使是同一個(gè)TCP連接的報(bào)文,也有可能由于諸如等價(jià)路由等因素被路由器轉(zhuǎn)發(fā)到不同的路徑,于是,在接收端就可能出現(xiàn)報(bào)文的亂序到達(dá),甚至丟包!舉個(gè)例子,發(fā)送端發(fā)送了數(shù)據(jù)DATA[1]、DATA[2]、……、DATA[8],但由于某些因素,DATA[2]在傳輸過(guò)程中被丟了,接收端只收到另外7個(gè)報(bào)文,它會(huì)連續(xù)回復(fù)多次 ACK[1](請(qǐng)求發(fā)送端發(fā)送DATA[2])。這個(gè)時(shí)候,發(fā)送端還需要等待DATA[2]的回復(fù)超時(shí)(2個(gè)RTT)嗎?
快速重傳的策略是,不等了!擋發(fā)送端收到第3個(gè)重復(fù)的ACK[1]時(shí)(也就是第4個(gè)ACK[1]),它要馬上重傳DATA[2],然后進(jìn)入慢啟動(dòng)階段,設(shè)置ssthresh = cwnd / 2 , cwnd = 1.
?
如上圖所示,其中cwnd的初始值為8,當(dāng)發(fā)送端收到第3個(gè)重復(fù)的ACK[1]時(shí),迅速進(jìn)入慢啟動(dòng)階段,之后當(dāng)再收到ACK[1]時(shí),由于cwnd = 1只有1,因此并不會(huì)發(fā)送新的報(bào)文
Reno 快速恢復(fù)(Fast Recovery)
在快速重傳中,當(dāng)出現(xiàn)報(bào)文亂序丟包后,擁塞窗口cwnd變?yōu)?,由于該限制,在丟失的數(shù)據(jù)包被應(yīng)答之前,沒(méi)有辦法發(fā)送新的數(shù)據(jù)包。這樣大大降低了網(wǎng)絡(luò)的吞吐量。針對(duì)這個(gè)問(wèn)題,TCP Reno在TCP Tahoe的基礎(chǔ)上增加了快速恢復(fù)(Fast Recovery)。
快速恢復(fù)的策略是當(dāng)收到第3個(gè)重復(fù)的ACK后,快速重傳丟失的包,然后
- 設(shè)置 sshthresh = cwnd / 2
- 設(shè)置 cwnd = cwnd /2
還是以上面的例子為例
?
與快速重傳中不同的是,發(fā)送端在收到第3個(gè)重復(fù)的ACK后,cwnd變?yōu)?,EFS設(shè)置為7
這里EFS表示發(fā)送端認(rèn)為的正在向?qū)Χ税l(fā)送的包(Estimated FlightSize),或者說(shuō)正在鏈路上(in flight)的包。一般情況下,EFS是與cwnd相等的。但在快速恢復(fù)的時(shí)候,就不同了。假設(shè)擁塞避免階段時(shí)cwnd = EFS = N,在啟動(dòng)快速恢復(fù)時(shí),收到了3個(gè)重復(fù)的ACK,注意,這3個(gè)ACK是不會(huì)占用網(wǎng)絡(luò)資源的(因?yàn)樗鼈円呀?jīng)被對(duì)端收到了),所以EFS = N - 3,而既然是出發(fā)了快速恢復(fù),那么一定是有一個(gè)包沒(méi)有到達(dá),所以EFS = N - 4,然后,本端會(huì)快速重傳一個(gè)報(bào)文,EFS = N - 3,這就是上面EFS設(shè)置為7的來(lái)源。
其他部分沒(méi)什么好說(shuō)的,發(fā)送端會(huì)在EFS < cwnd時(shí)發(fā)送信的數(shù)據(jù),而同時(shí),這又會(huì)使得EFS = cwnd
TCP New Reno
根據(jù)Reno的描述,TCP發(fā)送端會(huì)在收到3個(gè)重復(fù)的ACK時(shí)進(jìn)行快速重傳和快速恢復(fù),但還有有一個(gè)問(wèn)題,重復(fù)的ACK背后可能不僅僅是一個(gè)包丟了!如果是多個(gè)包丟了,即使發(fā)送端快速重傳了丟失的第一個(gè)包,進(jìn)入快速恢復(fù),那么后面也會(huì)收到接收端發(fā)出的多個(gè)請(qǐng)求其他丟失的包的重復(fù)ACK!這個(gè)時(shí)候?發(fā)送端需要再累計(jì)到3個(gè)重復(fù)的ACK才能重傳!
問(wèn)題出在哪里?發(fā)送端不能重收到的重復(fù)ACK中獲得更多的丟包信息!它只知道第一個(gè)被丟棄的報(bào)文,后面還有多少被丟棄了?完全不知道!也許使用SACK(參考RFC6675)這就不是問(wèn)題,但這需要兩端都支持SACK。對(duì)于不支持SACK的場(chǎng)景,TCP需要更靈活!
RFC6582中描述的New Reno算法,在Reno中的基礎(chǔ)上,引入了一個(gè)新的變量recover,當(dāng)進(jìn)入快速恢復(fù)狀態(tài)時(shí)(收到3個(gè)重復(fù)的ACK[a]),將recover設(shè)置為已經(jīng)發(fā)送的最后的報(bào)文的序號(hào)。如果之后收到的新的ACK[b]序號(hào)b不超過(guò)recover,就說(shuō)明這還是一個(gè)丟包引起的ACK !這種ACK在標(biāo)準(zhǔn)中也稱(chēng)之為部分應(yīng)答(partial acknowledgments), 這時(shí)發(fā)送端就不等了,立即重傳丟失的報(bào)文。
?
編輯
TCP在發(fā)送報(bào)文后,如果沒(méi)有收到對(duì)端應(yīng)答,那么在重傳定時(shí)器超時(shí)后會(huì)觸發(fā)重傳,超時(shí)時(shí)間遵循二進(jìn)制退避原則,也就是{1,2,4,8,16}這樣成倍地?cái)U(kuò)大超時(shí)時(shí)間。退避是因?yàn)門(mén)CP認(rèn)為丟包意味著網(wǎng)絡(luò)有擁塞,為了不加重網(wǎng)絡(luò)的擁塞,TCP選擇等待更長(zhǎng)的時(shí)間再進(jìn)行重傳。這和CSMA/CD中的二進(jìn)制退避算法如出一轍。
網(wǎng)絡(luò)中的網(wǎng)絡(luò)設(shè)備(路由器、交換機(jī))在收到了超過(guò)隊(duì)列限制的報(bào)文后,后續(xù)的報(bào)文會(huì)被丟棄。從TCP采用的二進(jìn)制退避算來(lái)看,TCP絕對(duì)算得上是網(wǎng)絡(luò)里的謙謙君子了,它信守的規(guī)則是:既然已經(jīng)堵了,我就等一會(huì)兒再發(fā),如果還堵,我就再多等翻倍的時(shí)間!
對(duì)整個(gè)網(wǎng)絡(luò)來(lái)說(shuō),這的確是減輕負(fù)擔(dān)的好辦法。要知道,在發(fā)送窗口已滿的情況下,指數(shù)退避一次,意味著單位時(shí)間內(nèi)發(fā)送的報(bào)文變成了原來(lái)的1/2,再退避一次,就只有原來(lái)的1/4!就像是汽車(chē)限號(hào)出行,單雙號(hào)限行不好用,我就規(guī)定一輛車(chē)只能在4天中開(kāi)1天...
But, TCP真的需要如此克制自己?jiǎn)幔浚瑩Q個(gè)說(shuō)法,為什么TCP一定要x2退避?難道重傳定時(shí)器超時(shí)時(shí)間不能線性增大(每次增加X(jué)秒),或者乘以一個(gè)更小的系數(shù)(比如x1.5) ? 我們可以從CSMA/CD找到靈感。
CSMA/CD使用x2的原因很好理解,共享介質(zhì)中的每個(gè)節(jié)點(diǎn)并不知道其他還有多少節(jié)點(diǎn),使用x2退避就是利用二分法快速找到讓整個(gè)系統(tǒng)穩(wěn)定運(yùn)行的時(shí)隙分配方案!
舉個(gè)例子,假設(shè)系統(tǒng)中有4個(gè)節(jié)點(diǎn)發(fā)包速率相同,那么最終的穩(wěn)定分配方案自然就是將一段時(shí)間分為4份,每個(gè)節(jié)點(diǎn)占用1個(gè)時(shí)隙。如果此時(shí)又加入4個(gè)相同的節(jié)點(diǎn)。那么顯然,所有的8個(gè)節(jié)點(diǎn)都會(huì)發(fā)包沖突。如何才能不沖突呢?當(dāng)然是分配給每個(gè)節(jié)點(diǎn)的時(shí)間再減小一半!當(dāng)然這里舉的例子節(jié)點(diǎn)都是2的整數(shù)冪。如果不是呢?此時(shí)2進(jìn)制退避依然能很快地達(dá)到穩(wěn)定,也許這個(gè)時(shí)候的時(shí)隙分配方案不是最合理的,但是正如前面說(shuō)的,每個(gè)節(jié)點(diǎn)并不知道其他還有多少節(jié)點(diǎn),對(duì)單個(gè)節(jié)點(diǎn)來(lái)說(shuō),二進(jìn)制退避是最快速找到讓每個(gè)節(jié)點(diǎn)都正常工作的方案!
?
編輯
二進(jìn)制退避方案中隱含了對(duì)公平性的考慮,它是站在整個(gè)網(wǎng)絡(luò)的角度,而不是其中某一臺(tái)主機(jī)!
但對(duì)一臺(tái)特定的主機(jī),不遵守這個(gè)退避規(guī)則顯然好處更多...比如使用固定的重傳定時(shí)器時(shí)間。在這種網(wǎng)絡(luò)中,沒(méi)有擁塞時(shí)大家相安無(wú)事,一旦出現(xiàn)了擁塞,那么不退避的主機(jī)理論上就能發(fā)出更多的報(bào)文!
?
編輯
當(dāng)然,這似乎犧牲了其他遵守規(guī)則主機(jī)的利益,它們的重傳次數(shù)會(huì)增加。那么,如果大家都不遵守呢?結(jié)果就是大家的重傳次數(shù)都增加了,擁塞甚至比大家都遵守還要糟糕,因?yàn)榫W(wǎng)絡(luò)上的設(shè)備丟的包更多了!
這就有點(diǎn)囚徒困境的味道了,如果別人退避你不退避,你能獲利,如果別人比退避而你退避,那么你就吃虧了,如果大家都不遵守,那么比都遵守還要糟糕......
當(dāng)然,現(xiàn)實(shí)中的網(wǎng)絡(luò)并不同于CSMA/CD中的共享介質(zhì),一個(gè)簡(jiǎn)單的區(qū)別就是,在CSMA/CD中,如果一個(gè)節(jié)點(diǎn)監(jiān)聽(tīng)到信道繁忙,它是不是發(fā)送數(shù)據(jù)的,它需要等待;而在網(wǎng)絡(luò)中,并不存在這樣的共享介質(zhì),并且路由器是又緩沖隊(duì)列的,因此兩個(gè)主機(jī)還是可以都發(fā)送報(bào)文。
話雖如此,很多游戲?yàn)榱吮WC實(shí)時(shí)性不會(huì)選擇TCP !畢竟,TCP太無(wú)私了,它是為了整個(gè)網(wǎng)絡(luò)考慮的。而實(shí)時(shí)游戲需要的是什么!快速!那么如果它們有需要可靠傳輸怎么辦? 很簡(jiǎn)單,使用UDP,然后在用戶態(tài)自己做ARQ就好了,想怎么折騰怎么折騰,比如KCP就是這么個(gè)東西。