這篇文章是下篇晚树,所以如果你對TCP不熟悉的話稚晚,還請你先看看上篇《TCP的那些事兒(上)》 上篇中崇堵,我們介紹了TCP的協(xié)議頭、狀態(tài)機(jī)客燕、數(shù)據(jù)重傳中的東西鸳劳。但是TCP要解決一個(gè)很大的事,那就是要在一個(gè)網(wǎng)絡(luò)根據(jù)不同的情況來動(dòng)態(tài)調(diào)整自己的發(fā)包的速度也搓,小則讓自己的連接更穩(wěn)定赏廓,大則讓整個(gè)網(wǎng)絡(luò)更穩(wěn)定。在你閱讀下篇之前傍妒,你需要做好準(zhǔn)備幔摸,本篇文章有好些算法和策略,可能會(huì)引發(fā)你的各種思考颤练,讓你的大腦分配很多內(nèi)存和計(jì)算資源既忆,所以,不適合在廁所中閱讀。
TCP的RTT算法
從前面的TCP重傳機(jī)制我們知道Timeout的設(shè)置對于重傳非常重要尿贫。
設(shè)長了电媳,重發(fā)就慢,丟了老半天才重發(fā)庆亡,沒有效率匾乓,性能差;
設(shè)短了又谋,會(huì)導(dǎo)致可能并沒有丟就重發(fā)拼缝。于是重發(fā)的就快,會(huì)增加網(wǎng)絡(luò)擁塞彰亥,導(dǎo)致更多的超時(shí)咧七,更多的超時(shí)導(dǎo)致更多的重發(fā)。
而且任斋,這個(gè)超時(shí)時(shí)間在不同的網(wǎng)絡(luò)的情況下继阻,根本沒有辦法設(shè)置一個(gè)死的值。只能動(dòng)態(tài)地設(shè)置废酷。 為了動(dòng)態(tài)地設(shè)置瘟檩,TCP引入了RTT——Round Trip Time,也就是一個(gè)數(shù)據(jù)包從發(fā)出去到回來的時(shí)間澈蟆。這樣發(fā)送端就大約知道需要多少的時(shí)間墨辛,從而可以方便地設(shè)置Timeout——RTO(Retransmission TimeOut),以讓我們的重傳機(jī)制更高效趴俘。 聽起來似乎很簡單睹簇,好像就是在發(fā)送端發(fā)包時(shí)記下t0,然后接收端再把這個(gè)ack回來時(shí)再記一個(gè)t1寥闪,于是RTT = t1 – t0太惠。沒那么簡單,這只是一個(gè)采樣疲憋,不能代表普遍情況垛叨。
經(jīng)典算法
RFC793中定義的經(jīng)典算法是這樣的:
1)首先,先采樣RTT柜某,記下最近好幾次的RTT值。
2)然后做平滑計(jì)算SRTT( Smoothed RTT)敛纲。公式為:(其中的 α 取值在0.8 到 0.9之間喂击,這個(gè)算法英文叫Exponential weighted moving average,中文叫:加權(quán)移動(dòng)平均)
SRTT =?( α * SRTT ) + ((1- α) * RTT)
3)開始計(jì)算RTO淤翔。公式如下:
RTO = min [ UBOUND, ?max [ LBOUND, ? (β * SRTT) ] ?]
其中:
UBOUND是最大的timeout時(shí)間翰绊,上限值
LBOUND是最小的timeout時(shí)間,下限值
β 值一般在1.3到2.0之間。
Karn / Partridge 算法
但是上面的這個(gè)算法在重傳的時(shí)候會(huì)出有一個(gè)終極問題——你是用第一次發(fā)數(shù)據(jù)的時(shí)間和ack回來的時(shí)間做RTT樣本值监嗜,還是用重傳的時(shí)間和ACK回來的時(shí)間做RTT樣本值谐檀?
這個(gè)問題無論你選那頭都是按下葫蘆起了瓢。 如下圖所示:
情況(a)是ack沒回來裁奇,所以重傳桐猬。如果你計(jì)算第一次發(fā)送和ACK的時(shí)間,那么刽肠,明顯算大了溃肪。
情況(b)是ack回來慢了,但是導(dǎo)致了重傳音五,但剛重傳不一會(huì)兒惫撰,之前ACK就回來了。如果你是算重傳的時(shí)間和ACK回來的時(shí)間的差躺涝,就會(huì)算短了厨钻。
所以1987年的時(shí)候,搞了一個(gè)叫Karn / Partridge Algorithm坚嗜,這個(gè)算法的最大特點(diǎn)是——忽略重傳夯膀,不把重傳的RTT做采樣(你看,你不需要去解決不存在的問題)惶傻。
但是棍郎,這樣一來,又會(huì)引發(fā)一個(gè)大BUG——如果在某一時(shí)間银室,網(wǎng)絡(luò)閃動(dòng)涂佃,突然變慢了,產(chǎn)生了比較大的延時(shí)蜈敢,這個(gè)延時(shí)導(dǎo)致要重轉(zhuǎn)所有的包(因?yàn)橹暗腞TO很泄架),于是抓狭,因?yàn)橹剞D(zhuǎn)的不算伯病,所以,RTO就不會(huì)被更新否过,這是一個(gè)災(zāi)難午笛。 于是Karn算法用了一個(gè)取巧的方式——只要一發(fā)生重傳,就對現(xiàn)有的RTO值翻倍(這就是所謂的?Exponential backoff)苗桂,很明顯药磺,這種死規(guī)矩對于一個(gè)需要估計(jì)比較準(zhǔn)確的RTT也不靠譜。
Jacobson / Karels 算法
前面兩種算法用的都是“加權(quán)移動(dòng)平均”煤伟,這種方法最大的毛病就是如果RTT有一個(gè)大的波動(dòng)的話癌佩,很難被發(fā)現(xiàn)木缝,因?yàn)楸黄交袅恕K裕?988年围辙,又有人推出來了一個(gè)新的算法我碟,這個(gè)算法叫Jacobson / Karels Algorithm(參看RFC6289)。這個(gè)算法引入了最新的RTT的采樣和平滑過的SRTT的差距做因子來計(jì)算姚建。 公式如下:(其中的DevRTT是Deviation RTT的意思)
SRTT?= SRTT?+ α?(RTT?– SRTT) ?—— 計(jì)算平滑RTT
DevRTT?= (1-β)*DevRTT?+ β*(|RTT-SRTT|)?——計(jì)算平滑RTT和真實(shí)的差距(加權(quán)移動(dòng)平均)
RTO= μ *?SRTT + ? *DevRTT?—— 神一樣的公式
(其中:在Linux下矫俺,α = 0.125,β = 0.25桥胞, μ = 1恳守,??= 4 ——這就是算法中的“調(diào)得一手好參數(shù)”,nobody knows why, it just works…) 最后的這個(gè)算法在被用在今天的TCP協(xié)議中(Linux的源代碼在:tcp_rtt_estimator)贩虾。
TCP滑動(dòng)窗口
需要說明一下催烘,如果你不了解TCP的滑動(dòng)窗口這個(gè)事,你等于不了解TCP協(xié)議缎罢。我們都知道伊群,TCP必需要解決的可靠傳輸以及包亂序(reordering)的問題,所以策精,TCP必需要知道網(wǎng)絡(luò)實(shí)際的數(shù)據(jù)處理帶寬或是數(shù)據(jù)處理速度舰始,這樣才不會(huì)引起網(wǎng)絡(luò)擁塞,導(dǎo)致丟包咽袜。
所以丸卷,TCP引入了一些技術(shù)和設(shè)計(jì)來做網(wǎng)絡(luò)流控,Sliding Window是其中一個(gè)技術(shù)询刹。 前面我們說過谜嫉,TCP頭里有一個(gè)字段叫Window,又叫Advertised-Window凹联,這個(gè)字段是接收端告訴發(fā)送端自己還有多少緩沖區(qū)可以接收數(shù)據(jù)沐兰。于是發(fā)送端就可以根據(jù)這個(gè)接收端的處理能力來發(fā)送數(shù)據(jù),而不會(huì)導(dǎo)致接收端處理不過來蔽挠。 為了說明滑動(dòng)窗口住闯,我們需要先看一下TCP緩沖區(qū)的一些數(shù)據(jù)結(jié)構(gòu):
上圖中,我們可以看到:
接收端LastByteRead指向了TCP緩沖區(qū)中讀到的位置澳淑,NextByteExpected指向的地方是收到的連續(xù)包的最后一個(gè)位置比原,LastByteRcved指向的是收到的包的最后一個(gè)位置,我們可以看到中間有些數(shù)據(jù)還沒有到達(dá)杠巡,所以有數(shù)據(jù)空白區(qū)春寿。
發(fā)送端的LastByteAcked指向了被接收端Ack過的位置(表示成功發(fā)送確認(rèn)),LastByteSent表示發(fā)出去了忽孽,但還沒有收到成功確認(rèn)的Ack,LastByteWritten指向的是上層應(yīng)用正在寫的地方。
于是:
接收端在給發(fā)送端回ACK中會(huì)匯報(bào)自己的AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;
而發(fā)送方會(huì)根據(jù)這個(gè)窗口來控制發(fā)送數(shù)據(jù)的大小兄一,以保證接收方可以處理厘线。
下面我們來看一下發(fā)送方的滑動(dòng)窗口示意圖:
上圖中分成了四個(gè)部分,分別是:(其中那個(gè)黑模型就是滑動(dòng)窗口)
#1已收到ack確認(rèn)的數(shù)據(jù)出革。
#2發(fā)還沒收到ack的造壮。
#3在窗口中還沒有發(fā)出的(接收方還有空間)。
#4窗口以外的數(shù)據(jù)(接收方?jīng)]空間)
下面是個(gè)滑動(dòng)后的示意圖(收到36的ack骂束,并發(fā)出了46-51的字節(jié)):
下面我們來看一個(gè)接受端控制發(fā)送端的圖示:
Zero Window
上圖耳璧,我們可以看到一個(gè)處理緩慢的Server(接收端)是怎么把Client(發(fā)送端)的TCP Sliding Window給降成0的。此時(shí)展箱,你一定會(huì)問旨枯,如果Window變成0了,TCP會(huì)怎么樣混驰?是不是發(fā)送端就不發(fā)數(shù)據(jù)了攀隔?是的,發(fā)送端就不發(fā)數(shù)據(jù)了栖榨,你可以想像成“Window Closed”昆汹,那你一定還會(huì)問,如果發(fā)送端不發(fā)數(shù)據(jù)了婴栽,接收方一會(huì)兒Window size 可用了满粗,怎么通知發(fā)送端呢?
解決這個(gè)問題愚争,TCP使用了Zero Window Probe技術(shù)映皆,縮寫為ZWP,也就是說准脂,發(fā)送端在窗口變成0后劫扒,會(huì)發(fā)ZWP的包給接收方,讓接收方來ack他的Window尺寸狸膏,一般這個(gè)值會(huì)設(shè)置成3次沟饥,第次大約30-60秒(不同的實(shí)現(xiàn)可能會(huì)不一樣)。如果3次過后還是0的話湾戳,有的TCP實(shí)現(xiàn)就會(huì)發(fā)RST把鏈接斷了贤旷。
注意:只要有等待的地方都可能出現(xiàn)DDoS攻擊,Zero Window也不例外砾脑,一些攻擊者會(huì)在和HTTP建好鏈發(fā)完GET請求后幼驶,就把Window設(shè)置為0,然后服務(wù)端就只能等待進(jìn)行ZWP韧衣,于是攻擊者會(huì)并發(fā)大量的這樣的請求盅藻,把服務(wù)器端的資源耗盡购桑。(關(guān)于這方面的攻擊,大家可以移步看一下Wikipedia的SockStress詞條)
另外氏淑,Wireshark中勃蜘,你可以使用tcp.analysis.zero_window來過濾包,然后使用右鍵菜單里的follow TCP stream假残,你可以看到ZeroWindowProbe及ZeroWindowProbeAck的包缭贡。
Silly Window Syndrome
Silly Window Syndrome翻譯成中文就是“糊涂窗口綜合癥”。正如你上面看到的一樣辉懒,如果我們的接收方太忙了阳惹,來不及取走Receive Windows里的數(shù)據(jù),那么眶俩,就會(huì)導(dǎo)致發(fā)送方越來越小莹汤。到最后,如果接收方騰出幾個(gè)字節(jié)并告訴發(fā)送方現(xiàn)在有幾個(gè)字節(jié)的window仿便,而我們的發(fā)送方會(huì)義無反顧地發(fā)送這幾個(gè)字節(jié)体啰。
要知道,我們的TCP+IP頭有40個(gè)字節(jié)嗽仪,為了幾個(gè)字節(jié)荒勇,要達(dá)上這么大的開銷,這太不經(jīng)濟(jì)了闻坚。
另外沽翔,你需要知道網(wǎng)絡(luò)上有個(gè)MTU,對于以太網(wǎng)來說窿凤,MTU是1500字節(jié)仅偎,除去TCP+IP頭的40個(gè)字節(jié),真正的數(shù)據(jù)傳輸可以有1460雳殊,這就是所謂的MSS(Max Segment Size)注意橘沥,TCP的RFC定義這個(gè)MSS的默認(rèn)值是536,這是因?yàn)?RFC 791里說了任何一個(gè)IP設(shè)備都得最少接收576尺寸的大泻煌骸(實(shí)際上來說576是撥號(hào)的網(wǎng)絡(luò)的MTU座咆,而576減去IP頭的20個(gè)字節(jié)就是536)。
如果你的網(wǎng)絡(luò)包可以塞滿MTU仓洼,那么你可以用滿整個(gè)帶寬介陶,如果不能,那么你就會(huì)浪費(fèi)帶寬色建。(大于MTU的包有兩種結(jié)局哺呜,一種是直接被丟了,另一種是會(huì)被重新分塊打包發(fā)送) 你可以想像成一個(gè)MTU就相當(dāng)于一個(gè)飛機(jī)的最多可以裝的人箕戳,如果這飛機(jī)里滿載的話某残,帶寬最高国撵,如果一個(gè)飛機(jī)只運(yùn)一個(gè)人的話,無疑成本增加了驾锰,也而相當(dāng)二卸留。
所以,Silly Windows Syndrome這個(gè)現(xiàn)像就像是你本來可以坐200人的飛機(jī)里只做了一兩個(gè)人椭豫。 要解決這個(gè)問題也不難,就是避免對小的window size做出響應(yīng)旨指,直到有足夠大的window size再響應(yīng)赏酥,這個(gè)思路可以同時(shí)實(shí)現(xiàn)在sender和receiver兩端。
如果這個(gè)問題是由Receiver端引起的谆构,那么就會(huì)使用?David D Clark’s 方案裸扶。在receiver端,如果收到的數(shù)據(jù)導(dǎo)致window size小于某個(gè)值搬素,可以直接ack(0)回sender呵晨,這樣就把window給關(guān)閉了,也阻止了sender再發(fā)數(shù)據(jù)過來熬尺,等到receiver端處理了一些數(shù)據(jù)后windows size 大于等于了MSS摸屠,或者,receiver buffer有一半為空粱哼,就可以把window打開讓send 發(fā)送數(shù)據(jù)過來季二。
如果這個(gè)問題是由Sender端引起的,那么就會(huì)使用著名的?Nagle’s algorithm揭措。這個(gè)算法的思路也是延時(shí)處理胯舷,他有兩個(gè)主要的條件:
1)要等到 Window Size>=MSS 或是 Data Size >=MSS,
2)收到之前發(fā)送數(shù)據(jù)的ack回包绊含,他才會(huì)發(fā)數(shù)據(jù)桑嘶,否則就是在攢數(shù)據(jù)。
以上兩點(diǎn)躬充,即是導(dǎo)致TCP粘包的原因逃顶。
另外,Nagle算法默認(rèn)是打開的麻裳,所以口蝠,對于一些需要小包場景的程序——比如像telnet或ssh這樣的交互性比較強(qiáng)的程序,你需要關(guān)閉這個(gè)算法津坑。你可以在Socket設(shè)置TCP_NODELAY選項(xiàng)來關(guān)閉這個(gè)算法(關(guān)閉Nagle算法沒有全局參數(shù)妙蔗,需要根據(jù)每個(gè)應(yīng)用自己的特點(diǎn)來關(guān)閉)
setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char*)&value,sizeof(int));
另外,網(wǎng)上有些文章說TCP_CORK的socket option是也關(guān)閉Nagle算法疆瑰,這不對眉反。TCP_CORK其實(shí)是更新激進(jìn)的Nagle算漢昙啄,完全禁止小包發(fā)送,而Nagle算法沒有禁止小包發(fā)送寸五,只是禁止了大量的小包發(fā)送梳凛。最好不要兩個(gè)選項(xiàng)都設(shè)置。
Nagle算法參考資料:
TCP的擁塞處理 –?Congestion Handling
上面我們知道了梳杏,TCP通過Sliding Window來做流控(Flow Control)韧拒,但是TCP覺得這還不夠,因?yàn)镾liding Window需要依賴于連接的發(fā)送端和接收端十性,其并不知道網(wǎng)絡(luò)中間發(fā)生了什么叛溢。TCP的設(shè)計(jì)者覺得,一個(gè)偉大而牛逼的協(xié)議僅僅做到流控并不夠劲适,因?yàn)榱骺刂皇蔷W(wǎng)絡(luò)模型4層以上的事楷掉,TCP的還應(yīng)該更聰明地知道整個(gè)網(wǎng)絡(luò)上的事。
具體一點(diǎn)霞势,我們知道TCP通過一個(gè)timer采樣了RTT并計(jì)算RTO烹植,但是,如果網(wǎng)絡(luò)上的延時(shí)突然增加愕贡,那么草雕,TCP對這個(gè)事做出的應(yīng)對只有重傳數(shù)據(jù),但是颂鸿,重傳會(huì)導(dǎo)致網(wǎng)絡(luò)的負(fù)擔(dān)更重促绵,于是會(huì)導(dǎo)致更大的延遲以及更多的丟包,于是嘴纺,這個(gè)情況就會(huì)進(jìn)入惡性循環(huán)被不斷地放大败晴。試想一下,如果一個(gè)網(wǎng)絡(luò)內(nèi)有成千上萬的TCP連接都這么行事栽渴,那么馬上就會(huì)形成“網(wǎng)絡(luò)風(fēng)暴”尖坤,TCP這個(gè)協(xié)議就會(huì)拖垮整個(gè)網(wǎng)絡(luò)。這是一個(gè)災(zāi)難闲擦。
所以慢味,TCP不能忽略網(wǎng)絡(luò)上發(fā)生的事情,而無腦地一個(gè)勁地重發(fā)數(shù)據(jù)墅冷,對網(wǎng)絡(luò)造成更大的傷害纯路。對此TCP的設(shè)計(jì)理念是:
TCP不是一個(gè)自私的協(xié)議,當(dāng)擁塞發(fā)生的時(shí)候寞忿,要做自我犧牲驰唬。就像交通阻塞一樣,每個(gè)車都應(yīng)該把路讓出來,而不要再去搶路了叫编。
關(guān)于擁塞控制的論文請參看《Congestion Avoidance and Control》(PDF)
擁塞控制主要是四個(gè)算法:1)慢啟動(dòng)辖佣,2)擁塞避免,3)擁塞發(fā)生搓逾,4)快速恢復(fù)卷谈。這四個(gè)算法不是一天都搞出來的,這個(gè)四算法的發(fā)展經(jīng)歷了很多時(shí)間霞篡,到今天都還在優(yōu)化中世蔗。 備注:
1988年,TCP-Tahoe 提出了1)慢啟動(dòng)朗兵,2)擁塞避免凸郑,3)擁塞發(fā)生時(shí)的快速重傳
1990年,TCP Reno 在Tahoe的基礎(chǔ)上增加了4)快速恢復(fù)
慢熱啟動(dòng)算法 – Slow Start
首先矛市,我們來看一下TCP的慢熱啟動(dòng)。慢啟動(dòng)的意思是诲祸,剛剛加入網(wǎng)絡(luò)的連接浊吏,一點(diǎn)一點(diǎn)地提速,不要一上來就像那些特權(quán)車一樣霸道地把路占滿救氯。新同學(xué)上高速還是要慢一點(diǎn)找田,不要把已經(jīng)在高速上的秩序給搞亂了。
慢啟動(dòng)的算法如下(cwnd全稱Congestion Window):
1)連接建好的開始先初始化cwnd = 1着憨,表明可以傳一個(gè)MSS大小的數(shù)據(jù)墩衙。
2)每當(dāng)收到一個(gè)ACK,cwnd++; 呈線性上升
3)每當(dāng)過了一個(gè)RTT甲抖,cwnd = cwnd*2; 呈指數(shù)讓升
4)還有一個(gè)ssthresh(slow start threshold)漆改,是一個(gè)上限,當(dāng)cwnd >= ssthresh時(shí)准谚,就會(huì)進(jìn)入“擁塞避免算法”(后面會(huì)說這個(gè)算法)
所以挫剑,我們可以看到,如果網(wǎng)速很快的話柱衔,ACK也會(huì)返回得快樊破,RTT也會(huì)短,那么唆铐,這個(gè)慢啟動(dòng)就一點(diǎn)也不慢哲戚。下圖說明了這個(gè)過程。
這里艾岂,我需要提一下的是一篇Google的論文《An Argument for Increasing TCP’s Initial Congestion Window》Linux 3.0后采用了這篇論文的建議——把cwnd 初始化成了 10個(gè)MSS顺少。而Linux 3.0以前,比如2.6澳盐,Linux采用了RFC3390祈纯,cwnd是跟MSS的值來變的令宿,如果MSS< 1095,則cwnd = 4腕窥;如果MSS>2190粒没,則cwnd=2;其它情況下簇爆,則是3癞松。
?擁塞避免算法 –?Congestion Avoidance
前面說過,還有一個(gè)ssthresh(slow start threshold)入蛆,是一個(gè)上限响蓉,當(dāng)cwnd >= ssthresh時(shí),就會(huì)進(jìn)入“擁塞避免算法”哨毁。一般來說ssthresh的值是65535枫甲,單位是字節(jié),當(dāng)cwnd達(dá)到這個(gè)值時(shí)后扼褪,算法如下:
1)收到一個(gè)ACK時(shí)想幻,cwnd = cwnd + 1/cwnd
2)當(dāng)每過一個(gè)RTT時(shí),cwnd = cwnd + 1
這樣就可以避免增長過快導(dǎo)致網(wǎng)絡(luò)擁塞话浇,慢慢的增加調(diào)整到網(wǎng)絡(luò)的最佳值脏毯。很明顯,是一個(gè)線性上升的算法幔崖。
擁塞狀態(tài)時(shí)的算法
前面我們說過食店,當(dāng)丟包的時(shí)候,會(huì)有兩種情況:
1)等到RTO超時(shí)赏寇,重傳數(shù)據(jù)包吉嫩。TCP認(rèn)為這種情況太糟糕,反應(yīng)也很強(qiáng)烈蹋订。
sshthresh = ?cwnd /2
cwnd 重置為 1
進(jìn)入慢啟動(dòng)過程
2)Fast Retransmit算法率挣,也就是在收到3個(gè)duplicate ACK時(shí)就開啟重傳,而不用等到RTO超時(shí)露戒。
TCP Tahoe的實(shí)現(xiàn)和RTO超時(shí)一樣椒功。
TCP Reno的實(shí)現(xiàn)是:
cwnd = cwnd /2
sshthresh = cwnd
進(jìn)入快速恢復(fù)算法——Fast Recovery
上面我們可以看到RTO超時(shí)后,sshthresh會(huì)變成cwnd的一半智什,這意味著动漾,如果cwnd<=sshthresh時(shí)出現(xiàn)的丟包,那么TCP的sshthresh就會(huì)減了一半荠锭,然后等cwnd又很快地以指數(shù)級(jí)增漲爬到這個(gè)地方時(shí)旱眯,就會(huì)成慢慢的線性增漲。我們可以看到,TCP是怎么通過這種強(qiáng)烈地震蕩快速而小心得找到網(wǎng)站流量的平衡點(diǎn)的删豺。
快速恢復(fù)算法 – Fast Recovery
TCP Reno
這個(gè)算法定義在RFC5681共虑。快速重傳和快速恢復(fù)算法一般同時(shí)使用呀页÷璋瑁快速恢復(fù)算法是認(rèn)為,你還有3個(gè)Duplicated Acks說明網(wǎng)絡(luò)也不那么糟糕蓬蝶,所以沒有必要像RTO超時(shí)那么強(qiáng)烈尘分。?注意,正如前面所說丸氛,進(jìn)入Fast Recovery之前培愁,cwnd 和 sshthresh已被更新:
cwnd = cwnd /2
sshthresh = cwnd
然后,真正的Fast Recovery算法如下:
cwnd = sshthresh ?+ 3 * MSS (3的意思是確認(rèn)有3個(gè)數(shù)據(jù)包被收到了)
重傳Duplicated ACKs指定的數(shù)據(jù)包
如果再收到 duplicated Acks缓窜,那么cwnd = cwnd +1
如果收到了新的Ack定续,那么,cwnd = sshthresh 禾锤,然后就進(jìn)入了擁塞避免的算法了香罐。
如果你仔細(xì)思考一下上面的這個(gè)算法,你就會(huì)知道时肿,上面這個(gè)算法也有問題,那就是——它依賴于3個(gè)重復(fù)的Acks港粱。注意螃成,3個(gè)重復(fù)的Acks并不代表只丟了一個(gè)數(shù)據(jù)包,很有可能是丟了好多包查坪。但這個(gè)算法只會(huì)重傳一個(gè)寸宏,而剩下的那些包只能等到RTO超時(shí),于是偿曙,進(jìn)入了惡夢模式——超時(shí)一個(gè)窗口就減半一下氮凝,多個(gè)超時(shí)會(huì)超成TCP的傳輸速度呈級(jí)數(shù)下降,而且也不會(huì)觸發(fā)Fast Recovery算法了望忆。
通常來說罩阵,正如我們前面所說的,SACK或D-SACK的方法可以讓Fast Recovery或Sender在做決定時(shí)更聰明一些启摄,但是并不是所有的TCP的實(shí)現(xiàn)都支持SACK(SACK需要兩端都支持)稿壁,所以,需要一個(gè)沒有SACK的解決方案歉备。而通過SACK進(jìn)行擁塞控制的算法是FACK(后面會(huì)講)
TCP New Reno
于是傅是,1995年,TCP New Reno(參見?RFC 6582)算法提出來,主要就是在沒有SACK的支持下改進(jìn)Fast Recovery算法的——
當(dāng)sender這邊收到了3個(gè)Duplicated Acks喧笔,進(jìn)入Fast Retransimit模式帽驯,開發(fā)重傳重復(fù)Acks指示的那個(gè)包。如果只有這一個(gè)包丟了书闸,那么尼变,重傳這個(gè)包后回來的Ack會(huì)把整個(gè)已經(jīng)被sender傳輸出去的數(shù)據(jù)ack回來。如果沒有的話梗劫,說明有多個(gè)包丟了享甸。我們叫這個(gè)ACK為Partial ACK。
一旦Sender這邊發(fā)現(xiàn)了Partial ACK出現(xiàn)梳侨,那么蛉威,sender就可以推理出來有多個(gè)包被丟了,于是乎繼續(xù)重傳sliding window里未被ack的第一個(gè)包走哺。直到再也收不到了Partial Ack蚯嫌,才真正結(jié)束Fast Recovery這個(gè)過程
我們可以看到,這個(gè)“Fast Recovery的變更”是一個(gè)非常激進(jìn)的玩法丙躏,他同時(shí)延長了Fast Retransmit和Fast Recovery的過程苗膝。
算法示意圖
下面我們來看一個(gè)簡單的圖示以同時(shí)看一下上面的各種算法的樣子:
FACK算法
FACK全稱Forward Acknowledgment 算法,論文地址在這里(PDF)Forward Acknowledgement: Refining TCP Congestion Control?這個(gè)算法是其于SACK的贮喧,前面我們說過SACK是使用了TCP擴(kuò)展字段Ack了有哪些數(shù)據(jù)收到掷邦,哪些數(shù)據(jù)沒有收到,他比Fast Retransmit的3 個(gè)duplicated acks好處在于废恋,前者只知道有包丟了谈秫,不知道是一個(gè)還是多個(gè),而SACK可以準(zhǔn)確的知道有哪些包丟了鱼鼓。 所以拟烫,SACK可以讓發(fā)送端這邊在重傳過程中,把那些丟掉的包重傳迄本,而不是一個(gè)一個(gè)的傳硕淑,但這樣的一來,如果重傳的包數(shù)據(jù)比較多的話嘉赎,又會(huì)導(dǎo)致本來就很忙的網(wǎng)絡(luò)就更忙了置媳。所以,F(xiàn)ACK用來做重傳過程中的擁塞流控公条。
這個(gè)算法會(huì)把SACK中最大的Sequence Number 保存在snd.fack這個(gè)變量中半开,snd.fack的更新由ack帶秋,如果網(wǎng)絡(luò)一切安好則和snd.una一樣(snd.una就是還沒有收到ack的地方赃份,也就是前面sliding window里的category #2的第一個(gè)地方)
然后定義一個(gè)awnd = snd.nxt – snd.fack(snd.nxt指向發(fā)送端sliding window中正在要被發(fā)送的地方——前面sliding windows圖示的category#3第一個(gè)位置)寂拆,這樣awnd的意思就是在網(wǎng)絡(luò)上的數(shù)據(jù)奢米。(所謂awnd意為:actual quantity of data outstanding in the network)
如果需要重傳數(shù)據(jù),那么纠永,awnd =?snd.nxt – snd.fack + retran_data鬓长,也就是說,awnd是傳出去的數(shù)據(jù) + 重傳的數(shù)據(jù)尝江。
然后觸發(fā)Fast Recovery 的條件是:?(?( snd.fack – snd.una ) > (3*MSS)?) || (dupacks == 3) ) 涉波。這樣一來,就不需要等到3個(gè)duplicated acks才重傳炭序,而是只要sack中的最大的一個(gè)數(shù)據(jù)和ack的數(shù)據(jù)比較長了(3個(gè)MSS)啤覆,那就觸發(fā)重傳。在整個(gè)重傳過程中cwnd不變惭聂。直到當(dāng)?shù)谝淮蝸G包的snd.nxt<=snd.una(也就是重傳的數(shù)據(jù)都被確認(rèn)了)窗声,然后進(jìn)來擁塞避免機(jī)制——cwnd線性上漲辜纲。
我們可以看到如果沒有FACK在,那么在丟包比較多的情況下耕腾,原來保守的算法會(huì)低估了需要使用的window的大小,而需要幾個(gè)RTT的時(shí)間才會(huì)完成恢復(fù)扫俺,而FACK會(huì)比較激進(jìn)地來干這事苍苞。 但是柒啤,F(xiàn)ACK如果在一個(gè)網(wǎng)絡(luò)包會(huì)被 reordering的網(wǎng)絡(luò)里會(huì)有很大的問題。
其它擁塞控制算法簡介
TCP Vegas 擁塞控制算法
這個(gè)算法1994年被提出畸颅,它主要對TCP Reno 做了些修改没炒。這個(gè)算法通過對RTT的非常重的監(jiān)控來計(jì)算一個(gè)基準(zhǔn)RTT。然后通過這個(gè)基準(zhǔn)RTT來估計(jì)當(dāng)前的網(wǎng)絡(luò)實(shí)際帶寬犯戏,如果實(shí)際帶寬比我們的期望的帶寬要小或是要多的活送火,那么就開始線性地減少或增加cwnd的大小。如果這個(gè)計(jì)算出來的RTT大于了Timeout后先匪,那么种吸,不等ack超時(shí)就直接重傳。(Vegas 的核心思想是用RTT的值來影響擁塞窗口呀非,而不是通過丟包) 這個(gè)算法的論文是《TCP Vegas: End to End Congestion Avoidance on a Global Internet》這篇論文給了Vegas和 New Reno的對比:
關(guān)于這個(gè)算法實(shí)現(xiàn)坚俗,你可以參看Linux源碼:/net/ipv4/tcp_vegas.h镜盯,/net/ipv4/tcp_vegas.c
HSTCP(High Speed TCP) 算法
這個(gè)算法來自RFC 3649(Wikipedia詞條)。其對最基礎(chǔ)的算法進(jìn)行了更改猖败,他使得Congestion Window漲得快速缆,減得慢。其中:
擁塞避免時(shí)的窗口增長方式: cwnd = cwnd + α(cwnd) / cwnd
丟包后窗口下降方式:cwnd = (1- β(cwnd))*cwnd
注:α(cwnd)和β(cwnd)都是函數(shù)恩闻,如果你要讓他們和標(biāo)準(zhǔn)的TCP一樣艺糜,那么讓α(cwnd)=1,β(cwnd)=0.5就可以了幢尚。 對于α(cwnd)和β(cwnd)的值是個(gè)動(dòng)態(tài)的變換的東西破停。 關(guān)于這個(gè)算法的實(shí)現(xiàn),你可以參看Linux源碼:/net/ipv4/tcp_highspeed.c
TCP BIC 算法
2004年尉剩,產(chǎn)內(nèi)出BIC算法≌媛現(xiàn)在你還可以查得到相關(guān)的新聞《Google:美科學(xué)家研發(fā)BIC-TCP協(xié)議 速度是DSL六千倍》 BIC全稱Binary Increase Congestion control,在Linux 2.6.8中是默認(rèn)擁塞控制算法边涕。BIC的發(fā)明者發(fā)這么多的擁塞控制算法都在努力找一個(gè)合適的cwnd – Congestion Window晤碘,而且BIC-TCP的提出者們看穿了事情的本質(zhì),其實(shí)這就是一個(gè)搜索的過程功蜓,所以BIC這個(gè)算法主要用的是Binary Search——二分查找來干這個(gè)事园爷。 關(guān)于這個(gè)算法實(shí)現(xiàn),你可以參看Linux源碼:/net/ipv4/tcp_bic.c
TCP WestWood算法
westwood采用和Reno相同的慢啟動(dòng)算法式撼、擁塞避免算法童社。westwood的主要改進(jìn)方面:在發(fā)送端做帶寬估計(jì),當(dāng)探測到丟包時(shí)著隆,根據(jù)帶寬值來設(shè)置擁塞窗口扰楼、慢啟動(dòng)閾值。?那么美浦,這個(gè)算法是怎么測量帶寬的?每個(gè)RTT時(shí)間浦辨,會(huì)測量一次帶寬,測量帶寬的公式很簡單币厕,就是這段RTT內(nèi)成功被ack了多少字節(jié)芽腾。因?yàn)椋@個(gè)帶寬和用RTT計(jì)算RTO一樣阴绢,也是需要從每個(gè)樣本來平滑到一個(gè)值的——也是用一個(gè)加權(quán)移平均的公式。 另外旱函,我們知道,如果一個(gè)網(wǎng)絡(luò)的帶寬是每秒可以發(fā)送X個(gè)字節(jié)踪古,而RTT是一個(gè)數(shù)據(jù)發(fā)出去后確認(rèn)需要的時(shí)候券腔,所以,X * RTT應(yīng)該是我們緩沖區(qū)大小枕扫。所以辱魁,在這個(gè)算法中,ssthresh的值就是est_BD * min-RTT(最小的RTT值)参滴,如果丟包是Duplicated ACKs引起的锻弓,那么如果cwnd > ssthresh,則 cwin = ssthresh暴心。如果是RTO引起的杂拨,cwnd = 1,進(jìn)入慢啟動(dòng)弹沽。 ? 關(guān)于這個(gè)算法實(shí)現(xiàn),你可以參看Linux源碼:/net/ipv4/tcp_westwood.c
其它
更多的算法,你可以從Wikipedia的TCP Congestion Avoidance Algorithm?詞條中找到相關(guān)的線索
后記
好了亏狰,到這里我想可以結(jié)束了,TCP發(fā)展到今天促脉,里面的東西可以寫上好幾本書。本文主要目的瘸味,還是把你帶入這些古典的基礎(chǔ)技術(shù)和知識(shí)中,希望本文能讓你了解TCP藕夫,更希望本文能讓你開始有學(xué)習(xí)這些基礎(chǔ)或底層知識(shí)的興趣和信心枯冈。
當(dāng)然,TCP東西太多了滩褥,不同的人可能有不同的理解炫加,而且本文可能也會(huì)有一些荒謬之言甚至錯(cuò)誤,還希望得到您的反饋和批評俗孝。