天天看點

RACK為TCP BBR提供動力源

先從需求談起吧。

        在上一篇文章《 Google's BBR TCP擁塞控制算法的四個變速引擎》的最後,我提到bbr算法作為一個标稱功率十足的引擎需要源源不斷的能源供給,而這類能源就是資料包。又提到,TCP的快速重傳機制幾乎隻會将判斷為LOST的資料包重傳一次,是以當重傳資料包再次丢失,滑動視窗無法滑動的時候,将會無法提供資料包發送,bbr引擎就會失速,此時隻能等待TCP的逾時!當然,逾時的代價有點大,不單單是對于擁塞控制而言,對于整個連接配接而言,都無異于異常劫難!

        是以,bbr需要源源不斷的資料包供給它開大馬力運作,bbr并不在乎這些資料包是新資料包,标記為LOST的資料包,重傳過的資料包,甚至是構造的錯包...隻要有資料包即可!bbr真正徹底實作了擁塞控制與資料包标記/發送之間解耦合。

        說完了需求,再談方案。

        Linux核心在bbr之前就已經引入了RACK機制,旨在快速發現并重傳那些曾經重傳後再次丢失的資料包,這些資料包的處理至關重要,如果不及時處理,便會陷入RTO的深淵。當然,RTO的回調在你自己手上,你也可以處理得不那麼激進,然而對于已經失速的連接配接,繼續勉強撐着也算是自欺欺人了...RACK解決了這個問題。然而bbr之前的RACK并沒有獲得最大收益,原因在于雖然RACK可以即時地探知哪些資料包丢失,特别是那些重傳後再次丢失的資料包,但是由于此時擁塞視窗的計算已經固定地朝着ssthresh的方向PRR跌落下去,受限于擁塞視窗,即時準備再多的可發送資料也無法着實發送出去!bbr引擎可以即時消化掉RACK送上來的能源,二者配合就開啟了一台動力十足的高端引擎。

        RACK(請先看 這個draft)從名字上看,Recent ACK,即最近的ACK,當然也包括SACK,是以正确的名字應該是Recent (s)ACK,RACK并不記錄資料被(s)ACK的時間,而是在收到ACK的時候,記錄被該ACK确認的資料包的發送時間,在這些發送時間中取Recent,即最晚發送的。RACK的思想是,記錄這個Recent (s)ACK所确認資料包的發送時間T.rack,然後給定一個時間視窗twin,在時間T.rack-twin之前發送的未被确認的資料包均被标記為LOST,然後這些資料包會被交給發送邏輯去發送。這非常符合常理。

        RACK的代碼超級簡單,核心邏輯就一個檔案兩個函數,位于net/ipv4/tcp_recovery.c中的:

/*
 * 在一次(s)ACK的處理過程中更新被确認的資料包的最晚發送時間rack.mstamp。
 * xmit_time-目前處理的被确認的資料包的發送時間
 * sacked-目前處理的被确認的資料包的sacked标記,被選擇确認過嗎?被重傳過嗎?等等。
 */
void tcp_rack_advance(struct tcp_sock *tp, const struct skb_mstamp *xmit_time, u8 sacked);
/*
 * 根據tcp_rack_advance記錄的最晚發送的被确認的資料包的發送時間rack.mstamp以及
 * 重傳隊列裡未被選擇确認的資料包的發送時間skb.mstamp的差,判斷是否标記為LOST。
 * RACK内置有一個twin,凡是符合rack.mstamp-skb.mstamp>twin的資料包,均标記為LOST。
 * 如果該資料包被重傳過,那麼清除其被重傳過的印記!
 */
int tcp_rack_mark_lost(struct sock *sk);
           

以上就是關于RACK的兩個接口,TCP在處理ACK的時候會調用這兩個接口:

1).處理ACK攜帶的資訊(TCP頭的ACK号或者選項中sACK塊)時調用tcp_rack_advance;

2).在發送ACK并非順序ACK時,進入異常Alert時,調用tcp_rack_mark_lost。

這個RACK機制的簡單性就在于,它不再區分正常的順序ACK以及SACK,它隻比較時間戳,不管發送順序如何,隻基于确認攜帶的資訊來決定一個資料包是不是要被重傳。以下面的序列為例:

1|2|3|4|5|6|7|8|

假設一個ACK确認了4,那麼UNA則是5,假設這個ACK沒有攜帶SACK資訊,隻是确認了4,那麼rack.mstamp就是4發送的時間了,現在的問題是,4之後的5,6,7,8怎麼可能在4之前發送呢?它明明是位于4之後的啊!RACK的簡單性就展現在這裡!5,6,7,8雖然在4後面但是誰也不能保證它們就一定是按照序列号順序發送的,更加合理的做法是記錄發送時間序列!典型的場景是,4,5,6,7,8均是重傳過的資料包,首先重傳了7,8,然後重傳了6,然後重傳4,最後重傳5,這樣發送的時間序列就是:

1|2|3|7|8|6|4|5|

現在4被确認了,按照上述時間序,我有理由繼續等待5的确認到來,因為5是在4之後發送的,然而7,8,6卻都是在4之前發送的,到底等不等呢?這裡7是最先發送的,要判斷skb7.mstamp與rack.mstamp之間的內插補點了,如果大于twin,說明繼續等待7的選擇确認是不可忍受的,反之,如果在twin之内,那麼便有理由繼續等待了,可能是亂序了!同樣的政策處理8和6。

        這樣處理是不是更加簡單些呢?隻需要按照時間序比較資料包最近一次的發送時間與rack.mstamp之間的差即可!完全忽略這個資料包是不是曾經被重傳過,這樣就解決了按照序列号進行LOST判斷的複雜性問題。

        然而,在亂序的情況下,你可能會認為RACK機制可能會誤傳很多并未丢失(實際上是亂序到達或者ACK亂序回報)的資料包,事實上這裡就展現了twin時間視窗的作用,RACK的時間序并非嚴格的時間序,它是一個帶有緩沖的準時間序機制。就算你認為twin也沒用,再不濟你也可以将RACK關掉的!

        注意twin的選擇,一般而言是最小RTT的1/4,這裡的最小RTT是與SRTT無關的,它是真實測量出來的離散RTT的win_minmax(請在《 Google's BBR TCP擁塞控制算法的四個變速引擎》看win_minmax詳情)最小值,基于一個自動向後滑動的時間視窗采樣的最小RTT,這裡的主要目的不是平滑掉噪點(這有點像鴕鳥政策...),而是過濾掉非擁塞導緻的抖動,這是一種主動發現噪點的行為,可以将 BufferBloat的影響最小化。

有了RACK機制,bbr再也不用為無包可發這種事發愁了。

        隻要bbr可以根據(s)ACK采集到帶寬和RTT,那麼bbr就可以根據這些帶寬,RTT的回報全速率運作,而能讓帶寬和RTT回報回來的,正是發出去的包,再次重申,無論是新包還是重傳包,隻要發送去,它們都可以回報回結果,不管是ACK,SACK,還是DSACK...我們又繞回來了,RACK機制即使在已經發生丢包/亂序等事件時也能提供源源不斷的可發送的資料包(即标記為LOST的資料包)。這個過程平滑的運作,不必再等待RTO逾時!

        在bbr之前,一旦發生丢包或者嚴重亂序,TCP就會接管擁塞控制算法,但現在不了!以前的做法是錯誤的,所謂的丢包,亂序,這些都是TCP的擁塞控制狀态機邏輯自己猜的,相當大程度是不真實的,任何算法都無法準确猜出是不是真的發生了丢包,想欺騙一隻蝙蝠撞牆是很容易的,同樣,TCP也是一個瞎子!是以,bbr的做法是正确的:

bbr算法本身:計算發送速率和視窗。

TCP擁塞控制狀态機:準備新資料,标記LOST(傳統方式以及RACK方式),即提供可傳輸的資料包,灌入bbr算法提供的食道。

TCP傳輸邏輯:實際傳輸任何可以傳輸的資料包,新的資料包,所有标記為LOST的資料包。

以上3者互相配合,回答了 ”傳輸多少?“,”傳輸什麼?“,”怎麼傳輸?“等問題,并且三者之間完全基于當下的(s)ACK回報來互動,彼此之間則完全獨立。

...

接下來幹什麼?

接下來我來吐個槽,事情要從工業革命後,人們企圖将蒸汽機裝在馬車上開始...

繼續閱讀