使用 TCP 時序圖解釋 BBR 擁塞控制演算法的幾個細節

Bomb250發表於2017-05-18

週六,由於要趕一個月底的Deadline,因此選擇了在家VPN加班,大半夜就爬起來跑用例,抓資料…自然也就沒有時間寫文章和外出耍了…不過利用週日的午夜時間(不要問我為什麼可以連續24小時不睡覺,因為我覺得吃飯睡覺是負擔),我決定把工作上的事情先放下,還是要把每週至少一文補上,這已經成了習慣。由於上週實在太忙亂,所以自然根本沒有更多的時間去思考一些“與工作無關且深入”的東西,我指的與工作無關並非意味著與IT,與網際網路無關,只是意味著不是目前我在做的。比如在兩年前,VPN,PKI這些是與工作有關的,而現在就成了與工作無關的,古代希臘羅馬史一直都是與工作無關的,直到我進了羅馬歷史研究相關的領域去領薪資,直白點說,老闆不為之給我支付薪水的,都算是工作無關的東西。玩轉與思考這些東西是比較放得開的,不需要對誰負責,沒有壓力,沒有KPI,沒有Deadline,完全自由的心態對待之,說不定真的很容易獲得真知。

我認識一個草根鼓手朋友,玩轉爵士鼓的水準遠高於那些所謂的專業鼓手,自然帶有一種俠客之風傳道授業解惑,鼓槌隨心所欲地揮舞在他自己的心中,沒有任何負擔和障礙,任何的節奏都可以一氣呵成,從來不打重複鼓點,那叫一個帥!然而他並非專業考級出來的,是拜師出來後自己摸索的。

要興趣去自然揮灑,而不是迫於壓力去應對。

我也是鼓手,但我打的不是爵士鼓,我是鼓譟者,技術的鼓譟者。本文與TCP BBR演算法相關。

0.說明

BBR熱了一段時間後終於迴歸了理性,這顯然要比過熱地炒作要好很多。這顯然也是我所期望的。

本文的內容主要解釋一些關於BBR的細節問題。這些問題一般人可能不會關注,但是針對這些問題仔細思考的話,會得到很多有用的東西。在解釋這些問題時,我依然傾向於使用圖解的方式,但這一次我不再使用Wireshark的tcptrace圖了,而是使用時序圖的方式,因為這種時序圖既然能夠令人一目瞭然地解釋TCP三次握手,四次分手,TIME-WAIT等,那它自然也能解釋更復雜的機制,比如說擁塞控制。

1.延遲ACK以及ACK丟失並不會影響TCP的傳輸速率

在大的時間尺度上看,延遲ACK以及ACK丟失並不會對速率造成任何影響,比如一個檔案4個TCP段正好發完,即便前面幾個ACK全部丟失,只有最後一個到達,那它的傳輸總時間也是不變的。

但是在細微的時間段內,由於延遲ACK或者ACK丟失帶來的時間偏差卻是不可忽略的。

首先我們再次看一下BBR是如何測量即時速率的。測量即時速率需要做一個除法,分子是一段時間內成功到達對端的資料包總量,分母就是這段時間。BBR會在每收到一次ACK的時候測量一次即時速率。計算需要的資料分別在資料傳輸和資料被ACK的時候取樣。很顯然,我們可以想當然地拍腦袋得出一個演算法:

設資料包x發出的時間為t1,資料包x被應答的時間為t2,則在資料包x被應答時採集的即時速率為:
Rate=(從x被髮出到x被應答之間一共ACK以及SACK了多少個資料包)/(t2-t1)

但是這會造成什麼問題呢?這會造成誤差。如下圖所示:

BBR如果依賴這種即時的速率測量機制來運作的話,在ACK丟失或者延遲ACK的情況下會造成測量值偏高。舉一個簡單的例子:

那麼,BBR是如何做到不引入這種誤差從而精確測量即時速率的呢?很簡單,將t1改成至資料包x發出時為止,最後一個(S)ACK收到的時間即可。

詳情請參考核心原始碼的net/ipv4/tcp_rate.c檔案,原理非常簡單。

所以說,BBR的速率測量值並不受延遲ACK,ACK丟失的影響,其測量方法是妥當的。之所以上面給出一個錯誤的方法,是想展示一下什麼樣的做法是不妥當的,以及容易引起質疑的點在哪裡。

結論很明確,延遲ACK,ACK丟失,並不影響BBR速率的採集值。

接下來談第二個問題,關於BBR的擁塞視窗大小的問題。

2.為什麼BBR要把計算出來的BDP乘以2作為擁塞視窗值?

這個問題可以換一種問法,即BBR的bbr_cwnd_gain值如何解釋:

/* The gain for deriving steady-state cwnd tolerates delayed/stretched ACKs: */  
static const int bbr_cwnd_gain  = BBR_UNIT * 2;

我們知道,BBR將Pacing Rate作為第一控制要素,按照計算得到的Pacing Rate平緩地傳送資料包即可,既然是這樣,擁塞視窗的存在還有何意義呢?

BBR的擁塞視窗控制已經退化到了規定一個限額,它主要是為了灌滿管道,解決由於ACK丟失導致的無包可發的問題。
我先來闡述問題。

BBR第一次把速率控制計算和實際的傳輸相分離,又一個典型的控制面與資料面相分離的案例。也就是說,BBR核心模組計算出一個速率,然後就把資料包扔給Pacing傳送引擎模組(當前的實現是FQ,我自己也實現了一個),具體何時傳送由Pacing傳送引擎來控制,二者之間通過一個傳送緩衝區來互動,具體結構如下圖:

可見,擁塞視窗控制的是“到底扔多少資料到傳送緩衝區合適”的。接下來的問題顯然就是,擁塞視窗到底是多少合適呢?

雖然BBR分離了控制邏輯和資料傳送邏輯,但是TCP的一切都是ACK時鐘驅動的,如果ACK該來的時候沒有來,比如說丟了,比如延遲了,那麼就會影響BBR整個核心的運作,進而影響Pacing傳送引擎的資料傳送動作,BBR要做的是,即便沒有ACK來驅動,也可以自行傳送本該傳送的資料包,因此Pacing傳送引擎的傳送緩衝區的意義重要,說白了就是,傳送緩衝區裡一定要有足夠的資料包才行,就算ACK沒有來,引擎還是有包可發的。

下面來展示一幅圖:

如果這個圖有不解之處,像往常一樣,大家一起討論,但總的來講,我覺得問題不大,所以說才會基於上圖產生了下圖:

該圖示中,我把TCP層的BBR核心模組和FQ的傳送模組都畫了出來,這樣我們可以清晰看出擁塞視窗的作用。實際上,BBR核心模組按照擁塞視窗即inflight的限制,將N個資料包注入到Pacing傳送引擎的傳送緩衝區中,這些包會在這個緩衝區內部排隊,最終在輪到自己的時候被髮送出去。由於這個緩衝區裡有足夠的資料包,所以即使是ACK丟失了多個,或者接收端有LRO導致ACK被大面積聚集且延遲,傳送緩衝區裡面的資料包也足夠傳送一陣子了。

維護這麼一個傳送緩衝區的好處是在緩衝區不溢位(為什麼不溢位?那是算出來的,正好兩倍)的前提下時時刻刻有包可發,然而代價也是有的,就是增加了RTT,因為在傳送緩衝區裡排隊的時間也要被算在RTT裡面的。不過這無所謂,這並不影響效能,資料包不管是在TCP層的傳送佇列裡,還是在FQ的佇列裡,最終都是要發出去的。值得注意的是,本地的FQ佇列和中間節點的佇列性質完全不同,本地的佇列是獨佔的,主動的,而中間節點佇列是共享的,被動的,所以這裡並沒有因為RTT的增加而損失效能。這就好比你有一張銀行卡專門用來還房貸,由於利息的浮動,所有每月還款金額不同,為了不欠款,你每個月總是要存進足額的錢進去,一般要遠多於平均的還貸額度才最保險,但這並不意味著你多存了錢這些錢就虧了,在還清貸款之前,存進去的錢早晚都是要還貸的。

3.為什麼在探測最小RTT的時候最少要保持4個資料包

首先要注意的是,用1個包去探測最小RTT會更好,然而效率可能會更低;用5個包去探測最小RTT效率更好,但是可能會導致排隊,為什麼4個包不多也不少呢?

我嘗試用一個時序圖來說明問題:

可見,4個包的視窗是合理的,infilght分別是:剛發出的包,已經到達接收端等待延遲應答的包,馬上到達的應答了2個包的ACK。一共4個,只有1個在鏈路上,另外1個在對端主機裡,另外2個在ACK裡。路上只有1個包,這絕對合理,如果一條路連1個包都容納不下了,那還玩個屎啊!

以上的論述,僅僅為了幫大家理解以下一段註釋的深意:

/* Try to keep at least this many packets in flight, if things go smoothly. For  
 * smooth functioning, a sliding window protocol ACKing every other packet  
 * needs at least 4 packets in flight:  
 */  
static const u32 bbr_cwnd_min_target = 4;

4.用時序圖總覽一下BBR的Startup/Drain/ProbeBW階段

我以下面的時序圖展示一下BBR的流程:

5.Startup階段擁塞視窗計算的滯後性

我們知道,BBR裡面擁塞視窗已經不再是主控因素,事實上它的名字應該改成“傳送緩衝區限額”會比較合適了,為了方便起見,我仍然稱它為擁塞視窗,雖然它的含義已經改變。

在Startup階段,傳送速率每收到一個ACK都會提高bbr_high_gain:

/* We use a high_gain value of 2/ln(2) because it's the smallest pacing gain  
 * that will allow a smoothly increasing pacing rate that will double each RTT  
 * and send the same number of packets per RTT that an un-paced, slow-starting  
 * Reno or CUBIC flow would:  
 */  
static const int bbr_high_gain  = BBR_UNIT * 2885 / 1000 + 1;

這個其實跟傳統擁塞演算法的“慢啟動”效果是類似的。

然而BBR計算擁塞視窗是用“當前採集到的速率”乘以“當前採集到的最小RTT”來計算的,這就造成了“當前傳送視窗”和“當前已經提高的速率”之間的不匹配,所以,計算擁塞視窗的時候,gain因子也必須是bbr_high_gain,從而可以吸收掉速率的實際提升。

6.由ACK通告的接收視窗還有意義嗎?

在以往的Reno/CUBIC年代,視窗的計算是根據既有的固定數學公式算出來的,完全僅僅由ACK來驅動,無視事實上的傳輸速率,所以彼一時的擁塞視窗僅僅可以代表網路的情況,即便如此,這種網路狀態的結論也是猜的。

到了BBR時代,主動測量傳輸速率,將網路處理能力和主機處理能力合二為一,如果網路瓶頸頻寬為10,而主機處理能力為8,那麼顯然採集到的頻寬不會大於8!反之亦然。如果BBR測量的即時速率很準確的話,我想通告視窗就完全沒有意義了,通告的接收視窗會被忠實地反映在傳送端採集到的即時速率裡。BBR只是重構了擁塞控制演算法,但還沒有重構TCP處理核心,我想BBR可以重構之!

7.BBR在計算擁塞視窗時其它的關鍵點

1>.延遲ACK的影響

計算擁塞視窗的時候,會將目標擁塞視窗進行一下調整:

/* Reduce delayed ACKs by rounding up cwnd to the next even number. */  
cwnd = (cwnd + 1) & ~1U;

此處向上取偶數就是為了平滑最後一個延遲ACK的影響,如果最後一個延遲ACK該來的沒來,那麼這個向上取偶數可以為之補上。

2>.Offload的影響

* To achieve full performance in high-speed paths, we budget enough cwnd to  
* fit full-sized skbs in-flight on both end hosts to fully utilize the path:  
*   - one skb in sending host Qdisc,  
*   - one skb in sending host TSO/GSO engine  
*   - one skb being received by receiver host LRO/GRO/delayed-ACK engine  
..  
    /* Allow enough full-sized skbs in flight to utilize end systems. */  
   cwnd += 3 * bbr->tso_segs_goal;

8.關於我的Pacing傳送引擎

我在今年1月份寫了一版和TCP BBR相結合的Pacing傳送引擎,以消除FQ對RTT測量值(增加排隊延遲)的影響,詳見:

徹底實現Linux TCP的Pacing傳送邏輯-普通timer版
徹底實現Linux TCP的Pacing傳送邏輯-高精度hrtimer版

個人覺得我這個要比FQ那個好很多,畢竟是原湯化原食的做法吧。

直接在TCP層做Pacing其實並不那麼Cheap,因為三十多年來,TCP並沒有特別嚴重的Buffer bloat問題,所以TCP的核心框架實現幾乎都是突發資料包的,完全靠ACK來驅動傳送,這個TCP核心框架比較類似一個令牌桶,而不是一個整型器!

令牌桶:決定能不能傳送;
整型器:決定如何傳送資料,是突發還是Pacing傳送;

可見這兩者是完全不同的機制!要想把一個改成另一個,這個重構的工作量是可想而知。因此我實現的那個TCP Pacing只是一個簡版。

真正要做得好的話,勢必要重構TCP傳送佇列的操作策略,比如出隊,入隊,排程策略。

現階段,我們能使用的一個穩定版本的Pacing替代方案就是FQ,我們看看Linux的註釋怎麼說:

/* Set the sk_pacing_rate to allow proper sizing of TSO packets.  
 * Note: TCP stack does not yet implement pacing.  
 * FQ packet scheduler can be used to implement cheap but effective  
 * TCP pacing, to smooth the burst on large writes when packets  
 * in flight is significantly lower than cwnd (or rwin)  
 */

結語

今天是週六,白天我折騰了一天工作,結果沒有什麼結果,也算認了。我又不能讓這麼一天就這麼過去,於是我去超市買了一瓶真露,回到家看了個系列紀錄片(關於甲午戰爭的),然後寫完並補充了這篇文章,唉,一想到天亮我就倍感恐懼,老婆一天都要去代課,小小下午還有排練和培訓,家裡還有一大堆掛件安裝工作…

相關文章