1-3 萬字 - 34 張圖帶你全面掌握高可用架構流量治理核心策略

👉導讀

對於人類的身體健康來說,“三高” 是個大忌,但在計算機界,系統的 “三高” 卻是健康的終極目標。本文將介紹一下流量治理是如何維持這種 “三高” 系統的健康,保障數據流動的均衡與效率,就如同營養顧問在維持人類健康飲食中所起的作用一般。

01 可用性的定義

在探討高可用架構之前,讓我們以 O2 系統爲例,解釋一下何謂可用性。O2 是騰訊內部的一個廣告投放系統,專注於提升投放效率、分析廣告效果,擁有自動化廣告投放、AIGC 自動化素材生產等多種功能。

其整體架構概覽如下:

一個完善的架構應該具備 3 個能力,也就是身體的 “三高”:

  1. 高性能;

  2. 高可用;

  3. 易擴展。

理解高可用時,通常參考兩個關鍵指標:

可用性(Availability)的計算公式:Availability= MTBF / (MTBF + MTTR) * 100%

這個公式反映了一個簡單的事實:只有當系統故障間隔時間越長,且恢復時間越短,系統的整體可用性纔會更高。

因此,在設計高可用系統時,我們的核心目標是延長 MTBF,同時努力縮短 MTTR,以減少任何潛在故障對服務的影響。

02 流量治理的目的

03 流量治理的手段

   3.1 熔斷

微服務系統中,一個服務可能會依賴多個服務,並且有一些服務也依賴於它

傳統熔斷器

當請求失敗比率達到一定閾值之後,熔斷器開啓,並休眠一段時間(由配置決定)。這段休眠期過後,熔斷器將處於半開狀態,在此狀態下將試探性地放過一部分流量,如果這部分流量調用成功後,再次將熔斷器關閉,否則熔斷器繼續保持開啓並進入下一輪休眠週期。

引入傳統熔斷器的請求時序圖:

Google SRE 熔斷器

是否可以做到在熔斷器 Open 狀態下(但是後端未 Shutdown)仍然可以放行少部分流量呢?Google SRE 熔斷器提供了一種算法:客戶端自適應限流(client-side throttling)。

解決的辦法就是客戶端自行限制請求速度,限制生成請求的數量,超過這個數量的請求直接在本地回覆失敗,而不會真正發送到服務端。

該算法統計的指標依賴如下兩種,每個客戶端記錄過去兩分鐘內的以下信息(一般代碼中以滑動窗口實現)。

Google SRE 熔斷器的工作流程:

客戶端請求被拒絕的概率(Client request rejection probability,以下簡稱爲 p)

p 基於如下公式計算(其中 K 爲倍率 - multiplier,常用的值爲 2)。

客戶端可以發送請求直到 requests = K∗accepts, 一旦超過限制, 按照 p 進行截流。

對於後端而言,調整 K 值可以使得自適應限流算法適配不同的服務場景

熔斷本質上是一種快速失敗策略。旨在通過及時中斷失敗或超時的操作,防止資源過度消耗和請求堆積,從而避免服務因小問題而引發的雪崩效應。

   3.2 隔離

微服務系統中,隔離策略是流量治理的關鍵組成部分,其主要目的是避免單個服務的故障引發整個系統的連鎖反應。

通過隔離,系統能夠局部化問題,確保單個服務的問題不會影響到其他服務,從而維護整體系統的穩定性和可靠性。

常見的隔離策略:

   3.2.1 動靜隔離

動靜隔離通常是指將系統的動態內容和靜態內容分開處理

動態內容

靜態內容

   3.2.2 讀寫隔離

讀寫隔離通常是指將讀操作和寫操作分離到不同的服務或實例中處理

DDD 中有一種常用的模式:CQRS(Command Query Responsibility Segregation,命令查詢職責分離)來實現讀寫隔離

寫服務

讀服務

事件驅動

獨立擴展

   3.2.3 核心隔離

核心隔離通常是指將資源按照 “核心業務”與 “非核心業務”進行劃分,優先保障 “核心業務” 的穩定運行 AI 助手

   3.2.4 熱點隔離

熱點隔離通常是指一種針對高頻訪問數據(熱點數據)的隔離策略

   3.2.5 用戶隔離

用戶隔離通常是指按照不同的分組形成不同的服務實例。這樣某個服務實例宕機了也只會影響對應分組的用戶,而不會影響全部用戶

基於 O2-SAAS 系統的租戶概念,按照隔離級別的從高到低有如下幾種隔離方式:

  1. 每個租戶有獨立的服務與數據庫
    網關根據 tenant_id 識別出對應的服務實例進行轉發

  1. 每個租戶有共享的服務與獨立的數據庫
    用戶服務根據 tenant_id 確定操作哪一個數據庫

  1. 每個租戶有共享的服務與數據庫
    用戶服務根據 tenant_id 確定操作數據庫的哪一行記錄

   3.2.6 進程隔離

進程隔離通常是指系統中每一個進程擁有獨立的地址空間,提供操作系統級別的保護區。一個進程出現問題不會影響其他進程的正常運行,一個應用出錯也不會對其他應用產生副作用

容器化部署便是進程隔離的最佳實踐:

   3.2.7 線程隔離

線程隔離通常是指線程池的隔離,在應用系統內部,將不同請求分類發送給不同的線程池,當某個服務出現故障時,可以根據預先設定的熔斷策略阻斷線程的繼續執行

   3.2.8 集羣隔離

集羣隔離通常是指將某些服務單獨部署成集羣,或對於某些服務進行分組集羣管理

具體來說就是每個服務都獨立成一個系統,繼續拆分模塊,將功能微服務化:

   3.2.9 機房隔離

機房隔離通常是指在不同的機房或數據中心部署和運行服務,實現物理層面的隔離

機房隔離的主要目的有兩個:

  1. 解決數據容量大、計算和 I/O 密集度高的問題。 將不同區域的用戶隔離到不同的地區,比如將湖北的數據存儲在湖北的服務器,浙江的數據存儲在浙江的服務器,這種區域化的數據管理能有效地分散流量和系統負載;

  2. 增強數據安全性和災難恢復能力。 通過在不同地理位置建立服務的完整副本(包括計算服務和數據存儲),系統可以實現異地多活或冷備份。這樣,即使一個機房因自然災害或其他緊急情況受損,其他機房仍能維持服務,確保數據安全和業務連續性。

   3.3 重試

如何在不可靠的網絡服務中實現可靠的網絡通信,這是計算機網絡系統中避不開的一個問題

微服務架構中,一個大系統被拆分成多個小服務,小服務之間大量的 RPC 調用,過程十分依賴網絡的穩定性。

網絡是脆弱的,隨時都可能會出現抖動,此時正在處理中的請求有可能就會失敗。場景:O2 Marketing API 服務調用媒體接口拉取數據。

對於網絡抖動這種情況,解決的辦法之一就是重試。但重試存在風險,它可能會解決故障,也可能會放大故障。

對於網絡通信失敗的處理一般分爲以下幾步:

  1. 感知錯誤;

    • 通過不同的錯誤碼來識別不同的錯誤,在 HTTP 中 status code 可以用來識別不同類型的錯誤。
  2. 重試決策;

    • 這一步主要用來減少不必要的重試,比如 HTTP 的 4xx 的錯誤,通常 4xx 表示的是客戶端的錯誤,這時候客戶端不應該進行重試操作,或者在業務中自定義的一些錯誤也不應該被重試。根據這些規則的判斷可以有效的減少不必要的重試次數,提升響應速度。
  3. 重試策略;

    • 重試策略就包含了重試間隔時間,重試次數等。如果次數不夠,可能並不能有效的覆蓋這個短時間故障的時間段,如果重試次數過多,或者重試間隔太小,又可能造成大量的資源 (CPU、內存、線程、網絡) 浪費。
  4. 對沖策略。

    • 對沖是指在不等待響應的情況主動發送單次調用的多個請求,然後取首個返回的回包。

如果重試之後還是不行,說明這個故障不是短時間的故障,而是長時間的故障。那麼可以對服務進行熔斷降級,後面的請求不再重試,這段時間做降級處理,減少沒必要的請求,等服務端恢復了之後再進行請求,這方面的工程實現很多,比如 go-zero 、 sentinel 、hystrix-go。

   3.3.1 重試方式

常見的重試主要有兩種方式:同步重試、異步重試

同步重試

異步重試

如果服務追求數據的強一致性,並且希望在下游服務故障的時候不影響上游服務的正常運行,此時可以考慮使用異步重試。

   3.3.2 最大重試次數

無限重試可能會導致系統資源(網絡帶寬、CPU、內存)的耗盡,甚至引發重試風暴

應評估系統的實際情況和業務需求來設置最大重試次數:

  1. 設置過低,可能無法有效地處理該錯誤;

  2. 設置過高,同樣可能造成系統資源的浪費。

   3.3.3 退避策略

我們知道重試是一個 trade-off 問題:

退避策略基於重試算法實現。重試算法有多種,思路都是在重試之間加上一個間隔時間

線性間隔(Linear Backoff)

線性間隔 + 隨機時間(Linear Jitter Backoff)

指數間隔(Exponential Backoff)

指數間隔 + 隨機時間(Exponential Jitter Backoff)

上面有兩種策略都加入了 擾動(jitter),目的是防止 驚羣問題 (Thundering Herd Problem) 的發生。

所謂驚羣問題當許多進程都在等待被同一事件喚醒的時候,當事件發生後最後只有一個進程能獲得處理。其餘進程又造成阻塞,這會造成上下文切換的浪費所以加入一個隨機時間來避免同一時間同時請求服務端還是很有必要的

gRPC 實現

gRPC 便是使用了 指數間隔 + 隨機時間 的退避策略進行重試:GRPC Connection Backoff Protocol https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md

/* 僞代碼 */
ConnectWithBackoff()
  current_backoff = INITIAL_BACKOFF
  current_deadline = now() + INITIAL_BACKOFF
  while (TryConnect(Max(current_deadline, now() + MIN_CONNECT_TIMEOUT))
         != SUCCESS)
    SleepUntil(current_deadline)
    current_backoff = Min(current_backoff * MULTIPLIER, MAX_BACKOFF)
    current_deadline = now() + current_backoff +
      UniformRandom(-JITTER * current_backoff, JITTER * current_backoff)

關於僞代碼中幾個參數的說明:

  1. INITIAL_BACKOFF:第一次重試等待的間隔;

  2. MULTIPLIER:每次間隔的指數因子;

  3. JITTER:控制隨機的因子;

  4. MAX_BACKOFF:等待的最大時長,隨着重試次數的增加,我們不希望第 N 次重試等待的時間變成幾十分鐘這樣不切實際的值;

  5. MIN_CONNECT_TIMEOUT:一次成功的請求所需要的時間,即使是正常的請求也會有響應時間,重試時間間隔需要大於這個響應時間纔不會出現請求明明已經成功,但卻進行重試的操作。

   3.3.4 重試風暴

通過一張圖來簡單介紹下重試風暴:

  1. DB 負載過高時,Service C 對 DB 的請求出現失敗;

  2. 因爲配置了重試機制,Service C 對 DB 發起了最多 3 次請求;

  3. 鏈路上爲了避免網絡抖動,上游的服務均設置了超時重試 3 次的策略;

  4. 這樣在一次業務請求中,對 DB 的訪問可能達到 3^(n) 次。

此時負載高的 DB 便被捲進了重試風暴中,最終很可能導致服務雪崩。

應該怎麼避免重試風暴呢?筆者整理瞭如下幾種方式:

1、限制單點重試

2、引入重試窗口

這裏介紹一下重試窗口:

3、限制鏈路重試

關於 Google SRE 的實現方式,大致細節如下:

該方法可以有效避免重試風暴,但請求鏈路上需要上下游服務約定好重試狀態碼並耦合對於的邏輯,一般需要在框架層面上做出約束。

   3.3.5 對沖策略

有時候我們接口只是偶然會出問題,並且我們的下游服務並不在乎多請求幾次,那麼我們可以考慮對沖策略 AI 助手

對沖是指在不等待響應的情況下主動發送單次調用的多個請求,然後取首個返回的回包

請求流程

  1. 第一次正常的請求正常發出;

  2. 在等待固定時間間隔後,沒有收到正確的響應,第二個對沖請求會被髮出;

  3. 再等待固定時間間隔後,沒有收到任何前面兩個請求的正確響應,第三個會被髮出;

  4. 一直重複以上流程直到發出的對沖請求數量達到配置的最大次數;

  5. 一旦收到正確響應,所有對沖請求都會被取消,響應會被返回給應用層。

與普通重試的區別

普通重試時序圖:

對沖重試時序圖:

   3.4 降級

降級是從系統功能角度出發,人爲或自動地將某些不重要的功能停掉或者簡化,以降低系統負載,這部分釋放的資源可以去支撐更核心的功能

   3.4.1 降級策略

以 O2 系統舉例,有以下幾類降級策略:

雖說故障是不可避免的,要達到絕對高可用一般都是使用冗餘 + 自動故障轉移,這個時候其實也不需要降級措施了。

但是這樣帶來的成本較高,而且可用性、成本、用戶體驗 3 者本身之間是需要權衡的,一般來說他們之前會是這樣的關係:

   3.4.2 自動降級

   3.4.3 手動降級

   3.4.4 執行降級

降級的策略還是比較豐富的,因此需要從多個角度去化簡

   3.4.5 與限流的區別

   3.5 超時

超時是一件很容易被忽視的事情 

早期架構發展階段,大家或多或少有過遺漏設置超時或者超時設置太長導致系統被拖慢甚至掛起的經歷 

隨着微服務架構的演進,超時逐漸被標準化到 RPC 中,並可通過微服務治理平臺快捷調整超時參數 

傳統超時會設定一個固定的閾值,響應時間超過閾值就返回失敗。在網絡短暫抖動的情況下,響應時間增加很容易產生大規模的成功率波動 

服務的響應時間並不是恆定的,在某些長尾條件下可能需要更多的計算時間,爲了有足夠的時間等待這種長尾請求響應,我們需要把超時設置足夠長,但超時設置太長又會增加風險,超時的準確設置經常困擾我們

   3.5.1 超時策略

目前業內常用的超時策略有:

  1. 固定超時時間;

  2. EMA 動態超時。

   3.5.2 超時控制

超時控制的本質是 fail fast,良好的超時控制可以儘快清空高延遲的請求,儘快釋放資源避免請求堆積。

服務間超時傳遞

一個請求可能由一系列 RPC 調用組成,每個服務在開始處理請求前應檢查是否還有足夠的剩餘時間處理,也就是應該在每個服務間傳遞超時時間。

如果都使用每個 RPC 服務設置的固定超時時間,這裏以上圖爲例

  1. A -> B,設置的超時時間爲 3s;

  2. B 處理耗時爲 2s,並繼續請求 C;

  3. 如果使用了超時傳遞那麼 C 的超時時間應該爲 1s,這裏不採用所以超時時間爲配置的 3s;

  4. C 繼續執行耗時爲 2s,此時最上層(A)設置的超時時間已截止;

  5. C -> D 的請求對 A 來說已經失去了意義。

進程內超時傳遞

上圖流程如下:

  1. 一個進程內串行調用了 MySQL、Redis 和 Service B,設置總的請求時間爲 3s;

  2. 請求 MySQL 耗時 1s 後再請求 Redis,這時的超時時間爲 2s,Redis 執行耗時 500 ms;

  3. 再請求 Service B,這時超時時間爲 1.5s。

由於每個組件或服務都會在配置文件中配置固定的超時時間,使用時應該取實際剩餘時間與配置的超時時間中的最小值。

Context 實現超時傳遞

   3.5.3 EMA 動態超時

如果我們的微服務系統對這種短暫的時延上漲具備足夠的容忍能力,可以考慮基於 EMA 算法動態調整超時時長。

EMA 算法引入 “平均超時” 的概念,用平均響應時間代替固定超時時間,只要平均響應時間沒有超時即可,而不是要求每次請求都不能超時。

算法實現

算法實現參考:https://github.com/jiamao/ema-timeout

總而言之:

  1. 總體情況不能超標;

  2. 平均情況表現越好,彈性越大;

  3. 平均情況表現越差,彈性越小。

適用條件

  1. 固定業務邏輯,循環執行;

  2. 程序大部分時間在等待響應,而不是 CPU 計算或者處理 I/O 中斷;

  3. 服務是串行處理模式,容易受異常、慢請求阻塞;

  4. 響應時間不宜波動過大;

  5. 服務可以接受有損。

使用方法

EMA 動態超時根據業務的請求鏈路有兩種用法:

1. 用於非關鍵路徑

Thwm 設置的相對小,當非關鍵路徑頻繁耗時增加甚至超時時,降低超時時間,減少非關鍵路徑異常帶來的資源消耗,提升服務吞吐量。

2. 用於關鍵路徑

Thwm 設置的相對大,用於長尾請求耗時比較多的場景,提高關鍵路徑成功率。

在 3.5.2 小節有提到,一般超時時間會在鏈路上傳遞,避免上游已經超時,下游繼續浪費資源請求的情況。

這個傳遞的超時時間一般是沒有考慮網絡耗時或不同服務器的時鐘不一致的,所以會存在一定的偏差。

   3.5.4 超時策略的選擇

超時策略的選擇:剩餘資源 = 資源容量 - QPS 單次請求消耗資源請求持續時長 – 資源釋放所需時長

Kzs8UR

   3.5.5 超時時間的選擇

如何選擇合適的超時閾值?超時時間選擇需要考慮的幾個點:

  1. 被調服務的重要性;

  2. 被調服務的耗時 P99、P95、P50、平均值;

  3. 網絡波動;

  4. 資源消耗;

  5. 用戶體驗。

   3.6 限流

預期外的突發流量總會出現,對我們系統可承載的容量造成巨大沖擊,極端情況下甚至會導致系統雪崩 

當系統的處理能力有限時,如何阻止計劃外的請求繼續對系統施壓,這便是限流的作用之處 

限流可以幫助我們應對突發流量,通過限制服務的請求率來保護服務不被過載 

除了控制流量,限流還有一個應用目的是用於控制用戶行爲,避免無用請求,比如頻繁地下載系統中的數據表格

限流一般來說分爲客戶端限流和服務端限流兩類。

   3.6.1 客戶端限流

在客戶端限流中,由於請求方和被請求方的關係明確,通常採用較爲簡單的限流策略,如結合分佈式限流和固定的限流閾值。

客戶端的限流閾值可被視作被調用方對主調方的配額。

合理設定限流閾值的方法包括:

  1. 容量評估:通過單機壓測確定服務的單機容量模型,並與下游服務協商以瞭解他們的限流閾值

  2. 容量規劃:根據日常運行、運營活動和節假日等不同場景,提前進行容量評估和規劃

  3. 全鏈路壓測:通過模擬真實場景的壓測,評估現有限流值的合理性

在限流算法方面,大家也都已經耳熟能詳。像滑動窗口、漏桶令牌桶均是常用的限流算法。

這些算法各有特點,能有效管理客戶端的請求流量,保障系統的穩定運行。

這裏筆者簡單梳理了一張常用的限流算法的思維導圖,主要闡述每個算法的侷限性,需要根據實際應用場景選擇合適的算法:

   3.6.2 服務端限流

服務端限流旨在通過主動丟棄或延遲處理部分請求,以應對系統過載的情況。

服務端限流實現的兩個關鍵點:

1、如何判斷系統是否過載

常用的判斷依據包括:

2、過載時如何選擇要丟棄的請求

常用的判斷依據包括:

關於服務端限流在業界內的實踐應用,筆者這裏整理了兩個示例:

開源的 Sentinel 採用類似 TCP BBR 的限流方法。它基於利特爾法則,計算時間窗口內的最大成功請求數 (MaxPass) 和最小響應時間(MinRt)。當 CPU 使用率超過 80% 時,根據 MaxPass 和 MinRt 計算窗口內理論上可以通過的最大請求量,進而確定每秒的最大請求數。如果當前處理中的請求數超過此計算值,則進行請求丟棄。

微信後臺則使用請求的平均排隊時間作爲系統過載的判斷標準。當平均等待時間超過 20 毫秒時,它會以一定的降速因子來過濾部分請求。相反,如果判斷平均等待時間低於 20 毫秒,則會逐漸提高請求的通過率。這種 “快速降低,緩慢提升” 的策略有助於防止服務的大幅波動。

04 總結

想要讓系統長期 “三高”,流量治理只是衆多策略的其中一個,其他還有像存儲高可用、緩存、負載均衡、故障轉移、冗餘設計、可回滾設計等等均是確保系統長期穩定運行的關鍵因素,筆者也期待在後續就這些策略再和大家進行分享。

本文在介紹高可用架構中流量治理部分時,我們詳細討論了從熔斷機制到隔離策略、重試邏輯、降級方案,以及超時和限流控制等多種手段,這裏簡單歸納一下:

綜合這些策略,我們可以構建出一個既高效又穩健的系統,它能夠在各種網絡條件和負載情況下保持高性能、高可用和易擴展。這些流量治理的手段不僅確保了服務的連續性和可靠性,還提高了用戶體驗和系統的整體效率。

最後想說,高可用的本質就是面向失敗設計。它基於一個現實且務實的前提:系統中的任何組件都有可能出現故障。

因此,在架構設計時,我們不僅要接受故障的可能性,而且要學會擁抱故障。這意味着從一開始就將容錯和恢復能力納入設計考慮,通過增強系統的彈性、自適應性和恢復機制來應對可能出現的故障和變化。這種方法確保了在面對各種挑戰時,系統能夠保持持續的運行和服務質量。

作者:孔奕凱

來源:騰訊雲開發者

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/z8OxtZprxjHsWR_upeqAyw