從TCP三次握手提及–淺析TCP協議中的疑難雜症(2)

版權聲明:本文由黃日成原創文章,轉載請註明出處: 
文章原文連接:https://www.qcloud.com/community/article/108html

來源:騰雲閣 https://www.qcloud.com/communitylinux

 

在」從TCP三次握手提及–淺析TCP協議中的疑難雜症(1)「文章中,咱們提到第6個疑問:TCP的頭號疼症TIME_WAIT狀態,下面咱們繼續這個問題的解答算法

  • TIME_WAIT的快速回收和重用shell

    • TIME_WAIT快速回收。
      linux下開啓TIME_WAIT快速回收須要同時打開tcp_tw_recycle和tcp_timestamps(默認打開)兩選項。Linux下快速回收的時間爲3.5 * RTO(Retransmission Timeout),而一個RTO時間爲200ms至120s。開啓快速回收TIME_WAIT,可能會帶來(問題1、)中說的三點危險,爲了不這些危險,要求同時知足如下三種狀況的新鏈接要被拒絕掉。

      [1]. 來自同一個對端Peer的TCP包攜帶了時間戳。
      [2].以前同一臺peer機器(僅僅識別IP地址,由於鏈接被快速釋放了,沒了端口信息)的某個TCP數據在MSL秒以內到過本Server
      [3].Peer機器新鏈接的時間戳小於peer機器上次TCP到來時的時間戳,且差值大於重放窗口戳(TCP_PAWS_WINDOW)
      初看起來正常的數據包同時知足下面3條几乎不可能, 由於機器的時間戳不可能倒流的,出現上述的3點均知足時,必定是老的重複數據包又回來了,丟棄老的SYN包是正常的。到此,彷佛啓用快速回收就能很大程度緩解TIME_WAIT帶來的問題。可是,這裏忽略了一個東西就是NAT。。。在一個NAT後面的全部Peer機器在Server看來都是一個機器,NAT後面的那麼多Peer機器的系統時間戳極可能不一致,有些快,有些慢。這樣,在Server關閉了與系統時間戳快的Client的鏈接後,在這個鏈接進入快速回收的時候,同一NAT後面的系統時間戳慢的Client向Server發起鏈接,這就頗有可能同時知足上面的三種狀況,形成該鏈接被Server拒絕掉。因此,在是否開啓tcp_tw_recycle須要慎重考慮了windows

    • TIME_WAIT重用
      linux上比較完美的實現了TIME_WAIT重用問題。只要知足下面兩點中的一點,一個TW狀態的四元組(即一個socket鏈接)能夠從新被新到來的SYN鏈接使用緩存

      [1]. 新鏈接SYN告知的初始序列號比TIME_WAIT老鏈接的末序列號大
      [2]. 若是開啓了tcp_timestamps,而且新到來的鏈接的時間戳比老鏈接的時間戳大安全

      要同時開啓tcp_tw_reuse選項和tcp_timestamps 選項才能夠開啓TIME_WAIT重用,還有一個條件是:重用TIME_WAIT的條件是收到最後一個包後超過1s。細心的同窗可能發現TIME_WAIT重用對Server端來講並沒解決大量TIME_WAIT形成的資源消耗的問題,由於無論TIME_WAIT鏈接是否被重用,它依舊佔用着系統資源。即使如此,TIME_WAIT重用仍是有些用處的,它解決了整機範圍拒絕接入的問題,雖然通常一個單獨的Client是不可能在MSL內用同一個端口鏈接同一個服務的,可是若是Client作了bind端口那就是同個端口了。時間戳重用TIME_WAIT鏈接的機制的前提是IP地址惟一性,得出新請求發起自同一臺機器,可是若是是NAT環境下就不能這樣保證了,因而在NAT環境下,TIME_WAIT重用仍是有風險的。服務器

      有些同窗可能會混淆tcp_tw_reuse和SO_REUSEADDR 選項,認爲是相關的一個東西,其實他們是兩個徹底不一樣的東西,能夠說兩個半毛錢關係都沒。tcp_tw_reuse是內核選項,而SO_REUSEADDR用戶態的選項,使用SO_REUSEADDR是告訴內核,若是端口忙,但TCP狀態位於 TIME_WAIT ,能夠重用端口。若是端口忙,而TCP狀態位於其餘狀態,重用端口時依舊獲得一個錯誤信息, 指明Address already in use」。若是你的服務程序中止後想當即重啓,而新套接字依舊使用同一端口,此時 SO_REUSEADDR 選項很是有用。可是,使用這個選項就會有(問題2、)中說的三點危險,雖然發生的機率不大。cookie

  • 清掉TIME_WAIT的奇技怪巧
    能夠用下面兩種方式控制服務器的TIME_WAIT數量網絡

    • 修改tcp_max_tw_buckets
      tcp_max_tw_buckets 控制併發的TIME_WAIT的數量,默認值是180000。若是超過默認值,內核會把多的TIME_WAIT鏈接清掉,而後在日誌裏打一個警告。官網文檔說這個選項只是爲了阻止一些簡單的DoS攻擊,日常不要人爲的下降它。
    • 利用RST包從外部清掉TIME_WAIT連接
      根據TCP規範,收到任何的發送到未偵聽端口、已經關閉的鏈接的數據包、鏈接處於任何非同步狀態(LISTEN, SYS-SENT, SYN-RECEIVED)而且收到的包的ACK在窗口外,或者安全層不匹配,都要回執以RST響應(而收到滑動窗口外的序列號的數據包,都要丟棄這個數據包,並回復一個ACK包),內核收到RST將會產生一個錯誤並終止該鏈接。咱們能夠利用RST包來終止掉處於TIME_WAIT狀態的鏈接,其實這就是所謂的RST攻擊了。爲了描述方便:假設Client和Server有個鏈接Connect1,Server主動關閉鏈接並進入了TIME_WAIT狀態,咱們來描述一下怎麼從外部使得Server的處於 TIME_WAIT狀態的鏈接Connect1提早終止掉。要實現這個RST攻擊,首先咱們要知道Client在Connect1中的端口port1(通常這個端口是隨機的,比較難猜到,這也是RST攻擊較難的一個點),利用IP_TRANSPARENT這個socket選項,它能夠bind不屬於本地的地址,所以能夠從任意機器綁定Client地址以及端口port1,而後向Server發起一個鏈接,Server收到了窗口外的包因而響應一個ACK,這個ACK包會路由到Client處,這個時候99%的可能Client已經釋放鏈接connect1了,這個時候Client收到這個ACK包,會發送一個RST包,server收到RST包而後就釋放鏈接connect1提早終止TIME_WAIT狀態了。提早終止TIME_WAIT狀態是可能會帶來(問題2、)中說的三點危害,具體的危害狀況能夠看下RFC1337。RFC1337中建議,不要用RST過早的結束TIME_WAIT狀態。

至此,上面的疑症都解析完畢,然而細心的同窗會有下面的疑問:

(7)TCP的可靠傳輸是確認號來實現的,那麼TCP的確認機制是怎樣的呢?是收到一個包就立刻確認,仍是能夠稍等一下在確認呢?
(8)假如發送一個包,一直都沒收到確認呢?何時重傳呢?超時機制的怎樣的?
(9)TCP兩端Peer的處理能力不對等的時候,好比發送方處理能力很強,接收方處理能力很弱,這樣發送方是否可以無論接收方死活狂發數據呢?若是不能,流量控制機制的如何的?
(10)TCP是端到端的協議,也就是TCP對端Peer只看到對方,看不到網絡上的其餘點,那麼TCP的兩端怎麼對網絡狀況作出反映呢?發生擁塞的時候,擁塞控制機制是如何的?

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的重傳機制以及重傳的超時計算

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

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

    其中: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) —— 計算平滑RTT
    DevRTT = (1-β)DevRTT + β(|RTT-SRTT|) ——計算平滑RTT和真實的差距(加權移動平均)
    RTO= µ SRTT + ∂ DevRTT —— 神同樣的公式
    (其中:在Linux下,α = 0.125,β = 0.25, μ = 1,∂ = 4 ——這就是算法中的「調得一手好參數」,nobody knows why, it just works…) 最後的這個算法在被用在今天的TCP協議中並工做很是好

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

    [1]. 每一次一個包含數據的包被髮送(包括重發),若是還沒開啓重傳定時器,則開啓它,使得它在RTO秒以後超時(按照當前的RTO值)。
    [2]. 當接收到一個ACK確認一個新的數據, 若是全部的發出數據都被確認了,關閉重傳定時器。
    [3]. 當接收到一個ACK確認一個新的數據,還有數據在傳輸,也就是還有沒被確認的數據,從新啓動重傳定時器,使得它在RTO秒以後超時(按照當前的RTO值)。
    當重傳定時器超時後,依次作下列3件事情:[4.1]. 重傳最先的還沒有被TCP接收方ACK的數據包[4.2]. 從新設置RTO 爲 RTO * 2(「還原定時器」),可是新RTO不該該超過RTO的上限(RTO有個上限值,這個上限值最少爲60s)[4.3]. 重啓重傳定時器。

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

  • TCP的重傳機制
    經過上面咱們能夠知道,TCP的重傳是由超時觸發的,這會引起一個重傳選擇問題,假設TCP發送端連續發了一、二、三、四、五、六、七、八、九、10共10包,其中四、六、8這3個包全丟失了,因爲TCP的ACK是確認最後連續收到序號,這樣發送端只能收到3號包的ACK,這樣在TIME_OUT的時候,發送端就面臨下面兩個重傳選擇:

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

    對於,上面兩個選擇的優缺點都比較明顯。
    方案[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丟了,或者是發送的包延遲到達了)
    2)發送端能夠判斷本身的RTO是否是有點小了,致使過早重傳(若是收到比較多的D-SACK就該懷疑是RTO小了)。
    3)發送端能夠判斷本身的數據包是否是被複制了。(若是明明沒有重傳該數據包,可是收到該數據包的D-SACK)
    4)發送端能夠判斷目前網絡上是否是出現了有些包被delay了,也就是出現先發的包卻後到了。

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)發送端是怎麼作到比較方便知道本身哪些包能夠發,哪些包不能發呢?
2)若是接收端通知一個零窗口給發送端,這個時候發送端還能不能發送數據呢?若是不發數據,那一直等接收端口通知一個非0窗口嗎,若是接收端一直不通知呢?
3)若是接收端處理能力很慢,這樣接收端的窗口很快被填滿,而後接收處理完幾個字節,騰出幾個字節的窗口後,通知發送端,這個時候發送端立刻就發送幾個字節給接收端嗎?發送的話會不會太浪費了,就像一艘萬噸油輪只裝上幾斤的油就開去目的地同樣。對於發送端產生數據的能力很弱也同樣,若是發送端慢吞吞產生幾個字節的數據要發送,這個時候該不應當即發送呢?仍是累積多點在發送?

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

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


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

  • 疑問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)本質就是一個避免發送大量小包的問題。形成這個問題緣由有二: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 ,則容許發送;
    [2]若是該包含有 FIN ,則容許發送;
    [3]設置了 TCP_NODELAY 選項,則容許發送;
    [4]設置 TCP_CORK 選項時,若全部發出去的小數據包(包長度小於 MSS )均被確認,則容許發送;
    [5]上述條件都未知足,但發生了超時(通常爲 200ms ),則當即發送。

    規則[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的數據包
[2] 該ACK沒有捎帶data
[3] 該ACK的SYN和FIN位都是off的,也就是既不是SYN包的ACK也不是FIN包的ACK。
[4] 該ACK的確認號等於接收ACK那端已經收到的ACK的最大確認號
[5] 該ACK通知的窗口等接收該ACK的那端上一個收到的ACK的窗口

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大小。

  • 慢熱啓動算法 – Slow Start
    慢啓動體現了一個試探的過程,剛接入網絡的時候先發包慢點,探測一下網絡狀況,而後在慢慢提速。不要一上來就拼命發包,這樣很容易形成鏈路的擁堵,出現擁堵了在想到要降速來緩解擁堵這就有點成本高了,畢竟無數的先例告誡咱們先污染後治理的成本是很高的。慢啓動的算法以下(cwnd全稱Congestion Window):

    1)鏈接建好的開始先初始化cwnd = N,代表能夠傳N個MSS大小的數據。
    2)每當收到一個ACK,++cwnd; 呈線性上升
    3)每當過了一個RTT,cwnd = cwnd*2; 呈指數讓升
    4)還有一個慢啓動門限ssthresh(slow start threshold),是一個上限,當cwnd >= ssthresh時,就會進入"擁塞避免算法 - Congestion Avoidance"
    根據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後採用了這篇論文的建議。

  • 擁塞避免算法 – Congestion Avoidance
    慢啓動的時候說過,cwnd是指數快速增加的,可是增加是有個門限ssthresh(通常來講大多數的實現ssthresh的值是65535字節)的,到達門限後進入擁塞避免階段。在進入擁塞避免階段後,cwnd值變化算法以下:

    1)每收到一個ACK,調整cwnd 爲 (cwnd + 1/cwnd) * MSS個字節
    2)每通過一個RTT的時長,cwnd增長1個MSS大小。

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

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

  • 快速重傳 - Fast Retransimit 作的事情有:

    1) 調整門限ssthresh的值爲當前cwnd值的1/2。
    2) 將cwnd值設置爲新的ssthresh的值
    3) 從新進入擁塞避免階段。

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

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

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

    細心的同窗可能會發現快速恢復有個比較明顯的缺陷就是:它依賴於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通告接收端已經收到發送端的所有已經發出的數據的話,那麼就是隻丟失一個包,若是沒有,那麼就是有多個包丟失了。
    2)發送端根據segment1的ACK判斷出有多個包丟失,那麼發送端繼續重傳窗口內未被ACK的第一個包,直到sliding window內發出去的包全被ACK了,才真正退出Fast Recovery階段。

    咱們能夠看到,擁塞控制在擁塞避免階段,cwnd是加性增長的,在判斷出現擁塞的時候採起的是指數遞減。爲何要這樣作呢?這是出於公平性的原則,擁塞窗口的增長受惠的只是本身,而擁塞窗口減小受益的是你們。這種指數遞減的方式實現了公平性,一旦出現丟包,那麼當即減半退避,能夠給其餘新建的鏈接騰出足夠的帶寬空間,從而保證整個的公平性。

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

  • 【1】接收端擁有大窗口的經典鋸齒狀
    大多數狀況下都是處於這樣的穩定狀態,這是由於,通常狀況下機器的處理速度就是比較快,這樣TCP的接收端都是擁有較大的窗口,這時發送端的發送窗口就徹底由其擁塞窗口cwnd決定了;網絡上擁有成千上萬的TCP鏈接,它們在相互爭用網絡帶寬,TCP的流量控制使得它想要獨享整個網絡,而擁塞控制又限制其必要時作出犧牲來體現公平性。因而在傳輸穩定的時候TCP發送端呈現出下面過程的反覆

    [1]用慢啓動或者擁塞避免方式不斷增長其擁塞窗口,直到丟包的發生;
    [2]而後將發送窗口將降低到1或者降低一半,進入慢啓動或者擁塞避免階段(要看是因爲超時丟包仍是因爲冗餘ACK丟包);
    過程以下圖:

  • 【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/(3BW);
[2]而後,N2,N3,N4執行「人人爲我,我爲人人」的精神,將本身擁有的F2,F3,F4分別發給沒有的其餘的節點,這樣耗時FS/(3BW)完成交換。

因而總耗時爲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/(3KBW)
[2]節點N1將分段FS22,FS32,FS42分別發送給N2,N3,N4節點,同時節點N2,N3,N4將階段[1]收到的分段相互發給沒有的節點。耗時,FS/(3KBW)
。。。。。。
[K]節點N1將分段FS2K,FS3K,FS4K分別發送給N2,N3,N4節點,同時節點N2,N3,N4將階段[K-1]收到的分段相互發給沒有的節點。耗時,FS/(3KBW)
[K+1]節點N2,N3,N4將階段[K]收到的分段相互發給沒有的節點。耗時,FS/(3KBW)
因而總的耗時爲(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鏈接處於 非同步狀態 )
[2]accept鏈接隊列:Server端收到SYN,ACK包的ACK包後,就會將鏈接信息從[1]中的隊列移到另一個隊列,這個隊列就是accept鏈接隊列(這個時候TCP鏈接已經創建,三次握手完成了)
用戶進程調用accept()系統調用後,該鏈接信息就會從[2]中的隊列中移走。

相信很多同窗就backlog的具體含義進行爭論過,有些認爲backlog指的是[1]和[2]兩個隊列的和。而有些則認爲是backlog指的是[2]的大小。其實,這兩個說法都對,在linux kernel 2.2以前backlog指的是[1]和[2]兩個隊列的和。而2.2之後,就指的是[2]的大小,那麼在kernel 2.2之後,[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設置的值也會被忽略。

  • 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錯誤。

參考資料
http://blog.csdn.net/dog250/article/details/6612496
http://coolshell.cn/articles/11564.html
http://coolshell.cn/articles/11609.html
http://www.tcpipguide.com/free/t_TCPMessageSegmentFormat.htm

文章來源於公衆號:小時光茶社(Tech Teahouse)

相關文章
相關標籤/搜索