怎麼讓不可靠的UDP可靠

最近和很多實時音視頻領域的朋友交流中都有談論到 RUDP(Reliable UDP),這其實是個老生常談的問題,RUDP 在很多著名的項目上都有使用,例如 Google 的 QUIC 和 webRTC。在 UDP 之上做一層可靠,很多朋友認為這是很不靠譜的事情,也有朋友認為這是一個大殺器,可以解決實時領域裡大部分問題。

作為教育公司,學霸君在很多實時場景下確實使用 RUDP 技術來解決我們的問題,不同場景我們採用的 RUDP 方式也不一樣。先來看看學霸君哪些場景使用了 RUDP:

全局 250 毫秒延遲的實時 1V1 答疑,採用的是 RUDP + 多點 relay 智能路由方案。

500 毫秒 1080P 視頻連麥互動系統,採用的是 RUDP + PROXY 調度傳輸方案。

6 方實時同步書寫系統,採用的是 RUDP+redo log 的可靠傳輸技術。

弱網 Wi-Fi 下 Pad 的 720P 同屏傳輸系統,採用的是 RUDP +GCC 實時流控技術。

大型直播的 P2P 分發系統,通過 RUDP + 多點並聯 relay 技術節省了 75% 以上的分發帶寬。

涉及到實時傳輸我們都會先考慮 RUDP,RUDP 應用在學霸君核心傳輸體系的各個方面,但不同的系統場景我們設計了不同的 RUDP 方式,所以基於那些激烈的討論和我們使用的經驗我扒一扒 RUDP。

三角制約

其實在實時通信領域存在一個三角平衡關係:成本、質量和時延三者的制約關係:

怎麼讓不可靠的UDP可靠

圖 1

也就是說投入的成本、獲得的質量和通信的時延之間是一個三角制約 (LEQ) 關係,所以實時通信系統的設計者會在這三個制約條件下找到一個平衡點,TCP 屬於通過增大延遲和傳輸成本來保證質量的通信方式,UDP 是通過犧牲質量來保證時延和成本的通信方式,所以在一些特定場景下 RUDP 更容易找到這樣的平衡點。RUDP 是怎麼去找這個平衡點的,就要先從 RUDP 的可靠概念和使用場景來分析。

可靠的概念

在實時通信過程中,不同的需求場景對可靠的需求是不一樣的,我們在這裡總體歸納為三類定義:

盡力可靠:通信的接收方要求發送方的數據儘量完整到達,但業務本身的數據是可以允許缺失的。例如:音視頻數據、冪等性狀態數據。

無序可靠:通信的接收方要求發送方的數據必須完整到達,但可以不管到達先後順序。例如:文件傳輸、白板書寫、圖形實時繪製數據、日誌型追加數據等。

有序可靠:通信接收方要求發送方的數據必須按順序完整到達。

RUDP 是根據這三類需求和圖 1 的三角制約關係來確定自己的通信模型和機制的,也就是找通信的平衡點。

UDP 為什麼要可靠

說到這裡可能很多人會說:幹嘛那麼麻煩,直接用 TCP 好了! 確實很多人也都是這樣做的,TCP 是個基於公平性的可靠通信協議,但是在一些苛刻的網絡條件下 TCP 要麼不能提供正常的通信質量保證,要麼成本過高。為什麼要在 UDP 之上做可靠保證,究其原因就是在保證通信的時延和質量的條件下儘量降低成本,RUDP 主要解決以下相關問題:

端對端連通性問題:一般終端直接和終端通信都會涉及到 NAT 穿越,TCP 在 NAT 穿越實現非常困難,相對來說 UDP 穿越 NAT 卻簡單很多,如果是端到端的可靠通信一般用 RUDP 方式來解決,場景有:端到端的文件傳輸、音視頻傳輸、交互指令傳輸等等。

弱網環境傳輸問題:在一些 Wi-Fi 或者 3G/4G 移動網下,需要做低延遲可靠通信,如果用 TCP 通信延遲可能會非常大,這會影響用戶體驗。例如:實時的操作類網遊通信、語音對話、多方白板書寫等,這些場景可以採用特殊的 RUDP 方式來解決這類問題。

帶寬競爭問題:有時候客戶端數據上傳需要突破本身 TCP 公平性的限制來達到高速低延時和穩定,也就是說要用特殊的流控算法來壓榨客戶端上傳帶寬,例如:直播音視頻推流,這類場景用 RUDP 來實現不僅能壓榨帶寬,也能更好地增加通信的穩定性,避免類似 TCP 的頻繁斷開重連。

傳輸路徑優化問題:在一些對延時要求很高的場景下,會用應用層 relay 的方式來做傳輸路由優化,也就是動態智能選路,這時雙方採用 RUDP 方式來傳輸,中間的延遲進行 relay 選路優化延時。還有一類基於傳輸吞吐量的場景,例如:服務與服務之間數據分發、數據備份等,這類場景一般會採用多點並聯 relay 來提高傳輸的速度,也是要建立在 RUDP 上的(這兩點在後面著重來描述)。

資源優化問題:某些場景為了避免 TCP 的三次握手和四次揮手的過程,會採用 RUDP 來優化資源的佔用率和響應時間,提高系統的併發能力,例如 QUIC。

不管哪類場景,都是要保證可靠性,也就是質量,那麼在 UDP 之上怎麼實現可靠呢?答案就是重傳。

重傳模式

IP 協議在設計的時候就不是為了數據可靠到達而設計的,所以 UDP 要保證可靠,就依賴於重傳,這也就是我們通常意義上的 RUDP 行為,在描述 RUDP 重傳之前先來了解下 RUDP 基本框架,如圖:

怎麼讓不可靠的UDP可靠

圖 2

RUDP 分為發送端和接收端,每一種 RUDP 在設計的時候會做不一樣的選擇和精簡,概括起來就是圖中的單元。RUDP 的重傳是發送端通過接收端 ACK 的丟包信息反饋來進行數據重傳,發送端會根據場景來設計自己的重傳方式,重傳方式分為三類:定時重傳、請求重傳和 FEC 選擇重傳。

定時重傳

定時重傳很好理解,就是發送端如果在發出數據包(T1)時刻一個 RTO 之後還未收到這個數據包的 ACK 消息,那麼發送端就重傳這個數據包。這種方式依賴於接收端的 ACK 和 RTO,容易產生誤判,主要有兩種情況:

對方收到了數據包,但是 ACK 發送途中丟失。

ACK 在途中,但是發送端的時間已經超過了一個 RTO。

所以超時重傳的方式主要集中在 RTO 的計算上,如果你的場景是一個對延遲敏感但對流量成本要求不高的場景,就可以將 RTO 的計算設計得比較小,這樣能盡較大可能保證你的延時足夠小。

例如:實時操作類網遊、教育領域的書寫同步,是典型的用 expense 換 latency 和 quality 的場景,適合用於小帶寬低延遲傳輸。如果是大帶寬實時傳輸,定時重傳對帶寬的消耗是很大的,極端情況會有 20% 的重傳率,所以在大帶寬模式下一般會採用請求重傳模式。

請求重傳

請求重傳就是接收端在發送 ACK 的時候攜帶自己丟失報文的信息反饋,發送端接收到 ACK 信息時根據丟包反饋進行報文重傳。如下圖:

怎麼讓不可靠的UDP可靠

圖 3

這個反饋過程最關鍵的步驟就是回送 ACK 的時候應該攜帶哪些丟失報文的信息,因為 UDP 在網絡傳輸過程中會亂序會抖動,接收端在通信的過程中要評估網絡的 jitter time,也就是 rtt_var(RTT 方差值),當發現丟包的時候記錄一個時刻 t1,當 t1 + rtt_var < curr_t(當前時刻),我們就認為它丟失了。

這個時候後續的 ACK 就需要攜帶這個丟包信息並更新丟包時刻 t2,後續持續掃描丟包隊列,如果 t2 + RTO

這種方式是由丟包請求引起的重發,如果網絡很不好,接收端會不斷髮起重傳請求,造成發送端不停的重傳,引起網絡風暴,通信質量會下降,所以我們在發送端設計一個擁塞控制模塊來限流,這個後面我們重點分析。

整個請求重傳機制依賴於 jitter time 和 RTO 這個兩個時間參數,評估和調整這兩個參數和對應的傳輸場景也息息相關。請求重傳這種方式比定時重傳方式的延遲會大,一般適合於帶寬較大的傳輸場景,例如:視頻、文件傳輸、數據同步等。

FEC 選擇重傳

除了定時重傳和請求重傳模式以外,還有一種方式就是以 FEC 分組方式選擇重傳,FEC(Forward Error Correction)是一種前向糾錯技術,一般通過 XOR 類似的算法來實現,也有多層的 EC 算法和 raptor 湧泉碼技術,其實是一個解方程的過程。應用到 RUDP 上示意圖如下:

怎麼讓不可靠的UDP可靠

圖 4

在發送方發送報文的時候,會根據 FEC 方式把幾個報文進行 FEC 分組,通過 XOR 的方式得到若干個冗餘包,然後一起發往接收端,如果接收端發現丟包但能通過 FEC 分組算法還原,就不向發送端請求重傳,如果分組內包是不能進行 FEC 恢復的,就向發送端請求原始的數據包。

FEC 分組方式適合解決要求延時敏感且隨機丟包的傳輸場景,在一個帶寬不是很充裕的傳輸條件下,FEC 會增加多餘的包,可能會使得網絡更加不好。FEC 方式不僅可以配合請求重傳模式,也可以配合定時重傳模式。

RTT 與 RTO 的計算

在上面介紹重傳模式時多次提到 RTT、RTO 等時間度量,RTT(Round Trip Time)即網絡環路延時,環路延遲是通過發送的數據包和接收到的 ACK 包計算的,示意圖如下:

怎麼讓不可靠的UDP可靠

圖 5

RTT = T2 - T1

這個計算方式只是計算了某一個報文時刻的 RTT,但網絡是會波動的,這難免會有噪聲現象,所以在計算的過程中引入了加權平均收斂的方法(具體可以參考 RFC793)

SRTT = (α * SRTT) + (1-α)RTT

這樣可以求得逼近的 SRTT,在公式中一般α=0.8,確定了 SRTT,下一步就是計算 RTT_VAR(方差),我們設 RTT_VAR = |SRTT – RTT|,那麼 SRTT_VAR =(α * SRTT_VAR) + (1-α) RTT_VAR,這樣可以得到 RTT_VAR 的值。

但最終我們是需要 RTO,因為涉及到報文重傳,RTO 就是一個報文的重傳週期,從網絡的通信流程我們很容易知道,重傳一個包以後,如果一個 RTT+RTT_VAR 之後的時間還沒收到確定,那我們就可以再次重傳,則可知:RTO = SRTT + SRTT_VAR。

但一般網絡在嚴重抖動的情況下還是會有較大的重複率問題,所以:RTO = β*(SRTT + RTT_VAR),1.2

窗口與擁塞控制

RUDP 是通過重傳來保證可靠的,重傳在三角平衡關係中其實是用 expense 和 latency 來換取 quality 的行為,所以重傳會引來兩個問題,一個是延時,一個是重傳的帶寬,尤其是後者,如果控制不好會引來網絡風暴,所以在發送端會設計一個窗口擁塞機制了避免併發帶寬佔用過高的問題。

窗口

RUDP 需要一個收發的滑動窗口系統來配合對應的擁塞算法做流量控制,有些 RUDP 需要發送端和接收端的窗口嚴格地對應,有些 RUDP 不要求收發窗口嚴格對應。如果涉及到可靠有序的 RUDP,接收端就要做窗口排序和緩衝,如果是無序可靠或者盡力可靠的場景,接收端一般就不做窗口緩衝,只做位置滑動。先來看收發窗口關係圖:

怎麼讓不可靠的UDP可靠

圖 6

上圖描述的是發送端從發送窗口中發了 6 個數據報文給接收端,接收端收到 101,102,103,106 時會先判斷報文的連續性並滑動窗口開始位置到 103,接著每個包都回應 ACK,發送端在接收到 ACK 的時候,會確認報文的連續性,並滑動窗口到 103,發送端會再判斷窗口的空餘,然後填補新的發送數據,這就是整個窗口滑動的流程。

這裡值的一提的是在接收端收到 106 時的處理,如果是有序可靠,那麼 106 不會通知上層業務進行處理,而是等待 104、105。如果是盡力可靠和無序可靠場景,會將 106 通知給上層業務先進行處理。在收到 ACK 後,發送端的窗口要滑動多少是由自己的擁塞機制定的,也就是說窗口的滑動速度受擁塞機制控制,擁塞控制實現要麼基於丟包率來實現,要麼基於雙方的通信時延來實現,下面來看幾種典型的擁塞控制。

經典擁塞算法

TCP 經典擁塞算法分為四個部分:慢啟動、擁塞避免、擁塞處理和快速恢復,這四個部分都是為了決定發送窗口和發送速度而設計的,其實就是為了在當前網絡條件下通過網絡丟包來判斷網絡擁塞狀態,從而確定比較適合的發送傳輸窗口。

經典算法是建立在定時重傳的基礎上的,如果 RUDP 採用這種算法來做擁塞控制,一般的場景是為了保證有序可靠傳輸的同時又兼顧網絡傳輸的公平性原則。先逐個來解釋下這幾部分。

慢啟動(slow start)

當連接鏈路剛剛建立後,不可能一開始將 cwnd 設置得很大,這樣容易造成大量重傳,經典擁塞裡面會在開始將 cwnd = 1,然後根據通信過程的丟包情況來逐步擴大 cwnd 來適應當前的網絡狀態,直到達到慢啟動的門限閾值 (ssthresh),步驟如下:

1、初始化設置 cwnd = 1,並開始傳輸數據。

2、收到回饋的 ACK,會將 cwnd 加 1。

3、當發送端一個 RTT 後且未發現有丟包重傳,就會將 cwnd = cwnd * 2。

4、當 cwnd >= ssthresh 或發生丟包重傳時慢啟動結束,進入擁塞避免狀態。

擁塞避免

當通信連接結束慢啟動後,有可能還未到網絡傳輸速度的上線,這個時候需要進一步通過一個緩慢的調節過程來進行適配。一般是一個 RTT 後如果未發現丟包,就將 cwnd = cwnd + 1。一但發現丟包和超時重傳,就進入擁塞處理狀態。

擁塞處理

擁塞處理在 TCP 裡面實現很暴力,如果發生丟包重傳,直接將 cwnd = cwnd / 2,然後進入快速恢復狀態。

快速恢復

通過確認丟包只發生在窗口一個位置的包來確定是否進行快速恢復,如圖 6 中描述,如果只是 104 發生了丟失,而 105 和 106 是收到了的,那麼 ACK 總是會將 ACK 的 base = 103,如果連續 3 次收到 base 為 103 的 ACK,就進行快速恢復,也就是立即重傳 104,而後如果收到新的 ACK 且 base > 103,將 cwnd = cwnd + 1,並進入擁塞避免狀態。

經典擁塞控制是基於丟包檢測和定時重傳模式來設計的,在三角平衡關係中是一個典型的以 latency 換取 quality 的案例,但由於其公平性設計避免了過高的 expense,也就會讓這種傳輸方式很難壓榨網絡帶寬,很難保證網絡的大吞吐量和小時延。

BBR 擁塞算法

對於經典擁塞算法的延遲和帶寬壓榨問題 Google 設計了基於發送端延遲和帶寬評估的 BBR 擁塞控制算法。這種擁塞算法致力於解決兩個問題:

在一定丟包率網絡傳輸鏈路上充分利用帶寬

降低網絡傳輸中的 buffer 延遲

BBR 的主要策略是週期性通過 ACK 和 NACK 返回來評估鏈路的 min_rtt 和 max_bandwidth。較大吞吐量(cwnd)的大小就是:cwnd = max_bandwidth / min_rtt。

傳輸模型如下:

怎麼讓不可靠的UDP可靠

圖 7

BBR 整個擁塞控制是一個探測帶寬和 Pacing rate 的狀態,有 4 個狀態:

Startup:啟動狀態(相當於慢啟動),增益參數為 max_gain = 2.85

DRAIN:滿負荷傳輸狀態

PROBE_BW:帶寬評估狀態,通過一個較小的 BBR 增益參數來遞增(1.25)或者遞減 (0.75)

PROBE_RTT:延遲評估狀態,通過維持一個最小發送窗口(4 個 MSS)進行的 RTT 採樣

那麼這幾種狀態是怎麼來回切換的呢?以下是 QUIC 中 BBR 大致的步驟如下:

1、初始化連接時會設置一個初始的 cwnd = 8,並將狀態設置 Startup。

2、在 Startup 下發送數據,根據 ACK 數據的採樣週期性判斷是否可以增加帶寬,如果可以,將 cwnd = cwnd *max_gain。如果時間週期數超過了預設的啟動週期時間或者發生了丟包,進行 DRAIN 狀態。

3、在 DRAIN 狀態下,如果 flight_size(發送出去但還未確認的數據大小) >cwnd, 繼續保證 DRAIN 狀態,如果 flight_size

4、在PROBE_BW狀態下,如果未發生丟包且flight_size cwnd,將cwnd = cwnd * 1.25,如果發生丟包,cwnd = cwnd * 0.75。

5、在 Startup/DRAIN/PROBE_BW 三個狀態中,如果持續 10 秒鐘的通信中沒有出現 RTT <= min_rtt,就會進入到 PROBE_RTT 狀態,並將 cwnd = 4 *MSS。

6、在 PROBE_RTT 狀態,會在收到 ACK 返回的時候持續判斷 flight_size >= cwnd 並且無丟包,將本次統計的最小 RTT 作為 min_rtt,進入 Startup 狀態。

BBR 通過以上幾個步驟來週期性計算 cwnd,也就是網絡較大吞吐量和最小延遲,然後通過 pacing rate 來確定這一時刻發送端的碼率,最終達到擁塞控制的目的。

BBR 適合在隨機丟包且網絡穩定的情況下做擁塞,如果在網絡信號極不穩定的 Wi-Fi 或者 4G 上,容易出現網絡泛洪和預測不準的問題,BBR 在多連接公平性上也存在小 RTT 的連接比大 RTT 的連接更吃帶寬的情況,容易造成大 RTT 的連接速度過慢的情況。BBR 擁塞算法在三角平衡關係中是採用 expense 換取 latency 和 quality 的案例。

webRTC GCC

說到音視頻傳輸就必然會想到 webRTC 系統,在 webRTC 中對於視頻傳輸也實現了一個擁塞控制算法 (GCC),webRTC 的 GCC 是一個基於發送端丟包率和接收端延遲帶寬統計的擁塞控制,而且是一個盡力可靠的傳輸算法,在傳輸的過程中如果一個報文重發太多次後會直接丟棄,這符合視頻傳輸的場景,借用 weizhenwei 同學一張圖來看個究竟:

怎麼讓不可靠的UDP可靠

圖 8,來源 http://www.jianshu.com/u/102fafe8c6b9

GCC 的發送端會根據丟包率和一個對照表來 pacing rate,當 loss < 2% 時,會加大傳輸帶寬,當 loss >=2% &&loss <10%,會保持當前碼率,當 loss>=10%,會認為傳輸過載,進行調小傳輸帶寬。

GCC 的接收端是根據數據到達的延遲方差和大小進行 KalmanFilter 進行帶寬逼近收斂,具體的細節不介紹了,請查看 http://www.jianshu.com/p/bb34995c549a。

這裡值得一說的是 GCC 引入接收端對帶寬進行 KalmanFilter 評估是一個非常新穎的擁塞控制思路,如果實現一個盡力可靠的 RUDP 傳輸系統不失為是一個很好的參考。

但這種算法也有個缺陷,就是在網絡間歇性丟包情況下,GCC 可能收斂的速度比較慢,在一定程度上有可能會造成 REMB 很難反饋給發送端,容易出現發送端流控失效。GCC 在三角平衡關係算一個以 quality 和 expense 換取 latency 的案例。

弱窗口擁塞機制

其實在很多場景是不用擁塞控制或者只要很弱的擁塞控制即可,例如:師生雙方書寫同步、實時遊戲,因為本身的傳輸的數據量不大,只要保證足夠小的延時和可靠性就行,一般會採用固定窗口大小來進行流控,我們在系統中一般採用一個 cwnd =32 這樣的窗口來做流控,ACK 確認也是通過整個接收窗口數據狀態反饋給發送方,簡單直接,也很容易適應弱網環境。

傳輸路徑

RUDP 除了優化連接、壓榨帶寬、適應弱網環境等以外,它也繼承了 UDP 天然的動態性,可以在中間應用層鏈路上做傳輸優化,一般分為多點串聯優化和多點並聯優化。我們具體來說一說。

多點串聯 relay

在實時通信中一些業務場景對延遲非常敏感,例如:實時語音、同步書寫、實時互動、直播連麥等,如果單純的服務中轉或者 P2P 通信,很難滿足其需求,尤其是在物理距離很大的情況下。

在解決這個問題上 SKYPE 率先提出全球 RTN(實時多點傳輸網絡),其實就是在通信雙方之間通過幾個 relay 節點來動態智能選路,這種傳輸方式很適合 RUDP,我們只要在通信雙方構建一個 RUDP 通道,中間鏈路只是一個無狀態的 relay cache 集合,relay 與 relay 之間進行路由探測和選路,以此來做到鏈路的高可用和實時性。如下圖:

怎麼讓不可靠的UDP可靠

圖 9

通過多點 relay 來保證 RUDP 進行傳輸優化,這類場景在三角平衡關係裡是典型的用 expense 來換取 latency 的案例。

多點並聯 relay

在服務與服務進行媒體數據傳輸或者分發過程中,需要保證傳輸路徑高可用和帶寬併發,這類使用場景也會使用傳輸雙方構建一個 RUDP 通道,中間通過多 relay 節點的並聯來解決,如下圖所示:

怎麼讓不可靠的UDP可靠

圖 10

這種模型需要在發送端設計一個多點路由表探測機制,以此來判斷各個路徑同時發送數據的比例和可用性,這個模型除了鏈路備份和增大傳輸併發帶寬外,還有個輔助的功能,如果是流媒體分發系統,我們一般會用 BGP 來做中轉,如果節點與節點之間可以直連,這樣還可以減少對 BGP 帶寬的佔用,以此來減少成本。

後 記

到這裡 RUDP 的介紹也就結束了,說了些細節和場景相關的事,也算是個入門級的科普文章。RUDP 的概念從提出到現在也差不多有 20 年了,很多從業人員希望通過一套完善的方案來設計一個通用的 RUDP,我個人覺得這不太可能,就算設計出來了,估計和現在 TCP 差不多,這樣做的意義不大。

RUDP 的價值在於根據不同的傳輸場景進行不同的技術選型,可能選擇寬鬆的擁塞方式、也可能選擇特定的重傳模式,但不管怎麼選,都是基於 expense(成本)、latency(時延)、quality(質量)三者之間來權衡,通過結合場景和權衡三角平衡關係 RUDP 或許能幫助開發者找到一個比較好的方案。


分享到:


相關文章: