博客 / 詳情

返回

百度網盤防雪崩架構實踐

導讀

大模型在研發效能領域代碼生成方面發揮了越來越大的作用

而大模型的預訓練依賴大量的精標代碼,這些精標數據必須是比較好的工程實踐代碼

這些比較好的工程實踐代碼,需要大量的技術沉澱,包括工程架構,代碼架構等多緯度,涉及性能、可用性、擴展性、安全等方向

百度網盤有不少比較好的工程實踐,本文主要是介紹百度網盤工程架構中的防雪崩架構

拋磚引玉,與大家一起探討什麼才是優秀的工程實踐,為大模型的落地提供堅實的數據基礎

01 背景

1.1 百度網盤業務背景介紹

簡要介紹一下百度網盤的業務背景

  • 外部視角:用户規模超過10億,單純外部用户請求日pv過千億;
  • 內部視角:實例數超過60w+個,模塊數規模過千,單個功能最多涉及100+個模塊節點。

在複雜的鏈路+高併發情況下,短暫的異常就會使得整個系統出現雪崩,即使異常消失也無法自愈,對用户產生不好的用户體驗。

於是百度網盤服務端工程方向設計了一套防雪崩的機制,本文先介紹雪崩的技術背景,再介紹相關的解決方案。

1.2 雪崩發生的背景介紹

雪崩是如何發生的?簡單的説,某種場景下,觸發一個服務的處理能力(容量)不足而拒絕導致請求失敗,而它的上游因為請求失敗而重試,加劇服務持續處理無效請求,無法處理有效請求,從而形成雪崩死循環,服務無法恢復。

如下圖所示,某個變動(根因) –> 服務異常(誘因) –> 鏈路異常(局部雪崩) –\> 全局異常(全局雪崩)

簡單來説,雪崩分成兩個階段:

  • 初始階段:各種根因 + 誘因 –\> 服務過載;
  • 循環階段:上游重試 —> 服務持續處理無效請求 —\> 上游重試 ……

圖片

下面將按系統視角介紹:雪崩是咋發生的?

先從服務接收&處理請求來説,根據網絡TCP三次握手準則,客户端的請求會被放入半連接/全連接隊列中,而服務端從隊列中取出請求進行處理。由於隊列是先進先出的,處理的請求都是之前的請求(即使client已經斷開了連接,但是accept隊列也不會剔除這些斷開連接的請求)。如下圖,client 向serve發了5個請求,r0/r1/r2/r3/r4,這些請求會被放入server的accept隊列裏,但是由於server處理邏輯比較慢,導致client請求超時,斷開了連接r0/r1/r2的連接,但是server這邊還會從accept隊列裏取出r0/r1/r2的連接,進行請求request數據的讀取(即使client已經斷開連接,server還是能夠讀到之前client發送的請求數據),進行無效的處理,於是client不停的重試,而server不停的在處理失效的請求。

圖片

△server讀到已經斷開連接的socket連接

圖片

△ server讀到已經斷開連接的socket請求數據

從上下游處理視角來説,雪崩並不是局部行為,它會隨着鏈路漫延到整個系統鏈路上。如下圖,client以300ms的超時時間訪問server,server在訪問A和B之後,已經用掉了300ms,這個時候client已經斷開了和server的連接,但是server卻繼續訪問C和D,進行無效的請求。當這種無效請求在整個鏈路蔓延開,client又在大量的重試的時候,就是整個系統崩潰的時候。

圖片

02 傳統解決方案

從預防/阻止/止損雪崩三個子方向介紹相關實戰經驗,以及分析它們的優缺點。

2.1 預防

通過各種手段,規避雪崩初始階段的出現,例如熱點治理、長尾治理、分級操作、容量保障等。

2.2 阻止

處於雪崩初始階段,有發生雪崩的可能性,需要阻止雪崩進入循環階段。

【重試率控制】

  • 基本思路:當重試請求數佔當前請求數的X%, 就不會再進行重試;
  • 優點:可以避免大量重試導致雪崩死循環;
  • 不足:設置重試率多少才是合理的?另外,這個還需要持續更新,因為下游的容量是在變化的,可能連正常的流量都無法處理(比如實例被縮容了)。

【隊列控制】

  • 基本思路:不再基於系統層面的內核隊列,應用層自己維護隊列(比如用一個線程從系統讀取連接數據到隊列中),記錄請求在隊列中等待的時間。如下圖,隊列中有8個請求,代表他們在隊列中等待的時間(單位ms),假設上游的執行是200ms,處理一個請求需要100ms,那麼第1個到第4個請求明顯是失效請求(最大等待時間( = 上游執行超時 - 服務處理時間) < 當前等待時間),等處理完上游都已經斷開連接了,就不需要處理了,可以直接從第5個請求進行處理;

圖片

  • 優點:通過維護應用層的隊列,加上等待時間,快速拋棄無效請求,避免雪崩的發生;
  • 不足:依賴假設上游的訪問超時時間以及自己的執行時間,才能準確判斷出請求是否已經失效。但是有可能不同的上游的超時時間不一樣,自身的執行時間也不穩定性。同在服務在極端負載下,包括讀取系統隊列的線程也會受限資源,無法及時從系統隊列中讀取請求,導致讀到了無效請求。

【限流】

  • 基本思路:在服務接入層設置一個靜態閾值,表示後端服務最大的請求qps,超過這個qps的流量就丟棄請求;
  • 優點:可以避免瞬間突增的流量打垮服務,導致服務不可恢復,可以解決一部分場景問題;
  • 不足:提前設置限流一個靜態閾值,和重試率控制一樣,不是所有場景都有效果。另外也有運維維護成本,具體如下:
  • 閾值合理性:需要頻繁壓測才能知道當前系統的瓶頸qps閾值是多少,設置大了,達不到阻止過多流量的效果,設置小了,又有誤傷;
  • 故障期間服務處理能力退化:當花費了很多時間壓測出來一個系統的qps閾值是100,但是故障期間服務承載的qps可能會退化成80。會有很多原因會導致,比如一部分實例oom導致不可用,上線導致實例啓動失敗、長尾流量導致部分實例處理能力不足(比如某些視頻轉碼需要更長的時間,同時還是熱門視頻,負載均衡重試到不同實例上),最常見的是下游請求執行時間變長了。這樣導致每秒都需要承載多餘的qps請求,累計下來,這個系統遲早被打垮。本質問題還是靜態閾值導致的。

2.3 止損

通過各種手段,加速雪崩止損恢復,例如:

  • 限流:通過多次人工調整限流閾值,使後端請求減少,逐漸恢復;
  • 重啓服務:通過重啓服務,快速丟棄系統隊列中的無效請求。

屬於兜底的機制,缺點是整個恢復週期會比較長。

03 解決方案

傳統解決方案裏,包括預防、阻止、止損三個子方向。

一旦處於雪崩狀態,是不可逆的,需要比較長的時間去恢復。

所以重點在於預防和阻止,因為預防會有遺漏,更多的精力在於阻止,即處於雪崩初始階段,有發生雪崩的可能性,需要阻止雪崩進入循環階段。

業界的做法,其實可以分成兩種做法:

  • 減少過載流量:比如限流和重試率控制,剔除服務不能夠承載的流量;
  • 減少無效請求:比如隊列控制,使得服務處理有效的流量。

這兩種做法需要結合,單個做法是會有問題的。

比如減少過載流量,由於下游的處理能力是動態變化的,實際上還是會出現過載,下游會長期處於處理無效請求,無法處理有效的請求。

減少無效請求,大部分場景下是能搞定的,但是如果瞬間的請求量特別大,請求都是屬於有效的,內存可能會先達到瓶頸,導致oom了。

下面介紹一下百度網盤的防雪崩架構實踐。

3.1 減少過載流量-基於動態熔斷

【基本思路】

業界對減少過載流量,除了上面説的靜態限流之外,還有一種熔斷做法,具體流程如下:

流程説明
1 開始請求: 系統接收到一個外部請求。
2 熔斷器狀態判斷:
- 閉合狀態(Closed): 熔斷器允許請求通過,繼續執行。
- 打開狀態(Open): 熔斷器阻止請求,直接失敗或返回預定義的響應。
- 半開狀態(Half-Open): 熔斷器允許部分請求通過,以測試服務是否恢復。
3 執行請求:
- 請求成功:
    - 如果處於閉合狀態,則重置失敗計數。
    - 如果處於半開狀態,則關閉熔斷器,恢復正常。
- 請求失敗:
    - 增加失敗計數。
    - 如果失敗計數超過預設閾值,則打開熔斷器,跳閘。
4 冷卻時間: 熔斷器在打開狀態後,會等待一段冷卻時間,然後進入半開狀態。
5 半開狀態測試:
- 請求成功: 關閉熔斷器,恢復正常。
- 請求失敗: 重新打開熔斷器,繼續等待冷卻時間。


這個熔斷機制和限流一樣的,都是存在靜態的缺陷問題。

舉個例子,當前流量的qps為100,後端只能承載10的qps。

當後端失敗率超過閾值之後,觸發打開狀態,然後等待一段時間之後,進行半開狀態,用一部分流量進行測試。

本質上是不能動態根據後端的處理能力進行流量限制轉發,所以需要實現動態熔斷限流。

【具體實踐】

其核心思路是根據下游的請求成功率動態限制轉發到下游的請求數。具體如下圖所示,先隨機丟棄X%比例的請求,然後進行檢測,判斷服務是否恢復,如果還未恢復,説明需要繼續降低轉發到下游的請求數,設置X = X + Step,增大丟棄請求的比例,繼續熔斷限流。如果已經恢復了,則判斷丟棄請求的比例是否已經降低到0(即X是否為0),如果X不為0,則還需要繼續減少丟棄請求的比例,設置X = X – Step,繼續熔斷限流,如果X為0則説明整體已經恢復,則結束動態熔斷限流。

動態熔斷的思想是借鑑了網絡,當雪崩過載的時候,相當於發生了請求的擁塞,和網絡擁塞是一樣的特徵行為,網絡鏈路都帶寬相當於服務的容量。

圖片

圖片

這裏面其實還有一種問題沒法解決,即ddos攻擊。

這種一般需要有一個統一的接入層來解決,設置一個相對大的限流閾值,然後通過動態熔斷來轉給後端的業務。

3.2 減少過載流量-基於流量隔離

動態熔斷策略相對複雜,還有一種簡單粗暴的方式,即流量隔離。

適用於流量來源存在不同級別的,而且高優流量常態下比較穩定,低優流量有突增的情況。

【基本思路】

將流量進行分級,通過部署隔離解決高低優流量相互影響的問題,即使低優流量再怎麼增加也不影響高優流量。

  • 高優流量:比如用户感知明顯流量等;
  • 低優流量:比如後台流量、離線計算流量等。

【具體實踐】

首先是client需要打上流量標籤,其次是gateway or service mesh基於這些流量標籤進行相關的流量轉發。

3.3 減少無效請求-基於請求有效性

【基本思路】

上游服務A訪問下游服務B的請求時間可以由以下部分組成:

請求耗時 = 上游發送請求 + 網絡傳輸 + 下游tcp建連隊列等待時間 + 下游處理時間

  • 上游服務A發送請求:基本可以忽略,機器負載問題不再這裏考慮;
  • 請求在網絡中的耗時:基本可以忽略,網絡故障問題不再這裏考慮;
  • 請求在下游服務B系統隊列中的等待時間:堆積太多請求會導致請求失效;
  • 下游服務B的處理耗時:處理的慢導致請求長期阻塞在系統隊列中。

不再基於單獨維護一個隊列的思想去解決問題。單獨隊列一方面是需要支持不同語言,以及準確性不足,在虛擬化的環境裏,資源受限之後,隊列的等待時間就不準確了,還有上游不同的超時時間,沒法簡單判斷是否已經超時了。

這裏面的關鍵點是上游傳遞一個截止時間給下游,但是如何表示該請求截止時間呢,具體有絕對時間和相關時間兩種方式,相關缺點如下:

  • 絕對時間:用絕對時間戳表示(比如1577681783),但是不同設備上的時鐘不一致,如下圖A和B兩台機器的時鐘差了2分鐘,A訪問B的超時時間是5s,於是請求發到機器B上就直接超時失敗了,不符合預期;
  • 相對時間:用相對時間表示(比如5s),但是這個時間是從業務接收到請求開始計時的,沒有包括在系統隊列中的時間,有可能已經等待了很久的時間了,上游已經斷開了連接,不符合預期;

圖片

△絕對時間的問題

圖片

△相對時間的問題

【具體實踐】

從上文可知有這麼兩個問題,絕對時間和相對時間解決其中之一:

  • 機器之間的系統時鐘不一致
  • 需要關注在系統隊列中的等待時間

於是可以將絕對時間和相對時間結合在一起,藉助UFC(百度網盤的Service Mesh,詳見之前的一些分享Service Mesh在百度網盤數萬後端的實踐落地)來解決問題,以下圖流程為例子説明:

  • Host1機器上的服務A訪問Host2機器上的服務B;
  • 服務A先訪問本地的UFC-agent,傳遞相對超時時間為5S;
  • Host1機器上UFC-agent 訪問Host2機器上的UFC-agent,傳遞相對超時時間為5S;
  • Host2機器上的UFC-agent 把相對超時時間轉成絕對超時時間;
  • Host2機器上的UFC-agent 訪問本地的服務B。

圖片

通過雙端的Agent實現了相對時間到絕對時間的轉換,對業務解決了上面的那2個問題,對業務也是透明的。

圖片

目前網盤的核心鏈路都接入了這套請求執行時間過期丟失的機制。

不過這個也是存在一些缺陷場景的,比如網絡故障/ufc agent資源打滿等,使用其它的解決方案來解決這些缺陷。

3.4 減少無效請求-基於socket有效性

上面是基於deadline來判斷請求是否有效的,是百度網盤19年的技術方案。

當時百度網盤的技術棧還是比較多樣的,編程語言包括c/c++/php/golang/ngx_lua等多種,需要從Service Mesh這種中間件來解決問題。

另外,這個依賴Service Mesh這種中間件的落地覆蓋,並不是所有的業務都具備這種的前提條件。

這裏提供另外一種基於socket有效性的方案來判斷請求是否有效。

【基本思路】

需要有一個認知:

比如以下場景:

  • tcp三次握手之後,server 未accept請求,client直接調用close關閉連接;
  • tcp三次握手之後,server 未accept請求,client先寫request請求,然後調用close關閉連接;
  • server accept後進行請求處理時,client調用close關閉連接。

從c語言編程視角來看,非阻塞情況下read函數返回-1表示讀數據失敗,返回0表示讀到fin包,即client關閉了socket。

ssize\_t read(int fd, void *buf, size\_t count);

所以可以基於socket讀到fin包事件來判斷client是否已經斷開連接了,如果已經斷開,server則不需要處理接下來的邏輯了(異常場景收尾邏輯可能還需要)。

【具體實踐】

底層socket編程這塊,一般都是被編程語言/編程框架屏蔽了,這裏主要是介紹一下一些常見語言/編程框架怎麼判斷client已經斷開連接了。

先説一下brpc框架,如下圖所示,主要是調用IsCanceled函數來判斷client是否已經斷開了(不適用http 2.0等這樣的場景)。

驗證效果的話,可以通過在brpc框架裏面的accept 邏輯前面加sleep,構造server backlog 堆積的場景進去驗證。

圖片

圖片

再説一下golang語言,主要是通過http server的回調函數的參數r *http.Request進行判斷的,具體是判斷 r.Context().Done()。

驗證效果的話,可以通過netutil.LimitListener來設置最大處理的連接數。

圖片

圖片

不過golang和brpc的內部實現還是有點不一樣的,下面將舉例説明:

當一個client 與server通過tcp三次握手建立連接後,進入了server的全連接隊列中,client調用close關閉連接,1秒之後,server 調用accept取出該連接。

brpc通過IsCanceled是可以判斷出client已經斷開連接了,而golang通過r.Context().Done()卻判定client還未斷開連接,需要sleep若干時間之後才能判斷client已經斷開連接。

原因是golang在源碼裏是起了一個單獨的協程去讀socket,所以導致這個判斷會出現延遲。

圖片

解決方案是通過ConnContext回調函數,把連接存儲在context裏,然後在http server的回調函數裏取出連接,先讀一次,這樣就可以判斷是否已經斷開了。

圖片

圖片

04 總結

百度網盤業務形態眾多,業務的高速迭代發展需要建立在可靠的架構基礎之上。

在整個架構演進過程,可用性是非常重要的事情,於是設計了一套防雪崩架構,具體包括兩部分:

  • 流量限制:可以分成兩部分,一個是流量接入層,解決ddos連接數攻擊,另外一部分是流量轉發層,通過動態熔斷策略將後端能處理的請求數轉發給後端;
  • 流量處理:業務基於流量有效性進行處理,避免處理無效請求。

圖片

最終對雪崩的治理也取得了不錯的效果,單個季度可以規避若干次的雪崩故障發生,保障了網盤業務的可用性。

本文只是拋磚引玉,更多的是希望與大家一起探討什麼才是優秀的工程實踐,歡迎大家留言反饋,多謝!!!

————END————

推薦閲讀

如何在百度百舸部署滿血版DeepSeek-V3、DeepSeek-R1模型

首日調用客户破1.5萬!DeepSeek-V3/R1上線背後的超低推理成本技術揭秘

喚醒 AI 算力,專有云 ABC Stack 面向企業級智算平台的 GPU 提效實踐

百度APP iOS端磁盤優化實踐(上)

對話AI原生|比幫你寫代碼更爽的是:讓Agent來打工

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.