TCP核心概念-慢啟動,ssthresh,擁塞避免,公平性的真實含義
本文主要闡述TCP擁塞控制中ssthresh的來歷以及為什麼擁塞避免探測到丟包的時候,ssthresh會被設定為當前視窗的一半。 進入證實內容之前,不得不再次吐槽!目前在網上搜的,任何資料上看的,甚至RFC上,都沒有講明白到底什麼是ssthresh,它的值有什麼講究,幾乎所有的資料都是在說,如果視窗大於ssthresh,那麼就執行線性增窗的擁塞避免階段,否則執行慢啟動...這讓幾乎所有人記住了這個結論,並且在長期被洗滌之後,很多人對這個不知所以然的事實卻表現的不以為然,其實也包括我自己。因此當我明白了ssthresh到底是怎麼一回事的時候,當我知道了丟包後ssthresh的1/2係數與公平性之間的關係的時候,我便迫不及待地想把這些東西分享出來!
TCP資料段填滿端節點之間的網路 假設端系統A和B之間在進行TCP通訊,那麼只要A和B之間存在空間距離,由於光速的傳播時延,一定意味著A和B之間存在一定的容量,可以容納若干的資料段,你可以將其想做是一般的快取,另外,為了更具普遍性,我們認為A和B之間的所有路由器,交換機等中間節點的佇列快取,也包含在A和B的網路快取之中。如下圖所示:
為了達到最高的網路利用率,我們希望A和B之間的快取(包括節點佇列以及網路本身)中完全充盈著TCP的資料段,並且是持續維持。
TCP資料段無間隙地持續流動 如上圖所示,當A,B之間的網路被填滿時,A和B之間一共有N個數據段,傳送端還可以繼續傳送資料嗎?事實上是可以的,因為在網路被填滿之後,傳送端每傳送一個數據段,接收端同時也會消費掉一個數據段,同時發出一個ACK,直到填滿A,B間網路的那個資料段的ACK到達傳送端為止,依照假定,ACK的速率和資料段的速率一致,我們算一下,傳送端一共可以持續傳送2*N個數據段,這2*N個數據段傳送的開始時間點是第1個數據段傳送的時間,結束時間點是第一個資料段的ACK回到傳送端的時間,正好是一個RTT,設傳送速率為r,那麼以下的等式顯而易見: 2*N = r*RTT 過程如下圖所示:
我們還可以看到,前N個段是為了填滿A,B之間的網路,後N個段是在“A,B之間已經滿載的情況下”TCP的ACK clock驅動的pacing。緊隨著這2*N個數據段的是一個新的週期,又是一個RTT內2*N個數據段,這就是理想情況下的情景,資料段充盈著網路,不間斷地源源不斷從傳送端發出,ACK亦不間斷地從接收端返回。
兩個區域(safe & dangerous) 我其實不想現在就把謎底揭穿,但是我也不想賣關子,畢竟現在也不早了。 請注意上圖中的t28這個時間點,在t28之前,A和B之間並不總是被充滿,而在t28之後,卻總是滿的。這意味著,t28之前的資料段是可以緩衝的,即網路中還存在一些空閒的空間可以提供給資料進行緩衝,從而不至於資料被丟棄,然而在t28之後,網路滿載了,我們看到沒有絲毫的空白區域可供資料包緩衝,這意味一旦發生擁塞,資料包必然丟失! 顯而易見,t28之前是安全的,而t28之後則是危險的,這就是safe area和dangerous area的由來!劃分二者的是什麼?正是網路的容量!在上述圖示的例子中,就是4!當Flight的資料段小於4的時候,意味著可以激進的傳輸,當Flight資料一旦越過4,就必須保守傳輸了! 何謂激進?何謂保守?激進就是可以讓視窗最快的速度增加到safe和dangerous的邊界,由於TCP由ACK來驅動,只要收到一個ACK,就意味著通道是暢通的,視窗就可以遞增一個MSS,而所謂的保守就是,必須等待當前視窗的資料全部都被ACK了之後,說明剛才傳送的資料是可以到達對端的,此時才能將視窗增加一個MSS。現在來總結以下兩個區域的增窗方式: safe區域:
dangerous區域:
我們來比對一下視窗在這兩個區域時的特性,如果說安全區域的目標是填滿網路的話,那麼在安全區域我們已知的是網路並未被填滿,此時只要有ACK到來就可以增窗,直到網路被填滿,現在我們來看危險區域,此時我們已知的事實是網路已經滿了,我們希望讓它繼續滿下去,能滿則保持,提高網路的利用率,我們知道TCP的傳送視窗(即可以傳送多少資料)是ACK驅動的,ACK就像時鐘一樣,我們希望資料可以持續傳送以保持網路滿載,就要保證ACK源源不斷地到來,因此在這個階段繼續傳送資料的目標僅僅是為了消除ACK時鐘的空白期,或者稱作停擺期,因為只有這樣,源源不斷的ACK才能驅動資料來源源不斷地被髮送。 回過頭來再看一下增窗過程圖的t20這個時間點,網路被4,5,6,7這四個資料包已經充滿,但是在下一個時刻t21,隨著4被接收,由於沒有到達的ACK,網路被清空了1個MSS,理論上,我們知道最終的視窗肯定就是2*N,此例中,N就是4,在例子中,最終也確實視窗增加到了8,然而在現實中,擁塞隨時都會發生,也就是說在視窗從4增加到8的期間,隨時會發生丟包,這也就意味著視窗可能永遠都增加不到8!那麼我們怎麼可以知道當前的視窗是合理的,然後嘗試繼續增加視窗呢?答案就是前一個視窗的資料全部被確認!這就是擁塞避免的由來,這個過程很慢,並不是設計的問題,而是它必須這麼慢。擁塞避免這個名字非常好,確實在避免! 理解了上面的描述,我們可以給出TCP管道的概念了,然後所有的真相就大白了。 TCP管道的概念 事實上,TCP管道包含兩個部分,按照公式2*N=r*RTT,2*N便是管道的容量,這兩部分的第一部分是網路被填滿之前的容量,只要知道尚未被填滿,盡情地填,使用慢啟動足矣,第二部分是網路被填滿之後的容量,完全按照ACK來驅動,採用擁塞避免方式探測視窗,理想情況下,理論上,這兩部分的容量是相等的。因此,我們可以就可以知道事情的真相了。 問題1:N是什麼? 答:N就是ssthresh! 問題2:為什麼探測到擁塞後,ssthresh要下降為當前視窗的一半? 答:探測到擁塞說明管道的容量為當前視窗C,而C=2*N,因此N=(1/2)*C! 問題3:為什麼在擁塞避免階段要加性增?即AI 答:只有當一窗的資料被確認,才可以確保之前的視窗是有效的,畢竟網路已經塞滿,而ACK有可能不對稱返回,擁塞隨時可能發生。 問題4:為什麼乘性減? 答:見問題2. 問題5:慢啟動階段為什麼可以指數級增窗? 答:因為此時可以確保視窗小於N,即網路還沒有塞滿,即便發生擁塞,也還有富餘的快取可用。 問題6:還有問題嗎? 答:如果一切都如上那般美好,當然就沒有問題了,問題是,ssthresh從來就沒有估計準確過。
回到現實中的TCP TCP在設計之處的願景十分美好,然而現實世界並不是一個友好的世界,不過,TCP本身就是自適應的,它並沒有規定ssthresh的值的大小,甚至都沒有建議,它完全靠在TCP自行發現丟包或者擁塞後,以當前視窗的一半作為ssthresh的當前值,隨著連線的繼續,ssthresh也會動態調整,因為不管現實多麼殘酷,理想中的反饋系統總歸是一個萬物收斂的目標,那就是實際的頻寬總是趨向於ssthresh的2倍的大小,令人驚訝的是,ssthresh就是依靠擁塞避免演算法計算出來的。當然,隨著TCP的發展,這個C=2*N=r*RTT經典公式也歷經了諸多的變化形成了各種變體,比如cubic就不再以2作為ssthresh的係數來計算管道容量。
慢啟動的hystatr優化 我們知道,ssthresh的設定是以丟包作為反饋訊號的,現在問題是,連線剛剛建立的時候,沒有丟包作為反饋訊號的時候,如何來設定ssthresh? 一般而言,預設的實現都是將其設定為一個巨大的值,然後最快的速度歷經一次丟包,然後設定ssthresh為丟包時視窗的一半,然後像ssthresh的2倍緩慢逼近。但是這會帶來問題,由於沒有ssthresh作為閾值限制,用丟包作為代價,太高昂。因此在慢啟動過程中如果可以探測到ssthresh的值,那就可以隨時退出慢啟動狀態了。那麼我們如何來探測呢? 還是C=2*N=r*RTT這個公式,關鍵看看我們怎麼使用它。由於我們只是探測網路被塞滿時的情況,即N的值,因此: N=r*(RTT/2) 我們看看r是什麼?所謂速率其實就是一定的事務量除以做這些事務的時間,如果說我們發出去了N'個數據包,一共用了時間段T,那麼: r=N'/T 代入後得到: N=(N'/T)*(RTT/2) 理想情況下,在畢竟網路容量的時候,N=N',那麼就可以很簡單得到T等於RTT/2的時候,就說明達到了ssthresh,該退出慢啟動了! 那麼如何來實現它呢?由於我們無法單獨探測N個數據段到達接收端並計時,我們可以變相等價使用ACK來計算,以一個視窗的第一個資料段作為計時開始Tstart,每收到一個ACK即更新以下數值: RTTmin:取樣週期內最小的RTT,以最大限度地表示A和B之間的理想往返時延。 Tcurr:當前時間 如果下列條件成立,則可以退出慢啟動了: Tcurr - Tstart >= RTTmin/2 非常簡單易懂。然而現實並不是理想的,大多數情況下,以上的演算法並沒有帶來比較好的效果,為什麼呢?因為整個頻寬不是一個TCP連線獨享的,而是全世界的所有TCP連線甚至包括UDP共享的,因此以上的公式基本上無法表示任何真實的情況,所以實際當中,更傾向於使用RTT來預估網路已經被塞滿。使用RTT來估算網路容量ssthresh更加實際一些,因為它充分考慮了擁塞時的排隊延時,因此在該方法下,退出慢啟動的條件便成了: Tcurr_rtt > RTTmin + fixed_value
以上旨在解決首次慢啟動在還沒有ssthresh值的時候預測ssthresh的方式,其實在此後的任何時候,只要是慢啟動,都可以用以上的演算法來預測當前的ssthresh,而不是說必須要用擁塞演算法給出的ssthresh或者說僅僅是1/2丟包視窗(雖然你已經看到,這個1/2是多麼地合理!)
ssthresh的快速穿越問題 我們知道,慢啟動的時候,增窗速度非常快(基本就是根據ACK的反饋,將資料段翻倍突突出去的),那麼在視窗增加到接近到仍然小於ssthresh的時候,會出現如下圖所示的情況:
然而這在實現中是不易發生的,是什麼限制住它的發生呢?以下幾點: 1.TCP的延遲確認機制最多隻能延遲2個MSS 慢啟動增窗,收到一個ACK遞增1個MSS,即便在使用ABC的時候,也就是說視窗最多隻能超越ssthresh 2個MSS,這是由下述程式碼保證的: if (sysctl_tcp_abc > 1 && tp->bytes_acked >= 2*tp->mss_cache) cnt <<= 1;
2.即便發生了ACK大量丟失,TCP的預設實現也是數ACK的個數,而不是數被ACK的位元組數 3.發生大量ACK丟失又啟用ABC時,見方法1. 4.兩段處理方式 Linux的4.x版本核心中預設使用ACK的位元組數來計數增窗值(ABC方案),在穿越ssthresh的時候,TCP擁塞控制邏輯會將被ACK的位元組數分為兩個部分,ssthresh以下的部分用來計數慢啟動,而ssthresh以上的部分用來計數擁塞避免。綜上所述,下圖總結了ssthresh穿越的情況:
AIMD的公平性收斂 為了簡單起見,我們假設有TCP1和TCP2兩個連線共享一個鏈路,現在看它們是怎麼“收斂到公平”的,下面的圖示清晰顯示了一切:
如果你看不懂這個圖,請自行google。我們可以肯定,在公平線的下方,紅色的減窗線的斜率是恆小於公平線(斜45度角)的斜率的,兩個連結的每一次降窗,其降窗線的斜率都會越來越接近公平線的斜率,即收斂到公平,最終,它將在綠色粗線上震盪,永葆公平(雖然利用率不是那麼高!)。
我們還可以看到,TCP1和TCP2是等比例降窗的,在此例中比例是0.5,它們非得是0.5嗎?非也!只要保持等比例,圖中的註解1就永遠成立,最終的收斂也會永遠成立,不同的僅僅是最終收斂額綠色粗線的長度和範圍!雖然說按照最初的Reno TCP,保持0.5的降窗比例是多麼得合理(見上述推論),然而考慮到現實的複雜情況,比例不再是0.5也是合理的。 現在我們來看看如果TCP1和TCP2的降窗比例不同會怎樣。假設TCP2降窗依然為0.5的比例,而TCP2則小於0.5,那麼上圖將會變成下面的樣子:
我們可以看到,競爭者中降窗比率最小的將會最終搶佔幾乎所有的頻寬,它會將所有的其它連線的頻寬逐漸往左上角擠兌,最終歸零。這麼說來,如果想讓自己的TCP具有侵略性,減少降窗比率是不是就可以了呢?沒這麼這簡單!要知道,我上面的兩幅圖有一個共同的前提,那就是競爭者的RTT是相等的!但是現實中,會這樣嗎??非常難!如果RTT相等,比如它們的源頭和目標都在同一個地點,那麼它們十有八九是合作關係,而不是競爭!爆炸!
那麼,RTT將會是一個十分重要的角色!確實是這樣,實際的TCP在執行中,RTT的波動非常大,這就幾乎將我上面的論述全部推翻了,顯然很令人心碎!然而,上述的分析作為一個理論模型還是有意義的,它起碼讓你理解了TCP的本質行為。至於說實際情況,RTT的波動是一個有意義的訊號,它讓端系統看到了中間路由器交換機的排隊行為,因此會出現RTT所謂的“噪點”,很多人想除掉它們,平滑掉它們,但是這同時也意味著你遮蔽了重要的訊號。 RTT的波動非常具有動感且性感,它用數值表徵了整個排隊理論,或者你可以推出馬爾科夫到達過程,或者你只是覺得它們是令人難過的噪點...於是,現實中的TCP幾乎完全改進了Reno的指導,除了Reno幾乎沒有什麼擁塞演算法在發現丟包時把ssthresh降為當前視窗的一半。這就是TCP的進化,但是這種進化始終圍繞著一個核心,這個核心就是我上面說的這些,簡單,易懂,然而卻令人驚訝的東西。 --------------------- 作者:dog250 來源:CSDN 原文:https://blog.csdn.net/dog250/article/details/51439747 版權宣告:本文為博主原創文章,轉載請附上博文連結!