從TCP三次握手說起--淺析TCP協議中的疑難雜症(2)

作者介紹:黃日成,手Q遊戲中心後台開發,騰訊高級工程師。從事C++服務後台開發4年多,主要負責手Q遊戲中心後台基礎系統、複雜業務系統開發,主導過手Q遊戲公會、企鵝電競App-對戰系統等項目的後台系統設計,有豐富的後台架構經驗。本文獲得了小時光茶社的授權

7. 疑症(7)TCP的延遲確認機制

按照TCP協議,確認機制是累積的,也就是確認號X的確認指示的是所有X之前但不包括X的數據已經收到了。確認號(ACK)本身就是不含數據的分段,因此大量的確認號消耗了大量的帶寬,雖然大多數情況下,ACK還是可以和數據一起捎帶傳輸的,但是如果沒有捎帶傳輸,那麼就只能單獨回來一個ACK,如果這樣的分段太多,網路的利用率就會下降。為緩解這個問題,RFC建議了一種延遲的ACK,也就是說,ACK在收到數據後並不馬上回復,而是延遲一段可以接受的時間,延遲一段時間的目的是看能不能和接收方要發給發送方的數據一起回去,因為TCP協議頭中總是包含確認號的,如果能的話,就將數據一起捎帶回去,這樣網路利用率就提高了。延遲ACK就算沒有數據捎帶,那麼如果收到了按序的兩個包,那麼只要對第二包做確認即可,這樣也能省去一個ACK消耗。由於TCP協議不對ACK進行ACK的,RFC建議最多等待2個包的積累確認,這樣能夠及時通知對端Peer,我這邊的接收情況。

Linux實現中,有延遲ACK和快速ACK,並根據當前的包的收發情況來在這兩種ACK中切換。一般情況下,ACK並不會對網路性能有太大的影響,延遲ACK能減少發送的分段從而節省了帶寬,而快速ACK能及時通知發送方丟包,避免滑動窗口停等,提升吞吐率。關於ACK分段,有個細節需要說明一下,ACK的確認號,是確認按序收到的最後一個位元組序,對於亂序到來的TCP分段,接收端會回復相同的ACK分段,只確認按序到達的最後一個TCP分段。TCP連接的延遲確認時間一般初始化為最小值40ms,隨後根據連接的重傳超時時間(RTO)、上次收到數據包與本次接收數據包的時間間隔等參數進行不斷調整。

8. 疑症(8)TCP的重傳機制以及重傳的超時計算

【1】TCP的重傳超時計算

TCP交互過程中,如果發送的包一直沒收到ACK確認,是要一直等下去嗎?顯然不能一直等(如果發送的包在路由過程中丟失了,對端都沒收到又如何給你發送確認呢?),這樣協議將不可用,既然不能一直等下去,那麼該等多久呢?等太長時間的話,數據包都丟了很久了才重發,沒有效率,性能差;等太短時間的話,可能ACK還在路上快到了,這時候卻重傳了,造成浪費,同時過多的重傳會造成網路擁塞,進一步加劇數據的丟失。也是,我們不能去猜測一個重傳超時時間,應該是通過一個演算法去計算,並且這個超時時間應該是隨著網路的狀況在變化的。為了使我們的重傳機制更高效,如果我們能夠比較準確知道在當前網路狀況下,一個數據包從發出去到回來的時間RTT——Round Trip Time,那麼根據這個RTT我們就可以方便設置TimeOut——RTO(Retransmission TimeOut)了。

為了計算這個RTO,RFC793中定義了一個經典演算法,演算法如下:

[1] 首先採樣計算RTT值nn[2] 然後計算平滑的RTT,稱為Smoothed Round Trip Time (SRTT),SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT)nn[3] RTO = min[UBOUND,max[LBOUND,(BETA*SRTT)]]n

其中:UBOUND是RTO值的上限;例如:可以定義為1分鐘,LBOUND是RTO值的下限,例如,可以定義為1秒;ALPHA is a smoothing factor (e.g., .8 to .9), and BETA is a delay variance factor (e.g., 1.3 to 2.0). 然而這個演算法有個缺點就是:在算RTT樣本的時候,是用第一次發數據的時間和ack回來的時間做RTT樣本值,還是用重傳的時間和ACK回來的時間做RTT樣本值?不管是怎麼選擇,總會造成會要麼把RTT算過長了,要麼把RTT算過短了。如下圖:(a)就計算過長了,而(b)就是計算過短了。

針對上面經典演算法的缺陷,於是提出Karn / Partridge Algorithm對經典演算法進行了改進(演算法大特點是——忽略重傳,不把重傳的RTT做採樣),但是這個演算法有問題:如果在某一時間,網路閃動,突然變慢了,產生了比較大的延時,這個延時導致要重轉所有的包(因為之前的RTO很小),於是,因為重轉的不算,所以,RTO就不會被更新,這是一個災難。於是,為解決上面兩個演算法的問題,又有人推出來了一個新的演算法,這個演算法叫Jacobson / Karels Algorithm(參看RFC6289),這個演算法的核心是:除了考慮每兩次測量值的偏差之外,其變化率也應該考慮在內,如果變化率過大,則通過以變化率為自變數的函數為主計算RTT(如果陡然增大,則取值為比較大的正數,如果陡然減小,則取值為比較小的負數,然後和平均值加權求和),反之如果變化率很小,則取測量平均值。

公式如下:(其中的DevRTT是Deviation RTT的意思)

SRTT = SRTT + α (RTT – SRTT) —— 計算平滑RTTnnDevRTT = (1-β)*DevRTT + β*(|RTT-SRTT|) ——計算平滑RTT和真實的差距(加權移動平均)nnRTO= μ * SRTT + ? *DevRTT —— 神一樣的公式nn(其中:在Linux下,α = 0.125,β = 0.25, μ = 1,? = 4 ——這就是演算法中的「調得一手好參數」,nobody knows why, it just works…) 最後的這個演算法在被用在今天的TCP協議中並工作非常好n

知道超時怎麼計算後,很自然就想到定時器的設計問題。一個簡單直觀的方案就是為TCP中的每一個數據包維護一個定時器,在這個定時器到期前沒收到確認,則進行重傳。這種設計理論上是很合理的,但是實現上,這種方案將會有非常多的定時器,會帶來巨大內存開銷和調度開銷。既然不能每個包一個定時器,那麼多少個包一個定時器才好呢,這個似乎比較難確定。可以換個思路,不要以包量來確定定時器,以連接來確定定時器會不會比較合理呢?目前,採取每一個TCP連接單一超時定時器的設計則成了一個默認的選擇,並且RFC2988給出了每連接單一定時器的設計建議演算法規則:

[1]. 每一次一個包含數據的包被發送(包括重發),如果還沒開啟重傳定時器,則開啟它,使得它在RTO秒之後超時(按照當前的RTO值)。nn[2]. 當接收到一個ACK確認一個新的數據, 如果所有的發出數據都被確認了,關閉重傳定時器。nn[3]. 當接收到一個ACK確認一個新的數據,還有數據在傳輸,也就是還有沒被確認的數據,重新啟動重傳定時器,使得它在RTO秒之後超時(按照當前的RTO值)。n當重傳定時器超時後,依次做下列3件事情:n[4.1]. 重傳最早的尚未被TCP接收方ACK的數據包nn[4.2]. 重新設置RTO 為 RTO * 2(「還原定時器」),但是新RTO不應該超過RTO的上限(RTO有個上限值,這個上限值最少為60s)nn[4.3]. 重啟重傳定時器。n

上面的建議演算法體現了一個原則:沒被確認的包必須可以超時,並且超時的時間不能太長,同時也不要過早重傳。規則[1][3][4.3]共同說明了只要還有數據包沒被確認,那麼定時器一定會是開啟著的(這樣滿足 沒被確認的包必須可以超時的原則)。規則[4.2]說明定時器的超時值是有上限的(滿足 超時的時間不能太長 )。規則[3]說明,在一個ACK到來後重置定時器可以保護後發的數據不被過早重傳;因為一個ACK到來了,說明後續的ACK很可能會依次到來,也就是說丟失的可能性並不大。規則[4.2]也是在一定程度上避免過早重傳,因為,在出現定時器超時後,有可能是網路出現擁塞了,這個時候應該延長定時器,避免出現大量的重傳進一步加劇網路的擁塞。

【2】TCP的重傳機制

通過上面我們可以知道,TCP的重傳是由超時觸發的,這會引發一個重傳選擇問題,假設TCP發送端連續發了1、2、3、4、5、6、7、8、9、10共10包,其中4、6、8這3個包全丟失了,由於TCP的ACK是確認最後連續收到序號,這樣發送端只能收到3號包的ACK,這樣在TIME_OUT的時候,發送端就面臨下面兩個重傳選擇:

[1].僅重傳4號包n[2].重傳3號後面所有的包,也就是重傳4~10號包n

對於,上面兩個選擇的優缺點都比較明顯。方案[1],優點:按需重傳,能夠最大程度節省帶寬。缺點:重傳會比較慢,因為重傳4號包後,需要等下一個超時才會重傳6號包。方案[2],優點:重傳較快,數據能夠較快交付給接收端。缺點:重傳了很多不必要重傳的包,浪費帶寬,在出現丟包的時候,一般是網路擁塞,大量的重傳又可能進一步加劇擁塞。

上面的問題是由於單純以時間驅動來進行重傳的,都必須等待一個超時時間,不能快速對當前網路狀況做出響應,如果加入以數據驅動呢?TCP引入了一種叫Fast Retransmit(快速重傳 )的演算法,就是在連續收到3次相同確認號的ACK,那麼就進行重傳。這個演算法基於這麼一個假設,連續收到3個相同的ACK,那麼說明當前的網路狀況變好了,可以重傳丟失的包了。

快速重傳解決了timeout的問題,但是沒解決重傳一個還是重傳多個的問題。出現難以決定是否重傳多個包問題的根源在於,發送端不知道那些非連續序號的包已經到達接收端了,但是接收端是知道的,如果接收端告訴一下發送端不就可以解決這個問題嗎?於是,RFC2018提出了Selective Acknowledgment (SACK,選擇確認)機制,SACK是TCP的擴展選項,包括(1)SACK允許選項(Kind=4,Length=2,選項只允許在有SYN標誌的TCP包中),(2)SACK信息選項(Kind=5,Length)。一個SACK的例子如下圖,紅框說明:接收端收到了0-5500,8000-8500,7000-7500,6000-6500的數據了,這樣發送端就可以選擇重傳丟失的5500-6000,6500-7000,7500-8000的包。

SACK依靠接收端的接收情況反饋,解決了重傳風暴問題,這樣夠了嗎?接收端能不能反饋更多的信息呢?顯然是可以的,於是,RFC2883對對SACK進行了擴展,提出了D-SACK,也就是利用第一塊SACK數據中描述重複接收的不連續數據塊的序列號參數,其他SACK數據則描述其他正常接收到的不連續數據。這樣發送方利用第一塊SACK,可以發現數據段被網路複製、錯誤重傳、ACK丟失引起的重傳、重傳超時等異常的網路狀況,使得發送端能更好調整自己的重傳策略。D-SACK,有幾個優點:

1)發送端可以判斷出,是發包丟失了,還是接收端的ACK丟失了。(發送方,重傳了一個包,發現並沒有D-SACK那個包,那麼就是發送的數據包丟了;否則就是接收端的ACK丟了,或者是發送的包延遲到達了)nn2)發送端可以判斷自己的RTO是不是有點小了,導致過早重傳(如果收到比較多的D-SACK就該懷疑是RTO小了)。nn3)發送端可以判斷自己的數據包是不是被複制了。(如果明明沒有重傳該數據包,但是收到該數據包的D-SACK)nn4)發送端可以判斷目前網路上是不是出現了有些包被delay了,也就是出現先發的包卻後到了。n

9. 疑症(9)TCP的流量控制

我們知道TCP的窗口(window)是一個16bit位欄位,它代表的是窗口的位元組容量,也就是TCP的標準窗口最大為2^16-1=65535個位元組。另外在TCP的選項欄位中還包含了一個TCP窗口擴大因子,option-kind為3,option-length為3個位元組,option-data取值範圍0-14。窗口擴大因子用來擴大TCP窗口,可把原來16bit的窗口,擴大為31bit。這個窗口是接收端告訴發送端自己還有多少緩衝區可以接收數據。於是發送端就可以根據這個接收端的處理能力來發送數據,而不會導致接收端處理不過來。也就是,發送端是根據接收端通知的窗口大小來調整自己的發送速率的,以達到端到端的流量控制。儘管流量控制看起來簡單明了,就是發送端根據接收端的限制來控制自己的發送就好了,但是細心的同學還是會有些疑問的。

1)發送端是怎麼做到比較方便知道自己哪些包可以發,哪些包不能發呢?nn2)如果接收端通知一個零窗口給發送端,這個時候發送端還能不能發送數據呢?如果不發數據,那一直等接收埠通知一個非0窗口嗎,如果接收端一直不通知呢?nn3)如果接收端處理能力很慢,這樣接收端的窗口很快被填滿,然後接收處理完幾個位元組,騰出幾個位元組的窗口後,通知發送端,這個時候發送端馬上就發送幾個位元組給接收端嗎?發送的話會不會太浪費了,就像一艘萬噸油輪只裝上幾斤的油就開去目的地一樣。對於發送端產生數據的能力很弱也一樣,如果發送端慢吞吞產生幾個位元組的數據要發送,這個時候該不該立即發送呢?還是累積多點在發送?n

【1】.疑問1)的解決:

發送方要知道那些可以發,哪些不可以發,一個簡明的方案就是按照接收方的窗口通告,發送方維護一個一樣大小的發送窗口就可以了,在窗口內的可以發,窗口外的不可以發,窗口在發送序列上不斷後移,這就是TCP中的滑動窗口。如下圖所示,對於TCP發送端其發送緩存內的數據都可以分為4類

[1]-已經發送並得到接收端ACK的; n[2]-已經發送但還未收到接收端ACK的; n[3]-未發送但允許發送的(接收方還有空間); n[4]-未發送且不允許發送(接收方沒空間了)。n其中,[2]和[3]兩部分合起來稱之為發送窗口。n

下面兩圖演示的窗口的滑動情況,收到36的ACK後,窗口向後滑動5個byte。

【2】.疑問2)的解決

由問題1)我們知道,發送端的發送窗口是由接收端控制的。下圖,展示了一個發送端是怎麼受接收端控制的。

由上圖我們知道,當接收端通知一個zero窗口的時候,發送端的發送窗口也變成了0,也就是發送端不能發數據了。如果發送端一直等待,直到接收端通知一個非零窗口在發數據的話,這似乎太受限於接收端,如果接收端一直不通知新的窗口呢?顯然發送端不能幹等,起碼有一個主動探測的機制。為解決0窗口的問題,TCP使用了Zero Window Probe技術,縮寫為ZWP。

發送端在窗口變成0後,會發ZWP的包給接收方,來探測目前接收端的窗口大小,一般這個值會設置成3次,每次大約30-60秒(不同的實現可能會不一樣)。如果3次過後還是0的話,有的TCP實現就會發RST掉這個連接。正如有人的地方就會有商機,那麼有等待的地方就很有可能出現DDoS攻擊點。攻擊者可以在和Server建立好連接後,就向Server通告一個0窗口,然後Server端就只能等待進行ZWP,於是攻擊者會並發大量的這樣的請求,把Server端的資源耗盡。

【3】疑問點3)的解決

疑點3)本質就是一個避免發送大量小包的問題。造成這個問題原因有二:1)接收端一直在通知一個小的窗口; 2)發送端本身問題,一直在發送小包。這個問題,TCP中有個術語叫Silly Window Syndrome(糊塗窗口綜合症)。解決這個問題的思路有兩,1)接收端不通知小窗口,2)發送端積累一下數據在發送。

思路1)是在接收端解決這個問題,David D Clark』s 方案,如果收到的數據導致window size小於某個值,就ACK一個0窗口,這就阻止發送端在發數據過來。等到接收端處理了一些數據後windows size 大於等於了MSS,或者buffer有一半為空,就可以通告一個非0窗口。思路2)是在發送端解決這個問題,有個著名的Nagle』s algorithm。Nagle 演算法的規則

[1]如果包長度達到 MSS ,則允許發送;n [2]如果該包含有 FIN ,則允許發送;n [3]設置了 TCP_NODELAY 選項,則允許發送;n [4]設置 TCP_CORK 選項時,若所有發出去的小數據包(包長度小於 MSS )均被確認,則允許發送;n [5]上述條件都未滿足,但發生了超時(一般為 200ms ),則立即發送。n

規則[4]指出TCP連接上最多只能有一個未被確認的小數據包。從規則[4]可以看出Nagle演算法並不禁止發送小的數據包(超時時間內),而是避免發送大量小的數據包。由於Nagle演算法是依賴ACK的,如果ACK很快的話,也會出現一直發小包的情況,造成網路利用率低。TCP_CORK選項則是禁止發送小的數據包(超時時間內),設置該選項後,TCP會儘力把小數據包拼接成一個大的數據包(一個 MTU)再發送出去,當然也不會一直等,發生了超時(一般為 200ms ),也立即發送。Nagle 演算法和CP_CORK 選項提高了網路的利用率,但是增加是延時。從規則[3]可以看出,設置TCP_NODELAY 選項,就是完全禁用Nagle 演算法了。

這裡要說一個小插曲,Nagle演算法和延遲確認(Delayed Acknoledgement)一起,當出現( write-write-read)的時候會引發一個40ms的延時問題,這個問題在HTTP svr中體現的比較明顯。場景如下:

客戶端在請求下載HTTP svr中的一個小文件,一般情況下,HTTP svr都是先發送HTTP響應頭部,然後在發送HTTP響應BODY(特別是比較多的實現在發送文件的實施採用的是sendfile系統調用,這就出現write-write-read模式了)。當發送頭部的時候,由於頭部較小,於是形成一個小的TCP包發送到客戶端,這個時候開始發送body,由於body也較小,這樣還是形成一個小的TCP數據包,根據Nagle演算法,HTTP svr已經發送一個小的數據包了,在收到第一個小包的ACK後或等待200ms超時後才能在發小包,HTTP svr不能發送這個body小TCP包;

客戶端收到http響應頭後,由於這是一個小的TCP包,於是客戶端開啟延遲確認,客戶端在等待Svr的第二個包來在一起確認或等待一個超時(一般是40ms)在發送ACK包;這樣就出現了你等我、然而我也在等你的死鎖狀態,於是出現最多的情況是客戶端等待一個40ms的超時,然後發送ACK給HTTP svr,HTTP svr收到ACK包後在發送body部分。大家在測HTTP svr的時候就要留意這個問題了。

10. 疑症(10)TCP的擁塞控制

談到擁塞控制,就要先談談擁塞的因素和本質。本質上,網路上擁塞的原因就是大家都想獨享整個網路資源,對於TCP,端到端的流量控制必然會導致網路擁堵。這是因為TCP只看到對端的接收空間的大小,而無法知道鏈路上的容量,只要雙方的處理能力很強,那麼就可以以很大的速率發包,於是鏈路很快出現擁堵,進而引起大量的丟包,丟包又引發發送端的重傳風暴,進一步加劇鏈路的擁塞。另外一個擁塞的因素是鏈路上的轉發節點,例如路由器,再好的路由器只要接入網路,總是會拉低網路的總帶寬,如果在路由器節點上出現處理瓶頸,那麼就很容易出現擁塞。由於TCP看不到網路的狀況,那麼擁塞控制是必須的並且需要採用試探性的方式來控制擁塞,於是擁塞控制要完成兩個任務:[1]公平性;[2]擁塞過後的恢復。

TCP發展到現在,擁塞控制方面的演算法很多,其中Reno是目前應用最廣泛且較為成熟的演算法,下面著重介紹一下Reno演算法(RFC5681)。介紹該演算法前,首先介紹一個概念duplicate acknowledgment(冗餘ACK、重複ACK)一般情況下一個ACK被稱為冗餘ACK,要同時滿足下面幾個條件(對於SACK,那麼根據SACK的一些信息來進一步判斷)

[1] 接收ACK的那端已經發出了一些還沒被ACK的數據包n[2] 該ACK沒有捎帶datan[3] 該ACK的SYN和FIN位都是off的,也就是既不是SYN包的ACK也不是FIN包的ACK。n[4] 該ACK的確認號等於接收ACK那端已經收到的ACK的最大確認號n[5] 該ACK通知的窗口等接收該ACK的那端上一個收到的ACK的窗口n

Reno演算法包含4個部分:[1]慢熱啟動演算法 – Slow Start; [2]擁塞避免演算法 – Congestion Avoidance; [3]快速重傳 - Fast Retransimit; [4]快速恢復演算法 – Fast Recovery。TCP的擁塞控制主要原理依賴於一個擁塞窗口(cwnd)來控制,根據前面的討論,我們知道有一個接收端通告的接收窗口(rwnd)用於流量控制;加上擁塞控制後,發送端真正的發送窗口=min(rwnd, cwnd)。關於cwnd的單位,在TCP中是以位元組來做單位的,我們假設TCP每次傳輸都是按照MSS大小來發送數據,因此你可以認為cwnd按照數據包個數來做單位也可以理解,下面如果沒有特別說明是位元組,那麼cwnd增加1也就是相當於位元組數增加1個MSS大小。

【1】慢熱啟動演算法 – Slow Start

慢啟動體現了一個試探的過程,剛接入網路的時候先發包慢點,探測一下網路情況,然後在慢慢提速。不要一上來就拚命發包,這樣很容易造成鏈路的擁堵,出現擁堵了在想到要降速來緩解擁堵這就有點成本高了,畢竟無數的先例告誡我們先污染後治理的成本是很高的。慢啟動的演算法如下(cwnd全稱Congestion Window):

1)連接建好的開始先初始化cwnd = N,表明可以傳N個MSS大小的數據。n2)每當收到一個ACK,++cwnd; 呈線性上升n3)每當過了一個RTT,cwnd = cwnd*2; 呈指數讓升n4)還有一個慢啟動門限ssthresh(slow start threshold),是一個上限,當cwnd >= ssthresh時,就會進入"擁塞避免演算法 - Congestion Avoidance"n

根據RFC5681,如果MSS > 2190 bytes,則N = 2;如果MSS < 1095 bytes,則N = 4;如果2190 bytes >= MSS >= 1095 bytes,則N = 3;一篇Google的論文《An Argument for Increasing TCP』s Initial Congestion Window》建議把cwnd 初始化成了 10個MSS。Linux 3.0後採用了這篇論文的建議。

【2】擁塞避免演算法 – Congestion Avoidance

慢啟動的時候說過,cwnd是指數快速增長的,但是增長是有個門限ssthresh(一般來說大多數的實現ssthresh的值是65535位元組)的,到達門限後進入擁塞避免階段。在進入擁塞避免階段後,cwnd值變化演算法如下:

1)每收到一個ACK,調整cwnd 為 (cwnd + 1/cwnd) * MSS個位元組n 2)每經過一個RTT的時長,cwnd增加1個MSS大小。n

TCP是看不到網路的整體狀況的,那麼TCP認為網路擁塞的主要依據是它重傳了報文段。前面我們說過TCP的重傳分兩種情況:

1)出現RTO超時,重傳數據包。這種情況下,TCP就認為出現擁塞的可能性就很大,於是它反應非常強烈n [1] 調整門限ssthresh的值為當前cwnd值的1/2。n [2] reset自己的cwnd值為1n [3] 然後重新進入慢啟動過程。nn2)在RTO超時前,收到3個duplicate ACK進行重傳數據包。這種情況下,收到3個冗餘ACK後說明確實有中間的分段丟失,然而後面的分段確實到達了接收端,因為這樣才會發送冗餘ACK,這一般是路由器故障或者輕度擁塞或者其它不太嚴重的原因引起的,因此此時擁塞窗口縮小的幅度就不能太大,此時進入快速重傳。n

【3】快速重傳 - Fast Retransimit 做的事情有:

1) 調整門限ssthresh的值為當前cwnd值的1/2。n2) 將cwnd值設置為新的ssthresh的值n3) 重新進入擁塞避免階段。n

在快速重傳的時候,一般網路只是輕微擁堵,在進入擁塞避免後,cwnd恢復的比較慢。針對這個,「快速恢復」演算法被添加進來,當收到3個冗餘ACK時,TCP最後的[3]步驟進入的不是擁塞避免階段,而是快速恢復階段。

【4】快速恢復演算法 – Fast Recovery :

快速恢復的思想是「數據包守恆」原則,即帶寬不變的情況下,在網路同一時刻能容納數據包數量是恆定的。當「老」數據包離開了網路後,就能向網路中發送一個「新」的數據包。既然已經收到了3個冗餘ACK,說明有三個數據分段已經到達了接收端,既然三個分段已經離開了網路,那麼就是說可以在發送3個分段了。於是只要發送方收到一個冗餘的ACK,於是cwnd加1個MSS。快速恢復步驟如下(在進入快速恢復前,cwnd 和 sshthresh已被更新為:sshthresh = cwnd /2,cwnd = sshthresh):

1)把cwnd設置為ssthresh的值加3,重傳Duplicated ACKs指定的數據包nn2)如果再收到 duplicated Acks,那麼cwnd = cwnd +1nn3)如果收到新的ACK,而非duplicated Ack,那麼將cwnd重新設置為【3】中1)的sshthresh的值。然後進入擁塞避免狀態。n

細心的同學可能會發現快速恢復有個比較明顯的缺陷就是:它依賴於3個冗餘ACK,並假定很多情況下,3個冗餘的ACK只代表丟失一個包。但是3個冗餘ACK也很有可能是丟失了很多個包,快速恢復只是重傳了一個包,然後其他丟失的包就只能等待到RTO超時了。超時會導致ssthresh減半,並且退出了Fast Recovery階段,多個超時會導致TCP傳輸速率呈級數下降。出現這個問題的主要原因是過早退出了Fast Recovery階段。為解決這個問題,提出了New Reno演算法,該演算法是在沒有SACK的支持下改進Fast Recovery演算法(SACK改變TCP的確認機制,把亂序等信息會全部告訴對方,SACK本身攜帶的信息就可以使得發送方有足夠的信息來知道需要重傳哪些包,而不需要重傳哪些包),具體改進如下:

1)發送端收到3個冗餘ACK後,重傳冗餘ACK指示可能丟失的那個包segment1,如果segment1的ACK通告接收端已經收到發送端的全部已經發出的數據的話,那麼就是只丟失一個包,如果沒有,那麼就是有多個包丟失了。nn2)發送端根據segment1的ACK判斷出有多個包丟失,那麼發送端繼續重傳窗口內未被ACK的第一個包,直到sliding window內發出去的包全被ACK了,才真正退出Fast Recovery階段。n

我們可以看到,擁塞控制在擁塞避免階段,cwnd是加性增加的,在判斷出現擁塞的時候採取的是指數遞減。為什麼要這樣做呢?這是出於公平性的原則,擁塞窗口的增加受惠的只是自己,而擁塞窗口減少受益的是大家。這種指數遞減的方式實現了公平性,一旦出現丟包,那麼立即減半退避,可以給其他新建的連接騰出足夠的帶寬空間,從而保證整個的公平性。

至此,TCP的疑難雜症基本介紹完畢了,總的來說TCP是一個有連接的、可靠的、帶流量控制和擁塞控制的端到端的協議。TCP的發送端能發多少數據,由發送端的發送窗口決定(當然發送窗口又被接收端的接收窗口、發送端的擁塞窗口限制)的,那麼一個TCP連接的傳輸穩定狀態應該體現在發送端的發送窗口的穩定狀態上,這樣的話,TCP的發送窗口有哪些穩定狀態呢?TCP的發送窗口穩定狀態主要有上面三種穩定狀態:

【1】接收端擁有大窗口的經典鋸齒狀

大多數情況下都是處於這樣的穩定狀態,這是因為,一般情況下機器的處理速度就是比較快,這樣TCP的接收端都是擁有較大的窗口,這時發送端的發送窗口就完全由其擁塞窗口cwnd決定了;網路上擁有成千上萬的TCP連接,它們在相互爭用網路帶寬,TCP的流量控制使得它想要獨享整個網路,而擁塞控制又限制其必要時做出犧牲來體現公平性。於是在傳輸穩定的時候TCP發送端呈現出下面過程的反覆

[1]用慢啟動或者擁塞避免方式不斷增加其擁塞窗口,直到丟包的發生;n[2]然後將發送窗口將下降到1或者下降一半,進入慢啟動或者擁塞避免階段(要看是由於超時丟包還是由於冗餘ACK丟包);過程如下圖:n

【2】接收端擁有小窗口的直線狀態

這種情況下是接收端非常慢速,接收窗口一直很小,這樣發送窗口就完全有接收窗口決定了。由於發送窗口小,發送數據少,網路就不會出現擁塞了,於是發送窗口就一直穩定的等於那個較小的接收窗口,呈直線狀態。

【3】兩個直連網路端點間的滿載狀態下的直線狀態

這種情況下,Peer兩端直連,並且只有位於一個TCP連接,那麼這個連接將獨享網路帶寬,這裡不存在擁塞問題,在他們處理能力足夠的情況下,TCP的流量控制使得他們能夠跑慢整個網路帶寬。

通過上面我們知道,在TCP傳輸穩定的時候,各個TCP連接會均分網路帶寬的。相信大家學生時代經常會發生這樣的場景,自己在看視頻的時候突然出現視頻卡頓,於是就大叫起來,哪個開了迅雷,趕緊給我停了。其實簡單的下載加速就是開啟多個TCP連接來分段下載就達到加速的效果,假設宿舍的帶寬是1000K/s,一開始兩個在看視頻,每人平均網速是500k/s,這速度看起視頻來那叫一個順溜。突然其中一個同學打打開迅雷開著99個TCP連接在下載愛情動作片,這個時候平均下來你能分到的帶寬就剩下10k/s,這網速下你的視頻還不卡成幻燈片。在通信鏈路帶寬固定(假設為W),多人公用一個網路帶寬的情況下,利用TCP協議的擁塞控制的公平性,多開幾個TCP連接就能多分到一些帶寬(當然要忽略有些用UDP協議帶來的影響),然而不管怎麼最多也就能把整個帶寬搶到,於是在佔滿整個帶寬的情況下,下載一個大小為FS的文件,那麼最快需要的時間是FS/W,難道就沒辦法加速了嗎?

答案是有的,這樣因為網路是網狀的,一個節點是要和很多幾點互聯的,這就存在多個帶寬為W的通信鏈路,如果我們能夠將要下載的文件,一半從A通信鏈路下載,另外一半從B通信鏈路下載,這樣整個下載時間就減半了為FS/(2W),這就是p2p加速。相信大家學生時代在下載愛情動作片的時候也遇到過這種情況,明明外網速度沒這麼快的,自己下載的愛情動作片的速度卻達到幾M/s,那是因為,你的左後或右後的宿友在幫你加速中。我們都知道P2P模式下載會快,並且越多人下載就越快,那麼問題來了,P2P下載加速理論上的加速比是多少呢?

11.附加題1:P2P理論上的加速比

傳統的C/S模式傳輸文件,在跑滿Client帶寬的情況下傳輸一個文件需要耗時FS/BW,如果有n個客戶端需要下載文件,那麼總耗時是n(FS/BW),當然啦,這並不一定是串列傳輸,可以並行來傳輸的,這樣總耗時也就是FS/BW了,但是這需要伺服器的帶寬是n個client帶寬的總和nBW。C/S模式一個明顯的缺點是服務要傳輸一個文件n次,這樣對伺服器的性能和帶寬頻來比較大的壓力,我可以換下思路,伺服器將文件傳給其中一個Client後,讓這些互聯的Client自己來交互那個文件,那伺服器的壓力就減少很多了。這就是P2P網路的好處,P2P利用各個節點間的互聯,提倡「人人為我,我為人人」。

知道P2P傳輸的好處後,我們來談下理論上的最大加速比,為了簡化討論,一個簡單的網路拓撲圖如下,有4個相互互聯的節點,並且每個節點間的網路帶寬是BW,傳輸一個大小為FS的文件最快的時間是多少呢?假設節點N1有個大小為FS的文件需要傳輸給N2,N3,N4節點,一種簡單的方式就是:節點N1同時將文件傳輸給節點N2,N3,N4耗時FS/BW,這樣大家都擁有文件FS了。大家可以看出,整個過程只有節點1在發送文件,其他節點都是在接收,完全違反了P2P的「人人為我,我為人人」的宗旨。那怎麼才能讓大家都做出貢獻了呢?解決方案是切割文件。

[1]首先,節點N1 文件分成3個片段FS2,FS3,FS4 ,接著將FS2發送給N2,FS3發送給N3,FS4發送給N4,耗時FS/(3*BW);nn [2]然後,N2,N3,N4執行「人人為我,我為人人」的精神,將自己擁有的F2,F3,F4分別發給沒有的其他的節點,這樣耗時FS/(3*BW)完成交換。n

於是總耗時為2FS/(3BW)完成了文件FS的傳輸,可以看出耗時減少為原來的2/3了,如果有n個節點,那麼時間就是原來的2/(n-1),也就是加速比是2/(n-1),這就是加速的理論上限了嗎?還沒發揮最多能量的,相信大家已經看到分割文件的好處了,上面的文件分割粒度還是有點大,以至於,在第二階段[2]傳輸過程中,節點N1無所事事。為了最大化發揮大家的作用,我們需要將FS2,FS3,FS4在進行分割,假設將它們都均分為K等份,這樣就有FS21,FS22…FS2K、FS31,FS32…FS3K、FS41,FS42…FS4K,一共3K個分段。於是下面就開始進行加速分發:

[1]節點N1將分段FS21,FS31,FS41分別發送給N2,N3,N4節點。耗時,FS/(3K*BW)nn[2]節點N1將分段FS22,FS32,FS42分別發送給N2,N3,N4節點,同時節點N2,N3,N4將階段[1]收到的分段相互發給沒有的節點。耗時,FS/(3K*BW)n。。。。。。nn[K]節點N1將分段FS2K,FS3K,FS4K分別發送給N2,N3,N4節點,同時節點N2,N3,N4將階段[K-1]收到的分段相互發給沒有的節點。耗時,FS/(3K*BW)nn [K+1]節點N2,N3,N4將階段[K]收到的分段相互發給沒有的節點。耗時,FS/(3K*BW)n

於是總的耗時為(K+1) (FS/(3KBW)) = FS/(3BW) + FS/(3KBW),當K趨於無窮大的時候,文件進行無限細分的時候,耗時變成了FS/(3*BW),也就是當節點是n+1的時候,加速比是n。這就是理論上的最大加速比了,最大加速比是P2P網路節點個數減1。

12.附加題2:系統調用listen() 的backlog參數指的是什麼

要說明backlog參數的含義,首先需要說一下Linux的協議棧維護的TCP連接的兩個連接隊列:[1]SYN半連接隊列;[2]accept連接隊列

[1]SYN半連接隊列:Server端收到Client的SYN包並回復SYN,ACK包後,該連接的信息就會被移到一個隊列,這個隊列就是SYN半連接隊列(此時TCP連接處於 非同步狀態 )nn [2]accept連接隊列:Server端收到SYN,ACK包的ACK包後,就會將連接信息從[1]中的隊列移到另外一個隊列,這個隊列就是accept連接隊列(這個時候TCP連接已經建立,三次握手完成了)n用戶進程調用accept()系統調用後,該連接信息就會從[2]中的隊列中移走。n

相信不少同學就backlog的具體含義進行爭論過,有些認為backlog指的是[1]和[2]兩個隊列的和。而有些則認為是backlog指的是[2]的大小。其實,這兩個說法都對,在linux kernel 2.2之前backlog指的是[1]和[2]兩個隊列的和。而2.2以後,就指的是[2]的大小,那麼在kernel 2.2以後,[1]的大小怎麼確定的呢?兩個隊列的作用分別是什麼呢?

【1】SYN半連接隊列的作用

對於SYN半連接隊列的大小是由(/proc/sys/net/ipv4/tcp_max_syn_backlog)這個內核參數控制的,有些內核似乎也受listen的backlog參數影響,取得是兩個值的最小值。當這個隊列滿了,Server會丟棄新來的SYN包,而Client端在多次重發SYN包得不到響應而返回(connection time out)錯誤。但是,當Server端開啟了syncookies,那麼SYN半連接隊列就沒有邏輯上的最大值了,並且/proc/sys/net/ipv4/tcp_max_syn_backlog設置的值也會被忽略。

【2】accept連接隊列

accept連接隊列的大小是由backlog參數和(/proc/sys/net/core/somaxconn)內核參數共同決定,取值為兩個中的最小值。當accept連接隊列滿了,協議棧的行為根據(/proc/sys/net/ipv4/tcp_abort_on_overflow)內核參數而定。 如果tcp_abort_on_overflow=1,server在收到SYN_ACK的ACK包後,協議棧會丟棄該連接並回復RST包給對端,這個是Client會出現(connection reset by peer)錯誤。如果tcp_abort_on_overflow=0,server在收到SYN_ACK的ACK包後,直接丟棄該ACK包。這個時候Client認為連接已經建立了,一直在等Server的數據,直到超時出現read timeout錯誤。

loadrunner_http長連接設置

參考資料

Netfilter,iptables/OpenVPN/TCP guard:-(

TCP 的那些事兒(上) | | 酷 殼 - CoolShell

coolshell.cn/articles/1

TCP Message (Segment) Format

騰雲閣 - 騰訊雲

推薦閱讀:

能不能不使用Socket進行網路通信?
如何解決長城寬頻主動斷開tcp長連接的問題?
epoll非阻塞伺服器,在20k並發測試結束產生大量establish狀態假連接,可能原因?
用 wireshark抓包工具能做到哪些有趣的事情?
為什麼 ssh root@163.com 或者 ssh root@zhihu.com 都沒有反應?

TAG:TCP | 计算机网络 | 云计算 |