1. 程式人生 > >現代網際網路的TCP擁塞控制(CC)演算法評談

現代網際網路的TCP擁塞控制(CC)演算法評談

動機

寫這篇文章本質上的動機是因為前天發了一個朋友圈,見最後的寫在最後,但實際上,我早就想總結總結TCP擁塞控制演算法點點滴滴了,上週總結了一張圖,這周接著那些,寫點文字。

前些天,Linux中國微信公眾號推了一篇文章,上班路上仔細閱讀了一下,感受頗深,一方面由於此文說出了很多我想表達卻苦於組織語言的觀點,另一方面,此文表達了一些我沒能認識到的事實,整個一天的時間,我都在思考此文的字詞句,我想寫一篇讀後感是最合適不過的了。

OK,且聽我繼續嘮叨。

要聊TCP擁塞控制,如果你沒有讀過Van Jacobson(即大名鼎鼎的範.雅各布森)的那篇講TCP擁塞控制的經典論文,那就根本不好思議說自己是混這個行當的,這篇論文的連結我也附上: 《Congestion Avoidance and Control》

http://ee.lbl.gov/papers/congavoid.pdf 當然了,你可以以任何其它途徑搜尋到這篇論文。

本文並非針對這篇論文的評談,之所以在文章的開頭我就提到這篇論文,是因為《第二天》在開頭也引用了這篇論文,這篇論文非常重要,因為它道出了擁塞控制的本質問題和根本的解法,在我評談完《第二天》後,我會以大量的篇幅去解釋這篇論文。

首先,我先從《第二天》開始。

關於《netdev 第二天:從網路程式碼中移除“儘可能快”這個目標》

《第二天》講述了一個事實,即網際網路從1988年到2018年這30年間發生了變化,該變化要求我們的TCP擁塞控制演算法必須做出改變,否則,網際網路將崩塌

這不是危言聳聽,這是真的。讓我們來回顧一段歷史。

還記得1988年Van Jacobson那篇經典論文的寫作背景嗎?嗯,是的,1986年那時網際網路經歷了一次崩塌!嗯,1988年是一個分水嶺:

  • 1988年之前:TCP毫無擁塞控制 簡單的端到端滑動視窗流控機制,一切都都工作的很好!直到1988年前夕的1986年網路崩潰。
  • 1988年之後:引入了TCP擁塞控制 引入慢啟動,擁塞避免這兩個概念,此後TCP擁塞控制將在這兩個基本概念的基礎之上展開進化,直到…下一個分水嶺,嗯,2016年Google放出BBR(事實上Google早就在內部預研並實施BBR了)。

很多人可能並不知道這個事實,但事實真的是,1988年以前,TCP是沒有什麼擁塞控制演算法的,能發多少資料包全憑對端通告過來的那個TCP協議頭裡的視窗

來決定,自從1974年TCP協議誕生,經歷1983年的標準化一直到1988年前夕,這種機制一直工作的非常好,那麼到底是什麼原因導致了1988年前夕的網路崩潰進而導致擁塞控制被引入TCP呢?

這一切的答案藏匿於時間!

我們用發展的眼光來看待這段歷史。非常顯然,1988年接入網路的TCP終端數量要遠遠大於1974年或者1983年,這背後的推動力來自於網路本身規模的指數級增長。我們看一下網路節點的變化情況,由於本文不是專門講網路本身的,所以為了一種延續性,我將早期的網路統稱為ARPA網,雖然它的名字一直在變…

  • 1969年: ARPAnet初創,一共4個節點
  • 1970年:13個節點構成ARPA的全部
  • 1972年:法國CYCLADES網路!!!(非常關鍵)
  • 1973年:發展到約40個節點
  • 1974年5月:《分組網路資訊交換協議》釋出,TCP協議誕生
  • 1974年12月:RFC675釋出,網際網路標準誕生
  • 1976年:持續增加節點到57個
  • 1982年:網路節點增長到100個左右
  • 1983年1月1日:TCP/IP取代舊協議族成為ARPA網控制協議
  • 1984年:美國國防部將TCP/IP作為計算機網路的標準
  • 1984年:節點數量達到1000+,已經初具規模,網際網路雛形形成
  • 1986年:網路擁塞導致崩潰
  • 1988年:擁塞控制被引入TCP

這期間,隨著TCP協議被標準化,TCP幾乎成了預設的接入協議…

這就像中國的汽車產業一樣,80年代我們中國也許只有機關單位擁有少量的汽車,隨著時間的推移,汽車變得越來越多,還記得小學的課文《北京立交橋》嗎?在20世紀90年代,立體交通被首先引入北京,上海,廣州等城市,這個和網際網路的發展如出一轍,所謂日光之下,並無新事!

非常簡單的道理,節點多了,必然會擁塞。然而這是導致擁塞的唯一原因嗎?

我們上大學第一節計算機網路課的時候,老師一般會介紹網路可以分為通訊子網和資源子網。上述的ARPAnet發展脈絡說的更多的是通訊子網,到底何謂通訊子網?

請注意上面發展脈絡註解中的1972年法國CYCLADES網路,它首次提出了端到端原則,即:

The CYCLADES network was the first to make the hosts responsible for the reliable delivery of data, rather than this being a centralized service of the network itself. Datagrams were exchanged on the network using transport protocols that do not guarantee reliable delivery, but only attempt best-effort. To empower the network leaves, the hosts, to perform error-correction, the network ensured end-to-end protocol transparency, a concept later to be known as the end-to-end principle .

上述引用來自CYCLADES的Wiki頁面:https://en.wikipedia.org/wiki/CYCLADES 這個端到端原則非常重要!後來在1974年出爐的TCP協議就是一個非常成功的端到端協議,它保證了網路對資源的透明性,我們現在知道,TCP幾乎不瞭解網路的任何細節,這一切思想全部來源於那場端到端原則引發的頭腦風暴!

比較悲哀,先行者往往超越了時代,最終CYCLADES消亡了,一方面它太超前,另一方面可能是歐洲實在沒有類似美國的這種網際網路基因,它最終退出了歷史,關於CYCLADES,下面這個連結不錯: https://www.techopedia.com/definition/27855/cyclades

花開兩朵,各表一枝。我們再看看同時期的另外一條線,即資源子網的發展脈絡。所謂的資源子網,其實更多的指的就是計算機終端。

  • 1972年:Intel 8008處理器釋出
  • 1974年:Intel 8080處理器釋出
  • 1976年:《乙太網:區域計算機網路的分散式資料包交換技術》釋出,乙太網誕生
  • 1978年:Intel 8086處理器釋出
  • 1980年:微軟dos作業系統
  • 1985年:Intel 80386處理器釋出

時間告訴我們一個事實,處理器越來越強大,計算機終端在進步!這背後是一個摩爾定律在悄悄發揮著作用!

這意味著什麼?這意味著承載TCP的計算機終端,將會以越來越快的速度傳送資料包!

節點越來越多,發包越來越快,這便是1988年前夕網路擁塞的根源!

於是,1988年後,擁塞控制被引入了TCP協議,這便是我們再熟悉不過的現代TCP協議了,如果你手頭有Linux核心原始碼,看看net/ipv4/tcp_cong.c這個檔案,tcp_reno_cong_avoid函式,就是描述的Van Jacobson論文裡的思想:

/*
 * TCP Reno congestion control
 * This is special case used for fallback as well.
 */
/* This is Jacobson's slow start and congestion avoidance.
 * SIGCOMM '88, p. 328.
 */
void tcp_reno_cong_avoid(struct sock *sk, u32 ack, u32 acked)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (!tcp_is_cwnd_limited(sk))
        return;

    /* In "safe" area, increase. */
    if (tcp_in_slow_start(tp)) {
        acked = tcp_slow_start(tp, acked);
        if (!acked)
            return;
    }
    /* In dangerous area, increase slowly. */
    tcp_cong_avoid_ai(tp, tp->snd_cwnd, acked);
}

那麼然後呢?

有了這個擁塞控制演算法,是不是就萬事大吉了呢?從某種程度上說,是的!Why??

我們知道,發包的終端是計算機,而早期計算機的處理器決定了發包的速率,這個時候,我們看一下網路骨幹上的那些處理資料包轉發的裝置。

像思科這種廠商,他們的路由器,交換機的處理能力是發包的終端計算機難以企及的,專業的路由交換裝置擁有更加高頻的處理器,甚至有自己的線卡,這些高階裝置完全可以Cover住任何計算機發送的資料包的轉發,毫無壓力。一切都在儲存轉發式的網路中有條不紊的進行著。

中間節點的高階裝置只需要不多且固定數量的快取,就可以暫時儲存還沒有來得及處理的資料包,需要多少快取只需要通過排隊理論的公式就能計算出來。

如果將路由器交換機看作是服務檯,那麼終端計算機發送的資料包按照泊松到達的原則到達並排隊,然後被背靠背地服務並轉發出去,這看起來非常美好!如果接入網路的終端計算機更多更強了,只需要換更高階的轉發裝置即可,這難道有什麼問題嗎?

我記得2004年我第一次接觸H3C的講座的時候,從最低端的幾千元的裝置一直到上百萬的裝置,都給我們看過並且摸過,當時特別驚訝,我在想,一臺上百萬的裝置接在骨幹網的通訊機房,開機時,那將是多麼壯美的場景,所以我一直都想去通訊領域的企業工作,只可惜未能成行…

這場計算機網路終端和中間轉發裝置之間軍備競賽好像會促使他們中的任何一方持續進化成巨無霸,然而,它們忽略了一個事實!

那就是,摩爾定律遇到了熱密度的上限!

這是一件非常令人遺憾的事情,面對這個無法突破的上限,中間轉發裝置被逼停在了山巔,眼睜睜看著計算機終端的處理器,網絡卡效能和自己的距離越來越近,卻無法再前進一步!

把時間拉到眼前,我們現在可以在普通的伺服器甚至個人計算機上輕鬆安裝最新的Intel萬兆網絡卡,甚至40G的網絡卡,中間轉發裝置卻再也無法甩開這些幾個數量級了,畢竟有摩爾定律的大限在那!

《第二天》引文一段:

在演講中,Van Jacobson 說網際網路的這些已經發生了改變:在以前的網際網路上,交換機可能總是擁有比伺服器更快的網絡卡,所以這些位於網際網路中間層的伺服器也可能比客戶端更快,並且並不能對客戶端傳送資訊包的速率有多大影響。 . 很顯然今天已經不是這樣的了!眾所周知,今天的計算機相比於 5 年前的計算機在速度上並沒有多大的提升(我們遇到了某些有關光速的問題)。所以我想路由器上的大型交換機並不會在速度上大幅領先於資料中心裡伺服器上的網絡卡。

拋開價格不談,看看我們手裡的手機,再看看2004年那些大型網路裝置,看看它們之間的差距,是不是在縮小!

我們可以預見的是,隨著計算機終端越來越追平中間轉發裝置的效能,即使是1988年提出的擁塞控制演算法,對於真實的網路擁塞情況也將會越來越力不從心,網路變成了下面的樣子:

這裡寫圖片描述

1986年的擁塞崩潰還會重演,而且可能就在眼前!或者至少是不遠的將來!

我在高中的時候,訂閱過一本叫做《科學美國人》的雜誌,大概是2001年一期,引出一個《網際網路崩潰》的論題,當時正值第一次網際網路爆發或者說泡沫時期,作者基於1988年Van Jacobson的論文裡的擁塞控制演算法,提出了一種擔憂,作者擔憂網際網路將在10年內崩潰,即1988年的擁塞控制演算法將在10年內失效!後來的事實表明,崩潰來得確實遲到了。

除了學術界,其實,從進入21世紀第一次網際網路領域的寒武紀大爆發開始以來,上面的這個網際網路總有一天會擁塞崩潰的問題也已經被網路裝置廠商注意到了,雖然意識到自己面臨了摩爾定律的極限,處理效能無法做進一步的提高,但是,橫向的擴充套件還是可以繼續的,無法更快,但能更多,無疑,增加處理器和線卡的數量價效比遠不如將來不及處理的資料暫時存起來,這帶來了一種解決方案:增加快取大小

排隊理論和泊松到達原則預示著,快取總是會被清空,這使得超大快取佇列的中間網路裝置成了一種趨勢。

這並沒有解決問題,而是引入了問題,網路不再是一個用完即走的設施,而成了一個巨大的快取設施,類似北京四環快速路那樣,成了巨大的停車場!

對!這就是Bufferbloat!!

Bufferbloat將對資料包帶來嚴重的延遲,降低使用者體驗!不過,好歹Bufferbloat也是有些可以借鑑的地方的,比如我覺得Kafka這玩意兒說白了就是一個Bufferbloat模型下的元件,你說Kafka這種設計好嗎?反正我覺得挺不錯的!這裡給出一個Kafka的部落格連結,作為備註: 《Kafka設計思想的脈絡整理》https://blog.csdn.net/dog250/article/details/79588437

那麼,解決方案是什麼?

《第二天》裡說的很明確:以更慢的速率傳送更多的資訊包以達到更好的效能

反正發快了也是排隊,排隊造成的延遲對於接收端而言和發得慢沒有任何區別,不如用Pacing來平滑泊松到達的峰谷,還能減少甚至避免快取爆滿而造成的丟包。這就是Pacing慢發背後的邏輯。

核心骨幹網路的高頻寬等待使用者計算機發送效率提升的時代已經結束了,目前大多數計算機發送資料包的速率已經超過了具有光速極限的傳播速率,是時候要以頻寬作為傳送速率的基準了,以前是能發多快就發多快,核心交換裝置應付低速的計算機終端不是個事兒,如今高速計算機終端則必須傳送速率適應網路的處理能力,即適應網路的瓶頸頻寬

然後《第二天》又給出了一個具體的方案,那就是“使用BBR”

在上面我說過:“假設你可以辨別出位於你的終端和伺服器之間慢連線的速率……”,那麼如何做到呢?來自 Google(Jacobson 工作的地方)的某些專家已經提出了一個演算法來估計瓶頸的速率!它叫做 BBR,由於本次的分享已經很長了,所以這裡不做具體介紹,但你可以參考 BBR:基於擁塞的擁塞控制 和 來自晨讀論文的總結 這兩處連結。

我能說什麼呢?如果我把2016年BBR釋出的那一年作為TCP的第二個分水嶺,你覺得合適嗎?我覺得是合適的!

  • 1974年-1988年:第0代TCP,沒有擁塞控制
  • 1988年-2016年:第1代TCP,引入Van Jacobson論文裡AIMD模型的擁塞控制 計算機終端不如核心交換裝置,處理效能差距巨大,Burst傳送。
  • 2016年-至今/未來的某一年:第2代TCP,引入Google的BBR擁塞控制並持續進化 計算機終端處理效能追平核心交換裝置,Burst傳送會造成Bufferbloat,採用Pacing傳送。

說來也巧了,上週日,正好總結了一幅圖,展現擁塞控制演算法的發展脈絡: 《總結一幅TCP/QUIC擁塞控制(CC)演算法的圖示》https://blog.csdn.net/dog250/article/details/81834855 《第二天》這篇文章是週一才推給我的,和我前一天寫的這個基本在說同一個話題,這接力秒啊!

好了,現在我們從程式碼實現的角度看看程式碼是如何順應歷史的吧。

從第一行程式碼執行在第一臺計算機上的時候,正如《第二天》所說:網路程式碼被設計為執行得“儘可能快”!,任何控制子系統都是這般,是否更快幾乎是衡量一個演算法優劣的首要標準!網路軟體也不例外,如《第二天》文中所述:

所以,假設我們相信我們想以一個更慢的速率(例如以我們連線中的瓶頸速率)來傳輸資料。這很好,但網路軟體並不是被設計為以一個可控速率來傳輸資料的!下面是我所理解的大多數網路軟體怎麼做的: . 1. 現在有一個佇列的資訊包來臨; 2. 然後軟體讀取佇列並儘可能快地傳送資訊包; 3. 就這樣,沒有了。

在計算機終端和網路核心交換裝置處理效能差異巨大的年代,所有的計算機終端均採用“儘可能快”的方式發包時,這顯然是提高效率的最佳做法。

對於計算機發送資料包而言,依照這個原則就是讓資料包以最快的速度離開計算機,能多快就多快,事實上,當前的很多網絡卡(比如Intel x350…)的傳送頻寬已經超過了大多數的鏈路頻寬。

當核心網路交換裝置遇到摩爾定律瓶頸,其效能提升不足以扛得住大量高速計算機終端快速發包帶來的處理壓力時,佇列增長就是必然的,而我們知道,資料包排隊現象是一種毫無價值的臨時避免丟包的措施,而且平添了延時和儲存成本,因而,降低計算機終端發包的速率變成了最有效最經濟的措施。

《第二天》給出瞭解決方案:一個更好的方式:給每個資訊包一個“最早的出發時間”

好吧,具體到實現上,那就是所謂的用時間輪盤替換佇列!!!。你不知道什麼是時間輪盤?也許吧,但是你應該知道Linux核心網路資料包schedule模組FQ吧。是的,這個實現就是一個時間輪盤!

這個所謂的時間輪盤機制其實在Linux核心的TCP實現中已經有所使用了,比如針對TCP連線的超時重傳Timer就是用時間輪盤實現的,不然的話,每一個包都啟用一個Timer,開銷甚大。說白了,時間輪盤就是把待定的Event進行排序,然後採用一個一維的時間序列順序處理,思路非常簡單!

抽象地講,時間輪盤是下面的樣子:

這裡寫圖片描述

如此,資料包只需要在時間輪盤裡找到一個位置,就可以實現Pacing傳送了。

在傳統的第0代,第1代TCP時代,所謂的“儘可能快地傳送”,註定資料包的傳送方式是突發的,即Burst方式,而當時間來到我們已經意識到核心轉發裝置的極限從而對Burst傳送方式有所收斂的第2代TCP時代,我們必然要採用基於時間輪盤的Pacing傳送方式以適應鏈路的瓶頸頻寬!

在《第二天》這篇文章的最後,作者抒發了一種展望:

所以重點是假如你想大幅改善網際網路上的擁塞狀況,只需要改變 Linux 網路棧就會大所不同(或許 iOS 網路棧也是類似的)。這也就是為什麼在本次的 Linux 網路會議上有這樣的一個演講!

或者說,這也許是我們大家的展望!

插敘一段。這裡不得不說的是,Linux贏了。還記得當初有人比較Windows和Linux嗎?然而過去了很多年後,請問你有多久沒有開啟家裡的Windows機器了?是的,我們基本都是一部手機足矣,而我們的手機中有超過60%的,其底層,正是Linux核心。

微軟無疑是PC時代的王者中的王者,曾經記得微軟以Windows打Linux,微軟以dot NET對抗Java,多線作戰也未見頹勢。然而在移動網際網路時代,事情起了變化,你會發現Linux和Java似乎堆在一起了,這不就是Android嗎?有點意思,微軟從一打二變成了一打一,然而卻更被動了,事實上,微軟在一打一的時候,已經輸了…

網際網路依然在持續發展進化,作者在抒發了對這個變化的事實之一番感慨後,結束了《第二天》:

通常我以為 TCP/IP 仍然是上世紀 80 年代的東西,所以當從這些專家口中聽說這些我們正在設計的網路協議仍然有許多嚴重的問題時,真的是非常有趣,並且聽說現在有不同的方式來設計它們……等等,一直都在隨著時間發生著改變,所以正因為這樣,我們需要為 2018 年的網際網路而不是為 1988 年的網際網路設計我們不同的演算法。

是啊,30年前TCP/IP的架構在現如今卻依然可以擁抱變化,這也讓我不禁感嘆TCP/IP基因的優良,同時,還有一些同樣擁抱變化的別忘了,那就是UNIX,以及我們人類文明自身!

關於《第二天》的讀後感,我想就到此為止了,但正如本文一開始所說的,接下來還將有一個部分的內容,那就是關於Van Jacobson那篇經典論文的點點滴滴,如是解釋一番後,方可不留遺憾地結束本文!

關於《Congestion Avoidance and Control》

我說,沒有讀過這篇論文的,就別說自己是TCP擁塞控制圈裡人了,這不是開玩笑。因為只有你讀了這篇論文,才知道擁塞控制的基本原則是什麼不是什麼。

我再次重申,擁塞控制演算法不是用來加速TCP的,相反,它是用來減速TCP的。想加速TCP,你必須修改控制狀態機!

論文裡提到

The flow on a TCP connection (or ISO TP-4 or Xerox NS SPP connection) should obey a ‘conservation of packets’ principle. And, if this principle were obeyed, congestion collapse would become the exception rather than the rule. Thus congestion control involves finding places that violate conservation and fixing them. … A new packet isn’t put into the network until an old packet leaves

嗯,就是這個原則,出去一個,進來一個。

在這個基礎上,AIMD演算法被提出來。AIMD其實也是見招拆招的產物,它本身就是為了Fix下面的違規:

There are only three ways for packet conservation to fail: 1. The connection doesn’t get to equilibrium, or 2. A sender injects a new packet before an old packet has exited, or 3. The equilibrium can’t be reached because of resource limits along the path.

資料包守恆是擁塞控制的最最最最最最最最最基本的原則,這個原則基於單流單鏈路固定頻寬提出,非常直接且簡單,很容易理解。

在現實實施中,我們知道,TCP是一個遵循端到端原則的協議,即它對網路是無感知的,所以它必須靠某種演算法去動態適應不固定的頻寬,比如任何時候都可能會有任何地方發出的新的TCP流,同樣,任何時候都有可能有舊TCP流退出從而騰出新的頻寬空間,如何去動態適應這些,是最終的演算法必須要考慮的事情。

所以,最終,該論文提出了AIMD演算法的三大構件:

  • 1.總的原則:單流固定頻寬必須遵守資料包守恆,在此基礎上,多流動態頻寬的一般情形下—->
  • 2.動態適應:AI,即加性增窗探測
  • 3.公平性:MD,即乘性降窗收斂

其中,第2點和第3點在不斷地博弈,第2點顯然為了fetch可能並沒有的空餘頻寬而犧牲了公平性,而第3點則反過來為了公平性乘性降窗而犧牲了效能!

注意,這其中有一個叫做Slow-Start的插曲,所謂的Slow-Start,其要義是用比較快的速度上探網路的處理能力,並不是真正的Slow。

一旦一個連線開始,擁塞控制的AIMD原則最終會讓該連線收斂於與其它連線一致公平的位置,這樣的演算法就是好的演算法,一個好的演算法絕不是一個快的演算法,在效率之前首先要考慮的是公平!

AIMD的過程實際上是一個合作過程,是一個所有的節點一起來公平填充網路交換節點快取的過程,注意,AIMD的過程並沒有涉及到頻寬的概念,整個過程就是對交換節點快取的探測和考驗。因此,一旦快取被填滿,那麼所有的途經於此的TCP連線均將同時面臨丟包!以此為假設,我們來推導經典AIMD的公平性。

好的,接下來我來簡單說一下AIMD為什麼是收斂的,是公平的。

假設: 1. 快取填滿即擁塞,尾部丟包。 2. 擁有C1C2兩個TCP連線,其初始視窗分別為w1w2,接下來是簡單的視窗進化過程:

AIMD探測週期C1的視窗變化C2的視窗變化
1次AIMD探測 Wc1=w1 Wc2=w2
Wc1=w1+1 Wc2=w2+1
Wc1=w1+...+1(k11) Wc2=w2+...+1(k11)
Wc1=w1+k12