正文以前,給出本文的圖例:算法
bbr算法實際上很是簡單,在實現上它由5部分組成:緩存
計算一個即時的帶寬bw,該帶寬是bbr一切計算的基準,bbr將會根據當前的即時帶寬以及其所處的pipe狀態來計算pacing rate以及cwnd(見下文),後面咱們會看到,這個即時帶寬計算方法的突破式改進是bbr之因此簡單且高效的根源。計算方案按照標量計算,再也不關注數據的含義。在bbr運行過程當中,系統會跟蹤當前爲止最大的即時帶寬。安全
bbr之因此能夠獲取很是高的帶寬利用率,是由於它能夠很是安全且豪放地探測到帶寬的最大值以及rtt的最小值,這樣計算出來的BDP就是目前爲止TCP管道的最大容量。bbr的目標就是達到這個最大的容量!這個目標最終驅動了cwnd的計算。在bbr運行過程當中,系統會跟蹤當前爲止最小RTT。網絡
bbr算法根據互聯網的擁塞行爲有針對性地定義了4中狀態,即STARTUP,DRAIN,PROBE_BW, PROBE_RTT。bbr經過對上述計算的即時帶寬bw以及rtt的持續觀察,在這4個狀態之間自由切換,相比以前的全部擁塞控制算法,其革命性的改進在於bbr擁塞算法再也不跟蹤系統的TCP擁塞狀態機,而旨在用統一的方式來應對pacing rate和cwnd的計算,無論當前TCP是處在Open狀態仍是處在Disorder狀態,抑或已經在Recovery狀態,換句話說,bbr算法感受不到丟包,它能看到的就是bw和rtt!框架
首先必需要說一下,bbr的輸出並不只僅是一個cwnd,更重要的是pacing rate。在傳統意義上,cwnd是TCP擁塞控制算法的惟一輸出,可是它僅僅規定了當前的TCP最多能夠發送多少數據,它並無規定怎麼把這麼多數據發出去,在Linux的實現中,若是發出去這麼多數據呢?簡單而粗暴,突發!忽略接收端通告窗口的前提下,Linux會把cwnd一窗數據所有突發出去,而這每每會形成路由器的排隊,在深隊列的狀況下,會測量出rtt劇烈地抖動。
bbr在計算cwnd的同時,還計算了一個與之適配的pacing rate,該pacing rate規定cwnd指示的一窗數據的數據包之間,以多大的時間間隔發送出去。tcp
bbr之因此能夠高效地運行且如此簡單,是由於不少機制並非它自己實現的,而是利用了外部的已有機制,好比下一節中將要闡述的它爲何在計算帶寬bw時能如此放心地將重傳數據也計算在內...函數
bbr做爲一個純粹的擁塞控制算法,徹底忽略了系統層面的TCP狀態,計算帶寬時它僅僅須要兩個值就夠了:
1).應答了多少數據,記爲delivered;
2).應答1)中的delivered這麼多數據所用的時間,記爲interval_us。
將上述兩者相除,就能獲得帶寬:
bw = delivered/interval_us
很是簡單!以上的計算徹底是標量計算,只關注數據的大小,不關注數據的含義,好比delivered的採集中,bbr根本無論某一個應答是重傳後的ACK確認的,正常ACK確認的,仍是說SACK確認的。bbr只關心被應答了多少!
這和TCP/IP網絡模型是一致的,由於在中間鏈路上,路由器交換機們也不會去管這些數據包是重傳的仍是亂序的,然而擁塞也是在這些地方發生的,既然擁塞點都不關心數據的意義,TCP爲何要關注呢?反過來,咱們看一下擁塞發生的緣由,即數據量超過了路由器的帶寬限制,利用這一點,只須要精心地控制發送的數據量就行了,徹底不用管什麼亂序,重傳之類的。固然個人意思是說,擁塞控制算法中不用管這些,但這並不意味着它們是被放棄的,其它的機制會關注的,好比SACK機制,RACK機制,RTO機制等。
接下來咱們看一下這個delivered以及interval_us的採集是如何實現的。仍是像往常同樣,我不許備分析源碼,由於若是分析源碼的話,每每難以抓住重點,過一段時間本身也看不懂了,相反,畫圖的話,就能夠過濾掉不少諸如unlikely等異常流或者當前無需關注的東西:post
上圖中,我故意用了一個極端點的例子,在該例子中,我幾乎都是使用的SACK,當X被SACK時,咱們能夠根據圖示很容易算出從Delivered爲7時的數據包被確認到X被確認爲止,一共有12-7=5個數據包被確認,即這段時間網絡上清空了5個數據包!咱們便很容易算出帶寬值了。個人這個圖示在解釋帶寬計算方法以外,還有一個目的,即說明bbr在計算帶寬時是不關注數據包是否按序確認的,它只關注數量,即數據包被網絡清空的數量。實實在在的計算,不猜Lost,不猜亂序,這些東西,你再怎麼猜也猜不許!
計算所得的bw就是bbr此後一切計算的基準。學習
bbr的狀態機轉換圖以及註釋以下圖所示:測試
經過上述的狀態機以及上一節的帶寬計算方式,咱們知道了bbr的工做方式:不斷地基於當前帶寬以及當前的增益係數計算pacing rate以及cwnd,以此2個結果做爲擁塞控制算法的輸出,在TCP鏈接的持續過程當中,每收到一個ACK,都會計算即時的帶寬,而後將結果反饋給bbr的pipe狀態機,不斷地調節增益係數,這就是bbr的所有,咱們發現它是一個典型的封閉反饋系統,與TCP當前處於什麼擁塞狀態徹底無關,其簡圖以下:
這很是不一樣於以前的全部擁塞控制算法,在以前的算法中,咱們發現擁塞算法內部是受外部的擁塞狀態影響的,好比說在Recovery狀態下,甚至都不會進入擁塞控制算法,在bbr進入內核以前,Linux使用PRR算法控制了Recovery狀態的窗口調整,即使說這個時候網絡已經恢復,TCP也沒法發現,由於TCP的Recovery狀態還未恢復到Open,這就是根源!
這一節好像是重點中的重點,可是我以爲若是理解了bbr的帶寬計算,狀態機以及其增益係數的概念,這裏就不是重點了,這裏只是一個公式化的結論。
pacing rate怎麼計算?很簡單,就是是使用時間窗口內(默認10輪採樣)最大BW。上一次採樣的即時BW,用它來在可能的狀況下更新時間窗口內的BW採樣值集合。此次可否按照這個時間窗口內最大BW發送數據呢?這樣看當前的增益係數的值,設爲G,那麼BW*G就是pacing rate的值,是否是很簡單呢?!
至於說cwnd的計算可能要稍微複雜一點,可是也是能夠理解的,咱們知道,cwnd其實描述了一條網絡管道(rwnd描述了接收端緩衝區),所以cwnd其實就是這個管道的容量,也就是BDP!
BW咱們已經有了,缺乏的是D,也就是RTT,不過別忘了,bbr一直在持續蒐集最小的RTT值,注意,bbr並無採用什麼移動指數平均算法來「猜想」RTT(我用猜想而不是預測的緣由是,猜想的結果每每更加不可信!),而是直接冒泡採集最小的RTT(注意這個RTT是TCP系統層面移動指數平均的結果,即SRTT,但brr並不會對此結果再次作平均!)。咱們用這個最小RTT幹什麼呢?
當前是計算BDP了!這裏bbr取的RTT就是這個最小RTT。最小RTT表示一個曾經達到的最佳RTT,既然曾經達到過,說明這是客觀的能夠再次達到的RTT,這樣有益於網絡管道利用率最大化!
咱們採用BDP*G'就算出了cwnd,這裏的G'是cwnd的增益係數,與帶寬增益係數含義同樣,根據bbr的狀態機來獲取!
該節的題目比較怪異,既然是細節爲何又要淺述??
這是個人風格,一方面,說是細節是由於這些東西還真的不多有人注意到,另外一方面,說是淺述,是由於我通常都不會去分析代碼以及代碼裏每個異常流,我認爲那些對於理解原理幫助不大,那些東西只是在研發和優化時纔是有用的,因此說,像往常同樣,我這裏的這個小節仍是一如既往地去談及一些「細節」。
在看到bbr以後,我以爲以前的TCP擁塞控制算法都錯了,並非思想錯了,而是實現的問題。
bbr之因此敢大膽的去探測預估帶寬是由於TCP把更多的權力交給了它!在bbr以前,不少本應該由擁塞控制算法去處理的細節並不歸擁塞控制算法管。在詳述以前,咱們必須分清兩件事:
1).傳輸多少數據?
2).傳輸哪些數據?
按照「上帝的事情上帝管,凱撒的事情凱撒管」的原則,這兩件事原本就該由不一樣的機制來完成,不考慮對端接收窗口的狀況下,擁塞窗口是惟一的主導因素,「傳輸多少數據」這件事應該由擁塞算法來回答,而「傳輸哪些數據」這個問題應該由TCP擁塞狀態機以及SACK分佈來決定,誠然這兩個問題是不一樣的問題,不該該雜糅在一塊兒。
然而,在bbr進入內核以前的Linux TCP實現中,以上兩個問題並非分得特別清。TCP的擁塞狀態只有在Open時纔是上述的職責分離的完美樣子,一旦進入Lost或者Recovery,那麼擁塞控制算法即使對「問題1):傳輸多少數據」都無能爲力,在Linux的現有實現中,PRR算法將接管一切,一直把窗口降低到ssthresh,在Lost狀態則反應更加激烈,直接cwnd硬着陸!隨後等丟失數據傳輸成功後再執行慢啓動....在從新進入Open狀態以前,擁塞控制算法幾乎不會起做用,這並非一種高速公路上的模式(小碰擦,拍照後停靠路邊,自行解決),更像是鬧市區的交通事故處理方式(不管怎樣,保持現場,直到交警和保險公司的人來現場處置)。
bbr算法逃離了這一切錯誤的作法,在bbr的patch中,並不是只是完成了一個tcp_bbr.c,而是對整個TCP擁塞狀態控制框架進行了大手術,咱們能夠從如下的擁塞控制核心函數中可見一斑:
static void tcp_cong_control(struct sock *sk, u32 ack, u32 acked_sacked, int flag, const struct rate_sample *rs) { const struct inet_connection_sock *icsk = inet_csk(sk); if (icsk->icsk_ca_ops->cong_control) { // 若是是bbr,則徹底被bbr接管,無論如今處在什麼狀態! /* 目前而言,只有bbr使用了這個機制,但我相信,不久的未來, * 會有愈來愈多的擁塞控制算法使用這個統一的徹底接管機制! * 就我我的而言,在幾個月前就寫過一個patch,接管了tcp_cwnd_reduction * 這個prr的降窗過程。若是當時有了這個框架,我就有福了! */ icsk->icsk_ca_ops->cong_control(sk, rs); return; } // 不然繼續以往的錯誤方法! if (tcp_in_cwnd_reduction(sk)) { /* Reduce cwnd if state mandates */ // 非Open狀態中擁塞算法不受理窗口調整 tcp_cwnd_reduction(sk, acked_sacked, flag); } else if (tcp_may_raise_cwnd(sk, flag)) { /* Advance cwnd if state allows */ tcp_cong_avoid(sk, ack, acked_sacked); } tcp_update_pacing_rate(sk); }
在這個框架下,不管處在哪一個狀態(Open,Disorder,Recovery,Lost...),若是擁塞控制算法本身聲明有這個能力,那麼具體能夠傳輸多少數據,徹底由擁塞控制算法自行決定,TCP擁塞狀態控制機制再也不干預!
看懂了以上第1點,這一點就很容易理解了。
在第1點中,我描述了bbr確實忽略了Recovery等非Open的擁塞狀態,可是爲何能夠忽略呢?通常而言,不少人都會質疑,會說bbr採用這麼魯莽的方式,最終必定會讓窗口卡住再也不滑動,可是我要反駁,你難道不知道cwnd只是個標量嗎?我畫一個圖來分析:
看懂了嗎?不存在任何問題!基本上,咱們在討論擁塞控制算法的時候,會忽略流量控制,由於不想讓rwnd和cwnd雜糅起來,可是在這裏,它們相遇了,幸運的是,並無引起衝突!
然而,這並非所有,本節旨在「淺析」,所以就不會關注代碼處理的細節。在bbr的實現中,若是算法外部的TCP擁塞狀態已經進入了Lost,那麼cwnd該是多少呢?在bbr以前的擁塞算法中,包括cubic在內的全部算法中,當TCP核心實現從將cwnd調整到1或者prr到ssthresh一直到恢復到Open狀態,擁塞算法無權干預流程,然而bbr不。雖說進入Lost狀態後,cwnd會硬着陸到1,然而因爲bbr的接管,在Lost期間,cwnd仍是能夠根據即時帶寬調整的!
這意味着什麼?
這意味着bbr能夠區別噪聲丟包和擁塞丟包了!
a).噪聲丟包
若是是噪聲丟包,在收到reordering個重複ACK後,因爲bbr並不區分一個確認是ACK仍是SACK引發的,因此在bbr看來,即時帶寬並無下降,可能還有所增長,因此一個數據包的丟失並不會引起什麼,bbr依舊會給出一個比較大的cwnd配額,此時雖然TCP可能已經進入了Recovery狀態,但bbr依舊按照本身的bw以及調整後的增益係數來計算cwnd的新值,過程當中並不會受到任何TCP擁塞狀態的影響。
如此一來,全部的噪聲丟包就被區別開來了!bbr的宗旨是:「首先,在個人bw計算指示我發生擁塞以前,任何傳統的TCP擁塞判斷-丟包/時延增長,均所有失效,我並不care丟包和RTT增長」,隨後brr又會說:「可是我比較care的是,RTT在一段時間內(隨你怎麼配,但我我的傾向於自學習)都沒有達到我所採集到的最小值或者更小的值!這也許意味着着鏈路真的發生擁塞了!」...
b).擁塞丟包
將a)的論述反過來,咱們就會獲得奇妙的封閉性結論。這樣,bbr不光是消除了吞吐曲線的鋸齒(ssthresh所致,bbr並不使用ssthresh!),並且還消除了傳統擁塞控制算法(指bbr以及封閉的傻逼Appex以前)的判斷滯後性問題。在cubic發現丟包進而判斷爲擁塞時,擁塞可能已經緩解了,可是cubic沒法發現這一點。爲何?緣由在於cubic在計算新的cwnd的時候,並無把當前的網絡狀態(好比bw)看成參數,而只是一味的按照數學意義上的三次方程去計算,這是錯誤的,這不是一個正確的反饋系統的作法!
基於a)和b),看到了吧,這就是新的擁塞判斷機制!綜合考慮丟包和RTT的增長:
b-1).若是丟包時真的發生了擁塞,那麼測量的即時帶寬確定會減小,不然,丟包即擁塞就是謊話。
b-2).若是RTT增長時真的發生了擁塞,那麼測量的即時帶寬確定會減小,不然,時延增長即擁塞就是謊話。
bbr測量了即時帶寬,這個統一cwnd和rtt的計量,徹底忽略了丟包,所以bbr的算法思想是TCP擁塞控制的正軌!事實上,丟包本就不該該做爲一種擁塞的標誌,它只是擁塞的表現。
我在上文已經呈現了關於STARTUP,DRAIN,PROBE_BW,PROBE_RTT的狀態圖以及些許細節,當時我指出這個狀態圖的目標是爲了完成bbr的目標,即填滿整個網絡!在這個狀態圖看來,全部已知的東西就是當前的即時帶寬,全部能夠計算的東西就是增益係數,而後根據這兩個元素就能夠輕易計算出pacing rate和cwnd,是否是很簡單呢?總體看來就是就是這麼簡單,可是從細節上看,不一樣的pipe狀態中的增益係數的計算倒是值得推敲的,如下是bbr處在各個狀態時的增益係數:
STARTUP:2~3
DRAIN:pacing rate的增益係數爲1000/2885,cwnd的增益係數爲1000/2005+1。
PROBE_BW:5/4,1,3/4,bbr在PROBE_BW期間會隨機在這些增益係數之間選擇當前的增益係數。
PROBE_RTT:1。可是在探測RTT期間,爲了防止丟包,cwnd會強制cut到最小值,即4個MSS。
咱們能夠看到,bbr並無明確的所謂「降窗時刻」,一切都是按照狀態機來的,期間絲絕不會理會TCP是否處在Open,Recovery等狀態。在此前的擁塞控制算法中,除了Vegas等基於延時的算法會在計算獲得的target cwnd小於當前cwnd時視爲擁塞而在算法中降窗外,其它的全部基於丟包的算法中均是檢測到丟包(RTO或者reordering個重複ACK)時降窗的,可悲的是,這個降窗過程並不受擁塞算法的控制,擁塞算法只能消極地給出一個ssthresh值,即降窗的目標,這顯然是使人無助的!
bbr再也不關注丟包事件,它並不把丟包當成很嚴重的事,這事也不歸它管,只要TCP擁塞狀態機控制機制能夠合理地將一些包標記爲LOST,而後重傳它們即是了,bbr能作的僅僅是告訴TCP一共能夠發出去多少數據,僅此而已!然而,若是TCP並無把LOST數據包合理標記好,bbr並不care,它只是根據當前的bw和增益係數給出下一個pacing rate以及cwnd而已!
這裏涉及的是bbr以外的東西,Fair queue!在bbr的patch最後,會發現幾行註釋:
NOTE: BBR *must* be used with the fq qdisc ("man tc-fq") with pacing
enabled, since pacing is integral to the BBR design and
implementation. BBR without pacing would not function properly, and
may incur unnecessary high packet loss rates.
記住這幾行文字並理解它們。
這是bbr最爲重要的一方面。雖說Linux的TCP實現早就支持的pacing rate,但直到4.8版本都沒有在TCP層面支持它,很大的一部分緣由是由於藉助已有的FQ能夠很完美地實現pacing rate!TCP能夠藉助FQ來實現平緩而非突發的數據發送!
關於FQ的詳細內容能夠去看相關的manual和源碼,這裏要說的僅僅是,FQ能夠根據bbr設置的pacing rate將一個cwnd內的數據的發送從「突發到網絡」這種行爲變換到「平緩發送到網路」的行爲,所謂的平緩發送指的就是數據包是按照帶寬速率計算的間隔一個個發送到網絡的,而不是突發進網絡的!
這樣一來,就給了網絡緩存以緩解的機會!記住,關鍵問題是bbr會在每收到ACK/SACK時計算bw,這個精確的測量不會漏掉任何可乘之機,即使當前網絡擁塞了,它只要能在下一時刻恢復,bbr就能夠發現,所以即時帶寬一般能夠表現這一點!
還有關於令牌桶監管發現(lt policed)的主題,long term採樣的主題,留到後面的文章具體闡述吧,本文已經足夠長了。
關於深隊列,數據包如何如何長時間排隊但不丟包卻引起RTO,對於淺隊列,數據包如何如何頻繁丟包...談起這個話題我一開始想口若懸河,後來想罵人,如今我三緘其口!任何人都知道端到端的QoS是一個典型的反饋系統,可是任何人都只是誇誇其談,我選擇的是閉口不說,若是非要我說,個人回答就是:不知道!
這是一個怎麼說都能對又怎麼說都能錯的話題,就像股票預測那樣,因此我選擇閉嘴。
bbr算法到來後,單單從公共測試結果上看,貌似解決了bufferbloat問題,也許吧,也許。bbr好像真的開始在高速公路上飈車了...最後給出一個測試圖,來自《A quick look at TCP BBR》:
我一貫以爲TCP擁塞控制算法太過複雜,而複雜的東西基本上就是用來裝逼的垃圾,直到遇到了bbr。
Neal Cardwell提供的patch簡單而又直接,你們能夠從該bbr的pach上一看究竟!在bbr模塊以外,Neal Cardwell主要更改了tcp_ack函數裏面關於delivered計數的部分以及擁塞控制主函數,這一切都十分顯然,只要patch代碼就能夠一目瞭然。在數據包被髮送的時候-無論是初次發送仍是重傳,均會被當前TCP的鏈接情況記錄在該數據包的tcp_skb_cb中,在數據包被應答的時候-無論是被ACK仍是被SACK,均會根據當前的狀態和其tcp_skb_cb中狀態計算出一個帶寬,這些顯而易見的邏輯相比任何人都應該知道哪裏的代碼被修改了! 然而,這種查找和確認的工做太使人感到悲哀,讀懂代碼是容易的,移植代碼是無聊的,由於時間卡的太緊!我必需要說的是,若是一件感興趣的事情變成了必需要完成的工做,那麼作它的激情起碼減小了1/4,OK,還不算太壞,然而若是這個必須完成的工做有了deadline,那麼激情就會再減小1/4,最後,若是有人在背後一直催,那麼完蛋,這件事能夠瞬間完成,可是我能夠鄭重說明這是湊合的結果!可是實際上,這件事本應該能夠當即快速有高質量的完成並驗收!