10 億級用戶,如何做熔斷降級架構?微信和 hystrix 的架構對比

說在前面

在 40 歲老架構師 尼恩的讀者交流羣 (50+) 中,最近有小夥伴拿到了一線互聯網企業如極兔、有贊、希音、百度、網易、滴滴的面試資格,遇到一幾個很重要的面試題:

(1) 什麼是熔斷,降級?如何實現?

(2) 服務熔斷,解決災難性雪崩效應的有效利器

(3 )說一下限流、熔斷、高可用

等等等等......

熔斷,降級,防止雪崩,是面試的重點和高頻點。 尼恩作爲技術中臺、數據中臺的架構師,致力於爲大家研究出一個 3 高架構知識宇宙, 所以這裏,結合億級 qps 微信後臺是如何熔斷降級方案,帶大家完成一個億級用戶場景,如何一步一步,進行熔斷,降級,防止雪崩架構。

當然,作爲一篇文章,僅僅是拋磚引玉,後面有機會,帶大家做一下這個高質量的實操,並且指導大家寫入簡歷。

讓面試官愛到 “不能自已、口水直流”

本文目錄

- 說在前面

- 億級 qps 微信後臺是如何熔斷降級,防止崩潰的?

- 降級保護的基本原理

  - 什麼是降級?

  - 降級保護的幾個核心策略

  - 資源隔離

  - 限流降級

  - 超時降級

  - 失敗次數降級

  - 熔斷降級(過載保護)

    - 過載保護的好處

    - 如何判斷過載

- 降級保護的主流架構方案

- Hystrix 熔斷降級、限流降級

- Hystrix 限流降級

- Hystrix 異常降級

- Hystrix 調用超時降級

- Hystrix 資源隔離

  - 線程池隔離 (艙壁模式)

  - Hystrix 線程池隔離

  - Hystrix 線程池隔離配置

  - Hystrix 信號量隔離

  - 線程池與信號量區別

- Hystrix 熔斷降級(過載保護)

  - 斷路器有 3 種狀態:

  - Hystrix 實現熔斷機制

  - 熔斷器狀態變化的演示實例

  - 熔斷器和滑動窗口的配置屬性

  - Hystrix 命令的執行流程

- Sentinel 限流降級

  - 基本的參數

  - 流控的幾種 strategy:

  - 直接失敗模式 限流

  - Sentinel 關聯模式限流

  - Warm up(預熱)模式 限流

- Sentinel 熔斷降級

  - 什麼是 Sentinel 熔斷降級

  - Sentinel 熔斷降級規則

  - 幾種降級策略

  - 熔斷降級代碼實現

  - 控制檯降級規則

- Sentinel 與 Hystrix 對比

  - 1、資源模型和執行模型上的對比

  - 2、隔離設計上的對比

  - 3、熔斷降級的對比

- Sentinel 與 Hystrix 的不足

- 分層分級細粒度、高精準熔斷限流降級策略

  - 微信後臺億級 QPS 吞吐量的過載場景

  - 微信後臺如何判斷過載

  - 微信後臺的限流降級策略(過載保護策略)

  - 分層分級細粒度、高精準熔斷限流降級策略

    - 1)業務分層

    - 2)用戶分級

    - 3)分層分級二維限流降級控制

    - 4)RPC 組件客戶端限流

  - 微信整個負載控制的流程

- 說在最後

- 作者介紹

- 參考文獻

- 推薦閱讀

億級 qps 微信後臺是如何熔斷降級,防止崩潰的?

微信作爲月活過 10 億的國民級應用,是中國最受歡迎的社交網絡平臺之一,擁有龐大的用戶羣體和廣泛的社交功能,包括朋友圈、微信支付、小程序、公衆號等。

同時,微信也在不斷地進行創新和擴展,如推出微信小程序、微信支付海外版等,以滿足不同用戶的需求。

微信的日活用戶數量一直在增長,截至 2022 年第一季度,微信的日活用戶數量已經達到了 10 億

微信的月活用戶數量也在不斷增長,截至 2022 年第三季度,微信的月活用戶數量已經達到了 13.09 億

微信作爲當之無愧的國民級應用,系統複雜程度超乎想象:

作爲頂級、超級互聯網應用,微信和其他的分佈式、微服務應用一樣,經常面臨特殊節點消息量暴增的問題,服務很容易出現過載問題。

但微信的服務一直比較穩定,是如何做到的呢?

尼恩帶着大家,從降級保護的基本原理 講起。

並且將微信和 hystrix、sentinel 對比介紹。

降級保護的基本原理

什麼是降級?

所謂降級,一般指整體的資源即將耗盡,爲了保留關鍵的服務,捨棄非核心的服務

核心鏈路又稱 黃金鍊路

黃金鍊路是團隊的生命線鏈路,由最核心的應用,最關鍵的 DB,最需要死保的接口,支撐的最核心業務。

黃金鍊路的治理就一個目標:不要讓非核心的東西影響了核心的

這裏的 “東西” 包括業務、系統、DB 等等

降級保護的幾個核心策略

資源隔離

所謂資源隔離 ,就是 隔離 黃金鍊路和 非核心鏈路,  對非核心鏈路進行降級, 對黃金鍊路進行  保護。

具體來說,對黃金鍊路上的,每一個服務乃至其對應的數據庫,分配獨立的服務器資源、網絡資源、數據庫資源,進行獨立部署就行!

對黃金鍊路進行資源重點投入,做好鏈路的高併發、高性能、高可用設計。

這樣,當非核心鏈路某個服務出現了故障,就不會影響到黃金鍊路,達到一種物理層面上的隔離!

資源隔離是一種隱性的降級策略,爲什麼叫做隱性而不是顯性呢?

資源隔離相當於對不同的鏈路分級對待,對非核心鏈路,本質是一種降級處理。

限流降級

當訪問量太大而導致系統崩潰時,使用限流來進行限制訪問量,當達到限流閥值,後續請求會被降級。

限流降級,兜底的處理方案可以是:

超時降級

超時降級是當某個微服務響應時間過長,超過了 正常的響應時長,我們不能讓他一直卡在那裏,所以要在準備一個兜底的策略,當發生這種問題的時候,我們直接調用一個降級方法來快速返回,不讓他一直卡在那 。

超時降級的策略爲:

失敗次數降級

失敗次數降級是當某個微服務總是調用失敗,我們不能讓他一直失敗,所以要在準備一個兜底的策略,當發生這種問題的時候,我們直接調用一個降級方法來快速返回,不讓他一直卡在那 。

失敗降級的策略爲:

熔斷降級(過載保護)

互聯網應用,天生就會有突發流量。

秒殺、搶購、突發大事件、節日甚至惡意攻擊等,都會造成服務承受平時數倍的壓力。

比如,微博經常出現某明星官宣結婚或者離婚導致服務器崩潰的場景,這就是服務過載。

服務過載是什麼意思呢?

就是服務的請求量,超過服務所能承受的最大值,從而導致服務器負載過高,響應延遲加大。

下游客戶端的表現:RT 響應時間變長,加載緩慢,甚至無法加載。

服務過載的存在級聯效應:

下游會進一步的重試,導致上游服務一直在處理無效請求,導致有效請求跌 0,甚至導致整個系統產生雪崩。

熔斷就像是家裏的保險絲一樣,當電流達到一定條件時,比如保險絲能承受的電流是 5A,如果電流達到了 6A,因爲保險絲承受不了這麼高的電流,保險絲就會融化,電路就會斷開,起到了保護電器的作用;

在微服務裏面也是一樣,當下遊的服務因爲某種原因突然變得不可用或響應過慢,上游服務爲了保證自己整體服務的可用性,不再繼續調用目標服務,直接返回,快速釋放資源。如果目標服務情況好轉則恢復調用;

所以,在互聯網應用中,由於某些原因使得服務出現了過載現象,爲防止造成整個系統故障,從而採用的一種熔斷降級(過載保護)措施。

過載保護的好處

服務過載容易導致系統癱瘓,系統雪崩,系統雪崩就意味着用戶流失、口碑變差、導致巨大的經濟損失(億級以上),和巨大的品牌損失(無法估量)。

提升用戶體驗、保障服務質量。在發生突發流量時仍然能夠提供一部分服務能力,而不是整個系統癱瘓,

如何判斷過載

通常判斷過載的方式很多,比如:

Hystrix  、Sentinel   主要使用 使用 失敗率 判斷過載

微信 使用 請求在隊列中的平均等待時間  判斷過載, 並且進行  分級分層細粒度 熔斷降級策略。

降級保護的主流架構方案

在大規模分佈式微服務應用中,主流的架構方案有

Hystrix 熔斷降級、限流降級

Spring Cloud Hystrix 是一款優秀的服務容錯與保護組件,也是 Spring Cloud 中最重要的組件之一。 Spring Cloud Hystrix 是基於 Netflix 公司的開源組件 Hystrix 實現的,它提供了熔斷器功能,能夠有效地阻止分佈式微服務系統中出現聯動故障,以提高微服務系統的彈性。

Spring Cloud Hystrix 具有服務降級、服務熔斷、線程隔離、請求緩存、請求合併以及實時故障監控等強大功能。

Hystrix [hɪst'rɪks],中文含義是豪豬,豪豬的背上長滿了棘刺,使它擁有了強大的自我保護能力。而 Spring Cloud Hystrix 作爲一個服務容錯與保護組件,也可以讓服務擁有自我保護的能力,因此也有人將其戲稱爲 “豪豬哥”。

熔斷器(Circuit Breaker)一詞來源物理學中的電路知識,它的作用是當線路出現故障時,迅速切斷電源以保護電路的安全。

在微服務領域,熔斷器最早是由 Martin Fowler 在他發表的 《Circuit Breake(https://martinfowler.com/bliki/CircuitBreaker.html)r》一文中提出。與物理學中的熔斷器作用相似,微服務架構中的熔斷器能夠在某個服務發生故障後,向服務調用方返回一個符合預期的、可處理的降級響應(FallBack),而不是長時間的等待或者拋出調用方無法處理的異常。這樣就保證了服務調用方的線程不會被長時間、不必要地佔用,避免故障在微服務系統中的蔓延,防止系統雪崩效應的發生。

hystrix 通過命令模式,將每個請求封裝成一個 Command,每個類型的 Command 對應一個線程池 (例如商品服務 Command)

請求過來,爲請求創建 Command

如果 Command 開啓了緩存 (配置的一個參數) ,會先向 requestCache 查詢調用服務的結果,如果有直接返回

每個 Command 執行完會上報自己的執行結果狀態給熔斷器 Circuit breaker,狀態包括:成功,失敗,超時,拒絕等,熔斷器會統計這些數據, 來決定是否降級:

在微服務系統中,Hystrix 能夠幫助我們實現以下目標:

Hystrix 限流降級

同樣是 A 服務調用 B 服務,服務 A 的連接已超過自身能承載的最大連接數,比如說 A 能承載的連接數爲 5,但是目前的併發有 6 個請求同時進行,前 5 請求能正常請求,最後一個會直接拒絕,執行 fallback 降級邏輯;

Hystrix  支持線程池或者信號量限流, 只要線程池滿,或者無限號,就進行限流降級,返回降級後的兜底結果。

HystrixCommand 的執行流程示意圖

hystrix 可以使用信號量和線程池來進行限流。

線程池限流

hystrix也可以使用線程池進行限流,在提供服務的方法上加下面的註解

@HystrixCommand(
    commandProperties = {
            @HystrixProperty(name = "execution.isolation.strategy"value = "THREAD")
    },
    threadPoolKey = "createOrderThreadPool",
    threadPoolProperties = {
            @HystrixProperty(name = "coreSize"value = "20"),
   @HystrixProperty(name = "maxQueueSize"value = "100"),
            @HystrixProperty(name = "maximumSize"value = "30"),
            @HystrixProperty(name = "queueSizeRejectionThreshold"value = "120")
    },
    fallbackMethod = "errMethod"
)

這裏要注意:queueSizeRejectionThreshold  建議大於  maxQueueSize

java的線程池中,如果線程數量超過coreSize,創建線程請求會優先進入隊列,如果隊列滿了,就會繼續創建線程直到線程數量達到maximumSize,之後走拒絕策略。

但在 hystrix 配置的線程池中多了一個參數queueSizeRejectionThreshold,如果queueSizeRejectionThreshold < maxQueueSize, 隊列數量達到queueSizeRejectionThreshold就會走拒絕策略了,因此maximumSize失效了。

如果queueSizeRejectionThreshold > maxQueueSize, 隊列數量達到maxQueueSize時,maximumSize是有效的,系統會繼續創建線程直到數量達到maximumSize

信號量限流

hystrix可以使用信號量進行限流,比如在提供服務的方法上加下面的註解。

這樣只能有 20 個併發線程來訪問這個方法,超過的就被轉到了 errMethod 這個降級方法。

@HystrixCommand(
 commandProperties= {
   @HystrixProperty(),
   @HystrixProperty()
 },
 fallbackMethod = "errMethod"
)

Hystrix 異常降級

hystrix 降級時可以忽略某個異常,在方法上加上@HystrixCommand註解:

下面的代碼定義降級方法是errMethod,對ParamErrorExceptionBusinessTypeException這兩個異常不做降級處理。

@HystrixCommand(
 fallbackMethod = "errMethod",
 ignoreExceptions = {ParamErrorException.class, BusinessTypeException.class}
)

Hystrix 調用超時降級

專門針對調用第三方接口超時降級。

同樣是 A 服務調用 B 服務,B 服務響應超過了 A 服務設定的閾值後,就會執行降級邏輯;

下面的方法是調用第三方接口 3 秒未收到響應就降級到 errMethod 方法。

@HystrixCommand(
    commandProperties = {
            @HystrixProperty(),
            @HystrixProperty(),
    },
    fallbackMethod = "errMethod"
)

Hystrix 資源隔離

前面講到: 資源隔離相當於對不同的鏈路分級對待,對非核心鏈路,本質是一種降級處理。

Hystrix 裏面核心的一項功能,其實就是所謂的資源隔離,要解決的最最核心的問題,就是將多個依賴服務的調用分別隔離到各自的資源池內。

一旦說某個服務的線程資源全部耗盡的話,就可能導致服務崩潰,甚至說這種故障會不斷蔓延。Hystrix 資源隔離避免,就是對某一個依賴服務的調用,因爲依賴服務的接口調用的延遲或者失敗,導致服務所有的線程資源全部耗費在這個服務的接口調用上。

Hystrix 實現資源隔離,主要有兩種技術:

默認情況下,Hystrix 使用線程池模式。

線程池隔離 (艙壁模式)

線程池隔離,本質上來說,就是艙壁模式。

船舶工業上爲了使船不容易沉沒,使用艙壁將船舶劃分爲幾個部分,以便在船體遭到破壞的情況下可以將船舶各個部件密封起來。

泰坦尼克號沉沒的主要原因之一:就是其艙壁設計不合理,水可以通過上面的甲板進入艙壁的頂部,導致整個船體淹沒。

在 RPC 調用過程中,使用艙壁模式可以保護有限的系統資源不被耗盡。

在一個基於微服務的應用程序中,通常需要調用多個微服務提供者的接口才能完成一個特定任務。不使用艙壁模式,所有的 RPC 調用都從同一個線程池中獲取線程,一個具體的實例如圖 6-4 所示。

在該實例中,微服務提供者 Provider A 對依賴 Provider B、Provider C、Provider D 的所有 RPC 調用都從公共的線程池中獲取線程。

圖 6-4 公共的 RPC 線程池

在高服務器請求的情況下,對某個性能較低的微服務提供者的 RPC 調用很容易 “霸佔” 整個公共的 RPC 線程池,對其他性能正常的微服務提供者的 RPC 調用往往需要等待線程資源的釋放。最後,整個 Web 容器(Tomcat)會崩潰。現在假定 Provider A 的 RPC 線程個數爲 1000,而併發量非常大,其中有 500 個線程來執行 Provider B 的 RPC 調用,如果 Provider B 不小心宕機了,那麼這 500 個線程都會超時,此時剩下的服務 Provider C、Provider D 的總共可用的線程爲 500 個,隨着併發量的增大,剩餘的 500 個線程估計也會被 Provider B 的 RPC 耗盡,然後 Provider A 進入癱瘓,最後導致整個系統的所有服務都不可用,這就是服務的雪崩效應。

爲了最大限度地減少 Provider 之間的相互影響,一個很好的做法是對於不同的微服務提供者設置不同的 RPC 調用線程池,讓不同 RPC 通過專門的線程池請求到各自的 Provider 微服務提供者,像艙壁一樣對 Provider 進行隔離。對於不同的微服務提供者設置不同的 RPC 調用線程池,這種模式就叫作艙壁模式,如圖 6-5 所示。

圖 6-5 艙壁模式的 RPC 線程池

使用艙壁模式可以避免對單個 Provider 的 RPC 消耗掉所有資源,從而防止由於某一個服務性能底而引起的級聯故障和雪崩效應。在 Provider A 中,假定對服務 Provider B 的 RPC 調用分配專門的線程池,該線程池叫作 Thread Pool B,其中有 10 個線程,只要對 Provider B 的 RPC 併發量超過了 10,後續的 RPC 就走降級服務,就算服務的 Provider B 掛了,最多也就導致 Thread Pool B 不可用,而不會影響系統中的其他服務的 RPC。

一般來說,RPC 線程與 Web 容器的 IO 線程也是需要隔離的。

如圖 6-6 所示,當 Provider A 的用戶請求涉及 Provider B 和 Provider C 的 RPC 的時候,Provider A 的 IO 線程會將任務交給對應的 RPC 線程池裏面的 RPC 線程來執行,Provider A 的 IO 線程就可以去幹別的事情去了,當 RPC 線程執行完遠程調用的任務之後,就會將調用的結果返回給 IO 線程。如果 RPC 線程池耗盡了,IO 線程池也不會受到影響,從而實現 RPC 線程與 Web 容器的 IO 線程的相互隔離。

圖 6-6  RPC 線程與 Web 容器的 IO 線程相互隔離

雖然線程在就緒狀態、運行狀態、阻塞狀態、終止狀態間轉變時需要由操作系統調度,這會帶來一定的性能消耗,但是 Netflix 詳細評估了使用異步線程和同步線程帶來的性能差異,結果表明在 99% 的情況下異步線程帶來的延遲僅爲幾毫秒,這種性能的損耗對於用戶程序來說是完全可以接受的。

Hystrix 線程池隔離

Hystrix 既可以爲 HystrixCommand 命令默認創建一個線程池,也可以關聯上一個指定的線程池。每一個線程池都有一個 Key,叫作 Thread Pool Key(線程池名)。

如果沒有爲 HystrixCommand 指定線程池,Hystrix 會爲 HystrixCommand 創建一個與 Group Key(命令組 Key)同名的線程池,當然,如果與 Group Key 同名的線程池已經存在,則直接進行關聯。也就是說,默認情況下,HystrixCommand 命令的 Thread Pool Key 與 Group Key 是相同的。

總體來說,線程池就是隔離的關鍵,所有的監控、調用、緩存等都圍繞線程池展開。

如果要指定線程池,可以通過如下代碼在 Setter 中定製線程池的 Key 和屬性:

/**
*在Setter實例中指定線程池的Key和屬性
*/
HystrixCommand.Setter rpcPool1_setter = HystrixCommand.Setter
        .withGroupKey(HystrixCommandGroupKey.Factory.asKey("group1"))
        .andCommandKey(HystrixCommandKey.Factory.asKey("command1"))
        .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("threadPool1"))
.andThreadPoolPropertiesDefaults(
                HystrixThreadPoolProperties.Setter()
                        .withCoreSize(10)    //配置線程池裏的線程數
                        .withMaximumSize(10)
        );

然後,可以通過 HystrixCommand 或者 HystrixObservableCommand 的構造函數傳入 Setter 配置 實例:

public class HttpGetterCommand extends HystrixCommand<String>
{
    private String url;
    ...
    public HttpGetterCommand(String url, Setter setter)
    {
        super(setter);
        this.url = url;
}
...
}

HystrixThreadPoolKey 是一個接口,它有一個輔助工廠類 Factory,它的 asKey(String)方法專門用於創建一個線程池的 Key,示例代碼如下:

HystrixThreadPoolKey.Factory.asKey("threadPoolN")

下面是一個完整的線程池隔離演示例子:創建了兩個線程池 threadPool1 和 threadPool2,然後通過這兩個線程池發起簡單的 RPC 遠程調用,其中,通過 threadPool1 線程池訪問一個錯誤連接 ERROR_URL,通過 threadPool2 訪問一個正常連接 HELLO_TEST_URL。在實驗過程中,可以通過調整 RPC 的次數多次運行程序,然後通過結果查看線程池的具體隔離效果。

線程池隔離實例的代碼如下:

package com.crazymaker.demo.hystrix;
//省略import

@Slf4j
public class IsolationStrategyDemo
{
    /**
     * 測試:線程池隔離
     */
    @Test
    public void testThreadPoolIsolationStrategy() throws Exception
    {

        /**
         * RPC線程池1
         */
        HystrixCommand.Setter rpcPool1_Setter = HystrixCommand.Setter
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey("group1"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("command1"))
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("threadPool1"))
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        .withExecutionTimeoutInMilliseconds(5000)  //配置執行時間上限
                ).andThreadPoolPropertiesDefaults(
                        HystrixThreadPoolProperties.Setter()
                                .withCoreSize(10)    //配置線程池裏的線程數
                                .withMaximumSize(10)
                );


        /**
         * RPC線程池2
         */
        HystrixCommand.Setter rpcPool2_Setter = HystrixCommand.Setter
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey("group2"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("command2"))
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("threadPool2"))
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        .withExecutionTimeoutInMilliseconds(5000)  //配置執行時間上限
                ).andThreadPoolPropertiesDefaults(
                        HystrixThreadPoolProperties.Setter()
                                .withCoreSize(10)    //配置線程池裏的線程數
                                .withMaximumSize(10)
                );

        /**
         * 訪問一個錯誤連接,讓threadpool1 耗盡
         */
        for (int j = 1; j <= 5; j++)
        {


            new HttpGetterCommand(ERROR_URL, rpcPool1_Setter)
                    .toObservable()
                    .subscribe(s -> log.info(" result:{}", s));
        }

        /**
         * 訪問一個正確連接,觀察threadpool2是否正常
         */
        for (int j = 1; j <= 5; j++)
        {

            new HttpGetterCommand(HELLO_TEST_URL, rpcPool2_Setter)
                    .toObservable()
                    .subscribe(s -> log.info(" result:{}", s));
        }
        Thread.sleep(Integer.MAX_VALUE);

    }
}

運行這個演示程序,輸出的結果節選如下:

[hystrix-threadPool1-4] INFO  c.c.d.h.HttpGetterCommand - req1 begin...
[hystrix-threadPool1-3] INFO  c.c.d.h.HttpGetterCommand - req4 begin...
[hystrix-threadPool2-3] INFO  c.c.d.h.HttpGetterCommand - req10 begin...
[hystrix-threadPool2-5] INFO  c.c.d.h.HttpGetterCommand - req7 begin...
[hystrix-threadPool1-5] INFO  c.c.d.h.HttpGetterCommand - req9 begin...
[hystrix-threadPool2-1] INFO  c.c.d.h.HttpGetterCommand - req6 begin...
[hystrix-threadPool1-1] INFO  c.c.d.h.HttpGetterCommand - req8 begin...
[hystrix-threadPool1-2] INFO  c.c.d.h.HttpGetterCommand - req2 begin...
[hystrix-threadPool2-4] INFO  c.c.d.h.HttpGetterCommand - req5 begin...
[hystrix-threadPool2-2] INFO  c.c.d.h.HttpGetterCommand - req3 begin...
[hystrix-threadPool1-1] INFO  c.c.d.h.HttpGetterCommand - req8 fallback: 熔斷false,直接失敗false
[hystrix-threadPool1-4] INFO  c.c.d.h.HttpGetterCommand - req1 fallback: 熔斷false,直接失敗false
[hystrix-threadPool1-2] INFO  c.c.d.h.HttpGetterCommand - req2 fallback: 熔斷false,直接失敗false
[hystrix-threadPool1-3] INFO  c.c.d.h.HttpGetterCommand - req4 fallback: 熔斷false,直接失敗false
[hystrix-threadPool1-5] INFO  c.c.d.h.HttpGetterCommand - req9 fallback: 熔斷false,直接失敗false
...
[hystrix-threadPool2-4] INFO  c.c.d.h.HttpGetterCommand -  req5 end: {"respCode":0,"respMsg":"操作成功...}
[hystrix-threadPool2-2] INFO  c.c.d.h.HttpGetterCommand -  req3 end: {"respCode":0,"respMsg":"操作成功...}
[hystrix-threadPool2-3] INFO  c.c.d.h.HttpGetterCommand -  req10 end: {"respCode":0,"respMsg":"操作成功...}
[hystrix-threadPool2-1] INFO  c.c.d.h.HttpGetterCommand -  req6 end: {"respCode":0,"respMsg":"操作成功...}
[hystrix-threadPool2-5] INFO  c.c.d.h.HttpGetterCommand -  req7 end: {"respCode":0,"respMsg":"操作成功...}
...

從上面的結果可知:threadPool1 的線程使用和 threadPool2 的線程使用是完全地相互獨立和相互隔離的,無論 threadPool1 是否耗盡,threadPool2 的線程都可以正常發起 RPC 請求。

默認情況下,在 Spring Cloud 中,Hystrix 會爲每一個 Command Group Key(命令組 Key)自動創建一個同名的線程池。

而在 Hystrix 客戶端,每一個 RPC 目標 Provider 的 Command Group Key(命令組 Key)的默認值爲它的應用名稱(application name)。

比如,demo-provider 服務的 Command Group Key 默認值爲其名稱 “demo-provider”。

所以,如果某個 Provider(如 uaa-provider)需發起對 demo-Provider 的遠程調用,則 Hystrix 爲該 Provider 創建的 RPC 線程池的名稱默認爲 “demo-provider”,專用於對 demo-provider 的 REST 服務進行 RPC 調用和隔離,如圖 6-7 所示。

圖 6-7 對 demo-provider 服務進行 RPC 調用的專用線程池

Hystrix 線程池隔離配置

在 Spring Cloud 微服務提供者中,如果需使用 Hystrix 線程池進行 RPC 隔離,可以在應用配置文件中進行相應配置。下面是 demo-provider 的 RPC 線程池配置的實例:

hystrix:
  threadpool:
    default:
      coreSize: 10  # 線程池核心線程數
      maximumSize: 20   # 線程池最大線程數
      allowMaximumSizeToDivergeFromCoreSize: true   # 線程池maximumSize最大線程數是否生效
      keepAliveTimeMinutes:10         # 設置可空閒時間,單位爲分鐘
  command:
    default:         #全局默認配置
      execution:        #RPC隔離的相關配置
        isolation:
           strategy: THREAD     #配置請求隔離的方式,這裏採用線程池方式
           thread:
          timeoutInMilliseconds: 100000   #RPC執行的超時時間,默認爲1000毫秒
          interruptOnTimeout: true     #發生超時後是否中斷方法的執行,默認值爲true

對上面實例中用到的與 Hystrix 線程池有關的配置項介紹如下:

(1)hystrix.threadpool.default.coreSize

設值線程池的核心線程數。

(2)hystrix.threadpool.default.maximumSize

設值線程池的最大線程數,起作用的前提是 allowMaximumSizeToDrivergeFromCoreSize 的屬性值爲 true。maximumSize 屬性值可以等於或者大於 coreSize 值,當線程池的線程不夠用時,Hystrix 會創建新的線程,直到線程數達到 maximumSize 的值,創建的線程爲非核心線程。

(3)hystrix.threadpool.default.allowMaximumSizeToDivergeFromCoreSize

該屬性允許 maximumSize 起作用。

(4)hystrix.threadpool.default.keepAliveTimeMinutes

該屬性設置非核心線程的存活時間,如果某個非核心線程的空閒時間超過 keepAliveTimeMinutes 設置的時間,非核心線程將被釋放。其單位爲分鐘,默認值爲 1,默認情況下非核心線程空閒 1 分鐘後釋放。

(5)hystrix.command.default.execution.isolation.strategy

該屬性設置完成 RPC 遠程調用 HystrixCommand 命令的隔離策略。它有兩個可選值:THREAD、SEMAPHORE,默認值爲 THREAD。THREAD 表示使用線程池進行 RPC 隔離,SEMAPHORE 表示通過信號量來進行 RPC 隔離和限制併發量。

(6)hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds

設置調用者等待 HystrixCommand 命令執行的超時限制,超過此時間,HystrixCommand 被標記爲 TIMEOUT,並執行回退邏輯。超時會作用在 HystrixCommand.queue(),即使調用者沒有調用 get() 去獲得 Future 對象。

以上的配置是 application 應用級別的默認線程池配置,覆蓋的範圍爲系統中的所有 RPC 線程池。有時,需要爲特定的 Provider 微服務提供者做特殊的配置,比如當某一個 Provider 的接口訪問的併發量非常大,是其他 Provider 的幾十倍時,則其遠程調用需要更多的 RPC 線程,這時候,可以單獨爲它進行專門的 RPC 線程池配置。作爲示例,在 demo-Provider 中對 uaa-provider 的 RPC 線程池配置如下:

hystrix:
  threadpool:
    default:
      coreSize: 10   # 線程池核心線程數
      maximumSize: 20    # 線程池最大線程數
      allowMaximumSizeToDivergeFromCoreSize: true   # 線程池最大線程數是否有效
    uaa-provider:
      coreSize: 20     # 線程池核心線程數
      maximumSize: 100    # 線程池最大線程數
      allowMaximumSizeToDivergeFromCoreSize: true   # 線程池最大線程數是否有效

上面的配置中使用了 hystrix.threadpool.uaa-provider 配置項前綴,其中 uaa-provider 部分爲 RPC 線程池的 Thread Pool Key(線程池名稱),也就是默認的 Command Group Key(命令組名)。

在調用處理器 HystrixInvocationHandler 的 invoke(…) 方法內打上斷點,在調試時,通過查看 hystrixCommand 對象的值可以看出,demo-provider 中針對微服務提供者 uaa-provider 的 RPC 線程池配置已經生效,如圖 6-8 所示。

圖 6-8 針對 uaa-provider 的 RPC 線程池配置已經生效

Hystrix 信號量隔離

除了使用線程池進行資源隔離之外,Hystrix 還可以使用信號量機制完成資源隔離。信號量所起到的作用就像一個開關,而信號量的值就是每個命令的併發執行數量,當併發數高於信號量的值時,就不再執行命令。

比如,如果 Provider A 的 RPC 信號量大小爲 10,那麼它同時只允許有 10 個 RPC 線程來訪問 Provider A,其他的請求都會被拒絕,從而達到資源隔離和限流保護的作用。

Hystrix 信號量機制不提供專用的線程池,也不提供額外的線程,在獲取信號量之後,執行 HystrixCommand 命令邏輯的線程還是之前 Web 容器的 IO 線程。

信號量可以細分爲 run 執行信號量和 fallback 回退信號量。

IO 線程在執行 HystrixCommand 命令之前,需要搶到 run 執行信號量,成功之後才允許執行 HystrixCommand.run() 方法。如果爭搶失敗,就準備回退,但是在執行 HystrixCommand.getFallback() 回退方法之前,還需要爭搶 fallback 回退信號量,成功之後才允許執行 HystrixCommand.getFallback() 回退方法。如果都獲取失敗,則操作直接終止。

在如圖 6-9 所示的例子中,假設有 5 個 Web 容器的 IO 線程併發進行 RPC 遠程調用,但是執行信號量的大小爲 3,也就是隻有 3 個 IO 線程能夠真正地搶到 run 執行信號量,爭搶成功後這些線程才能發起 RPC 調用。剩下的 2 個 IO 線程準備回退,去搶 fallback 回退信號量,爭搶成功後執行 HystrixCommand.getFallback() 回退方法。

圖 6-9 5 個 Web 容器的 IO 線程爭搶信號量

下面是一個模擬 Web 容器進行 RPC 調用的演示程序,使用一個擁有 50 個線程的線程池模擬 Web 容器的 IO 線程池,並使用隨書編寫的 HttpGetterCommand 命令模擬 RPC 調用。實驗之前,需要提前啓動的 demo-provider 服務的 REST 接口 / api/demo/hello/v1。

爲了演示信號量隔離,演示程序所設置的 run 執行信號量和 fallback 回退信號量都爲 4,並且通過 IO 線程池同時提交了 50 個模擬的 RPC 調用去爭搶這些信號量,具體的演示程序如下:

package com.crazymaker.demo.hystrix;

//省略import

@Slf4j
public class IsolationStrategyDemo
{

    /**
     * 測試: 信號量隔離
     */
    @Test
    public void testSemaphoreIsolationStrategy() throws Exception
    {
        /**
         *命令屬性實例
         */
        HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter()
                .withExecutionTimeoutInMilliseconds(5000)  //配置時間上限
                .withExecutionIsolationStrategy(
                        //隔離策略爲信號量隔離
                        HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE
                )
                //HystrixCommand.run()方法允許的最大請求數
                .withExecutionIsolationSemaphoreMaxConcurrentRequests(4)
                //HystrixCommand.getFallback()方法允許的最大請求數
                .withFallbackIsolationSemaphoreMaxConcurrentRequests(4);

       /**
         * 命令的配置實例
         */
        HystrixCommand.Setter setter = HystrixCommand.Setter
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey("group1"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("command1"))
                .andCommandPropertiesDefaults(commandProperties);

        /**
         * 模擬Web容器的IO線程池
         */
        ExecutorService mock_IO_threadPool = Executors.newFixedThreadPool(50);

        /**
         *  模擬Web容器的併發50
         */
        for (int j = 1; j <= 50; j++)
        {
            mock_IO_threadPool.submit(() ->
            {
                /**
                 * RPC調用
                 */
                new HttpGetterCommand(HELLO_TEST_URL, setter)
                        .toObservable()
                        .subscribe(s -> log.info(" result:{}", s));
            });
        }
        Thread.sleep(Integer.MAX_VALUE);
    }
}

在執行此演示程序之前,需要啓動 crazydemo.com(指向 127.0.0.1)主機上的 demo-provider 微服務提供者。demo-provider 啓動之後,再執行上面的演示程序,運行的結果節選如下:

[pool-2-thread-35] INFO  c.c.d.h.HttpGetterCommand - req3 fallback: 熔斷false,直接失敗true,失敗次數3
[pool-2-thread-45] INFO  c.c.d.h.HttpGetterCommand - req4 fallback: 熔斷false,直接失敗true,失敗次數4
[pool-2-thread-7] INFO  c.c.d.h.HttpGetterCommand - req2 fallback: 熔斷false,直接失敗true,失敗次數2
[pool-2-thread-15] INFO  c.c.d.h.HttpGetterCommand - req1 fallback: 熔斷false,直接失敗true,失敗次數1
[pool-2-thread-35] INFO  c.c.d.h.IsolationStrategyDemo -  result:req3:調用失敗
...
[pool-2-thread-27] INFO  c.c.d.h.HttpGetterCommand - req7 begin...
[pool-2-thread-18] INFO  c.c.d.h.HttpGetterCommand - req6 begin...
[pool-2-thread-13] INFO  c.c.d.h.HttpGetterCommand - req5 begin...
[pool-2-thread-48] INFO  c.c.d.h.HttpGetterCommand - req8 begin...
[pool-2-thread-18] INFO  c.c.d.h.HttpGetterCommand -  req6 end: {"respCode":0,"respMsg":"操作成功...}
[pool-2-thread-48] INFO  c.c.d.h.HttpGetterCommand -  req8 end: {"respCode":0,"respMsg":"操作成功...}
[pool-2-thread-27] INFO  c.c.d.h.HttpGetterCommand -  req7 end: {"respCode":0,"respMsg":"操作成功...}
[pool-2-thread-13] INFO  c.c.d.h.HttpGetterCommand -  req5 end: {"respCode":0,"respMsg":"操作成功...}
[pool-2-thread-13] INFO  c.c.d.h.IsolationStrategyDemo -  result:req5:{"respCode":0,"respMsg":"操作成...}
...

通過結果可以看出:

1)執行 RPC 遠程調用的線程就是模擬 IO 線程池中的線程。

2)雖然提交了 50 個 RPC 調用,但是隻有 4 個 RPC 調用搶到了執行信號量,分別爲 req5、req6、req7、req8。

3)雖然失敗了 46 個 RPC 調用,但是隻有 4 個 RPC 調用搶到了回退信號量,分別爲 req1、req2、req3、req4。

使用信號量進行 RPC 隔離時,是有自身弱點的。由於最終 Web 容器的 IO 線程完成實際 RPC 遠程調用,這樣就帶來了一個問題:由於 RPC 遠程調用是一種耗時的操作,如果 IO 線程被長時間佔用,將導致 Web 容器請求處理能力下降,甚至可能會在一段時間內由於 IO 線程被佔滿而造成 Web 容器無法對新的用戶請求及時響應,最終導致 Web 容器崩潰。因此,信號量隔離機制不適用於 RPC 隔離。但是,對於一些非網絡的 API 調用或者耗時很小的 API 調用,信號量隔離機制比線程池隔離機制的效率更高。

再來看信號量的配置,這一次使用代碼的方式進行命令屬性配置,涉及 Hystrix 命令屬性配置器 HystrixCommandProperties.Setter() 的以下實例方法:

(1)withExecutionIsolationSemaphoreMaxConcurrentRequests(int)

此方法設置使用執行信號量的大小,也就是 HystrixCommand.run() 方法允許的最大請求數。如果達到最大請求數,則後續的請求會被拒絕。

在 Web 容器中,搶佔信號量的線程應該是容器(比如 Tomcat)IO 線程池的一小部分,所以信號量的數量不能大於容器線程池大小,否則起不到保護作用。執行信號量大小的默認值爲 10。

如果使用屬性配置而不是代碼方式進行配置,則以上代碼配置所對應的配置項爲:

hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests

(2)withFallbackIsolationSemaphoreMaxConcurrentRequests (int)

此方法設置使用回退信號量的大小,也就是 HystrixCommand.getFallback() 方法允許的最大請求數。如果達到最大請求數,則後續的回退請求會被拒絕。

如果使用屬性配置而不是代碼方式進行配置,則以上代碼配置所對應的配置項爲:

hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests

線程池與信號量區別

最後,介紹一下信號量隔離與線程池隔離的區別,分別從調用線程、開銷、異步、併發量 4 個維度進行對比,具體如表 6-1 所示。

表 6-1 調用線程、開銷、異步、併發量 4 個維度的對比

9h7lmu

適用場景

Hystrix 熔斷降級(過載保護)

熔斷器的工作機制爲:統計最近 RPC 調用發生錯誤的次數,然後根據統計值中的失敗比例等信息,決定是否允許後面的 RPC 調用繼續,或者快速地失敗回退。

熔斷器的 3 種狀態如下:

1)closed:熔斷器關閉狀態,這也是熔斷器的初始狀態,此狀態下 RPC 調用正常放行。

2)open:失敗比例到一定的閾值之後,熔斷器進入開啓狀態,此狀態下 RPC 將會快速失敗,執行失敗回退邏輯。

3)half-open:在打開一定時間之後(睡眠窗口結束),熔斷器進入半開啓狀態,小流量嘗試進行 RPC 調用放行。如果嘗試成功則熔斷器變爲 closed 狀態,RPC 調用正常;如果嘗試失敗則熔斷器變爲 open 狀態,RPC 調用快速失敗。

斷路器有 3 種狀態:

斷路器觀察到請求失敗比例沒有達到閾值,斷路器認爲被代理服務狀態良好。

斷路器觀察到請求失敗比例已經達到閾值,斷路器認爲被代理服務故障,打開開關,請求不再到達被代理的服務,而是快速失敗。

斷路器打開後,爲了能自動恢復對被代理服務的訪問,會切換到 HALF-OPEN 半開放狀態,去嘗試請求被代理服務以查看服務是否已經故障恢復。如果成功,會轉成 CLOSED 狀態,否則轉到 OPEN 狀態。

斷路器的狀態切換圖如下:

Hystrix 實現熔斷機制

在 Spring Cloud 中,熔斷機制是通過 Hystrix 實現的。

Hystrix 會監控微服務間調用的狀況,當失敗調用到一定比例時(例如 5 秒內失敗 20 次),就會啓動熔斷機制。 Hystrix 實現服務熔斷的步驟如下:

  1. 當服務的調用出錯率達到或超過 Hystix 規定的比率(默認爲 50%)後,熔斷器進入熔斷開啓狀態。

  2. 熔斷器進入熔斷開啓狀態後,Hystrix 會啓動一個休眠時間窗,在這個時間窗內,該服務的降級邏輯會臨時充當業務主邏輯,而原來的業務主邏輯不可用。

  3. 當有請求再次調用該服務時,會直接調用降級邏輯快速地返回失敗響應,以避免系統雪崩。

  4. 當休眠時間窗到期後,Hystrix 會進入半熔斷轉態,允許部分請求對服務原來的主業務邏輯進行調用,並監控其調用成功率。

  5. 如果調用成功率達到預期,則說明服務已恢復正常,Hystrix 進入熔斷關閉狀態,服務原來的主業務邏輯恢復;否則 Hystrix 重新進入熔斷開啓狀態,休眠時間窗口重新計時,繼續重複第 2 到第 5 步。

熔斷器狀態之間相互轉換的邏輯關係如圖 6-10 所示。

圖 6-10 熔斷器狀態之間的轉換關係詳細圖

涉及到了 4 個與 Hystrix 熔斷機制相關的重要參數,這 4 個參數的含義如下表。

41AYBB

熔斷器狀態變化的演示實例

爲了觀察熔斷器的狀態變化,通過繼承 HystrixCommand 類,這裏特別設計了一個能夠設置運行時長的自定義命令類 TakeTimeDemoCommand,通過設置其運行佔用時間 takeTime 成員的值,可以控制其運行過程中是否超時。

演示實例的代碼如下:

package com.crazymaker.demo.hystrix;
//省略import

@Slf4j
public class CircuitBreakerDemo
{
    //執行的總次數,線程安全
    private static AtomicInteger total = new AtomicInteger(0);

    /**
     * 內部類:一個能夠設置運行時長的自定義命令類
     */
    static class TakeTimeDemoCommand extends HystrixCommand<String>
    {

        //run方法是否執行
        private boolean hasRun = false;
        //執行的次序
        private int index;
        //運行的佔用時間
        long takeTime;

        public TakeTimeDemoCommand(long takeTime, Setter setter)
        {
            super(setter);
            this.takeTime = takeTime;
        }

        @Override
        protected String run() throws Exception
        {
            hasRun = true;
            index = total.incrementAndGet();

            Thread.sleep(takeTime);
            HystrixCommandMetrics.HealthCounts hc = super.getMetrics().getHealthCounts();
            log.info("succeed- req{}:熔斷器狀態:{}, 失敗率:{}%",
                        index, super.isCircuitBreakerOpen(), hc.getErrorPercentage());
            return "req" + index + ":succeed";
        }

        @Override
        protected String getFallback()
        {
            //是否直接失敗
            boolean isFastFall = !hasRun;
            if (isFastFall)
            {
                index = total.incrementAndGet();
            }
            HystrixCommandMetrics.HealthCounts hc = super.getMetrics().getHealthCounts();
            log.info("fallback- req{}:熔斷器狀態:{}, 失敗率:{}%",
                        index, super.isCircuitBreakerOpen(), hc.getErrorPercentage());
            return "req" + index + ":failed";
        }

    }

    /**
     * 測試用例:熔斷器熔斷
     */

    @Test
    public void testCircuitBreaker() throws Exception
    {
        /**
         * 命令參數配置
         */
        HystrixCommandProperties.Setter propertiesSetter =
                HystrixCommandProperties.Setter()
                        //至少有3個請求, 熔斷器才達到熔斷觸發的次數閾值
                        .withCircuitBreakerRequestVolumeThreshold(3)
                        //熔斷器中斷請求5秒後會進入half-open狀態, 嘗試放行
                        .withCircuitBreakerSleepWindowInMilliseconds(5000)
                        //錯誤率超過60%,快速失敗
                        .withCircuitBreakerErrorThresholdPercentage(60)
                        //啓用超時
                        .withExecutionTimeoutEnabled(true)
                        //執行的超時時間,默認爲 1000毫秒,這裏設置爲500毫秒
                        .withExecutionTimeoutInMilliseconds(500)
                        //可統計的滑動窗口內的buckets數量,用於熔斷器和指標發佈
                        .withMetricsRollingStatisticalWindowBuckets(10)
                        //可統計的滑動窗口的時間長度
                        //這段時間內的執行數據用於熔斷器和指標發佈
                        .withMetricsRollingStatisticalWindowInMilliseconds(10000);

        HystrixCommand.Setter rpcPool = HystrixCommand.Setter
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey("group-1"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("command-1"))
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("threadPool-1"))
                .andCommandPropertiesDefaults(propertiesSetter);

      /**
         * 首先設置運行時間爲800毫秒,大於命令的超時限制500毫秒
         */
        long takeTime = 800;
        for (int i = 1; i <= 10; i++)
        {

            TakeTimeDemoCommand command = new TakeTimeDemoCommand(takeTime, rpcPool);
            command.execute();

            //健康信息
            HystrixCommandMetrics.HealthCounts hc = command.getMetrics().getHealthCounts();
            if (command.isCircuitBreakerOpen())
            {
                /**
                 * 熔斷之後,設置運行時間爲300毫秒,小於命令的超時限制 500毫秒
                 */
                takeTime = 300;
                log.info("============  熔斷器打開了,等待休眠期(默認5秒)結束");

                /**
                 * 等待7秒之後,再一次發起請求
                 */
                Thread.sleep(7000);
            }

        }

        Thread.sleep(Integer.MAX_VALUE);

    }
}

在上面的演示中,有以下配置器的配置命令需要重點說明:

1)通過 withExecutionTimeoutInMilliseconds(int)方法將默認爲 1000 毫秒的執行超時上限設置爲 500 毫秒,也就是說,只要 TakeTimeDemoCommand.run() 的執行超過 500 毫秒就會觸發 Hystrix 超時回退。

2)通過 withCircuitBreakerRequestVolumeThreshold(int)方法將熔斷器觸發熔斷的最少請求次數的默認值 20 次改爲了 3 次,這樣更容易測試。

3)通過 withCircuitBreakerErrorThresholdPercentage(int)方法設置錯誤率閾值百分比的值爲 60,滑動窗口時間內當錯誤率超過此值時,熔斷器進入 open 開啓狀態,所有請求都會觸發失敗回退(fallback),錯誤率閾值百分比的默認值爲 50。

執行上面的演示實例,運行的結果節選如下:

[HystrixTimer-1] INFO  c.c.d.h.CircuitBreakerDemo - fallback- req1:熔斷器狀態:false, 失敗率:0%
[HystrixTimer-1] INFO  c.c.d.h.CircuitBreakerDemo - fallback- req2:熔斷器狀態:false, 失敗率:100%
[HystrixTimer-2] INFO  c.c.d.h.CircuitBreakerDemo - fallback- req3:熔斷器狀態:false, 失敗率:100%
[HystrixTimer-1] INFO  c.c.d.h.CircuitBreakerDemo - fallback- req4:熔斷器狀態:true, 失敗率:100%
[main] INFO  c.c.d.h.CircuitBreakerDemo - ============  熔斷器打開了,等待休眠期(默認5秒)結束
[hystrix-threadPool-1-5] INFO  c.c.d.h.CircuitBreakerDemo - succeed- req5:熔斷器狀態:true, 失敗率:100%
[hystrix-threadPool-1-6] INFO  c.c.d.h.CircuitBreakerDemo - succeed- req6:熔斷器狀態:false, 失敗率:0%
[hystrix-threadPool-1-7] INFO  c.c.d.h.CircuitBreakerDemo - succeed- req7:熔斷器狀態:false, 失敗率:0%
[hystrix-threadPool-1-8] INFO  c.c.d.h.CircuitBreakerDemo - succeed- req8:熔斷器狀態:false, 失敗率:0%
[hystrix-threadPool-1-9] INFO  c.c.d.h.CircuitBreakerDemo - succeed- req9:熔斷器狀態:false, 失敗率:0%
[hystrix-threadPool-1-10] INFO  c.c.d.h.CircuitBreakerDemo - succeed- req10:熔斷器狀態:false, 失敗率:0%

從上面的執行結果可知,在第四次請求 req4 時,熔斷器才達到熔斷觸發的次數閾值 3,由於前 3 次皆爲超時失敗,失敗率大於閾值 60%,因此第四次請求執行之後,熔斷器狀態爲 open 熔斷狀態。

在命令的熔斷器打開後,熔斷器默認會有 5 秒的睡眠等待時間,在這段時間內的所有請求直接執行回退方法;5 秒之後,熔斷器會進入 half-open 狀態, 嘗試放行一次命令執行,如果成功則關閉熔斷器,狀態轉成 closed,否則,熔斷器回到 open 狀態。

在上面的程序中,在熔斷器熔斷之後,演示程序將命令的運行時間 takeTime 改成了 300 毫秒,小於命令的超時限制 500 毫秒。在等待 7 秒之後,演示程序再一次發起請求,從運行結果可以看到,第 5 次請求 req5 執行成功了,這是一次 half-open 狀態的嘗試放行,請求成功之後,熔斷器的狀態轉成了 open,後續請求將繼續放行。注意,演示程序第 5 次請求 req5 後的熔斷器狀態值反映在第 6 次請求 req6 的執行輸出中。

熔斷器和滑動窗口的配置屬性

熔斷器的配置包含了滑動窗口的配置和熔斷器自身的配置。

Hystrix 的健康統計是通過滑動窗口來完成的,其熔斷器的狀態也是依據滑動窗口的統計數據來變化的,所以這裏先介紹滑動窗口的配置。

先看看兩個概念:滑動窗口和時間桶。

1. 滑動窗口

可以這麼來理解滑動窗口:一位乘客坐在正在行駛的列車的靠窗座位上,列車行駛的公路兩側種着一排挺拔的白楊樹,隨着列車的前進,路邊的白楊樹迅速從窗口滑過,我們用每棵樹來代表一個請求,用列車的行駛代表時間的流逝,那麼,列車上的這個窗口就是一個典型的滑動窗口,這個乘客能通過窗口看到的白楊樹的數量,就是滑動窗口要統計的數據。

2. 時間桶

時間桶是統計滑動窗口數據時的最小單位。同樣類比列車窗口,在列車速度非常快時,如果每掠過一棵樹就統計一次窗口內樹的數據,顯然開銷非常大,如果乘客將窗口分成 N 份,前進行時列車每掠過窗口的 N 分之一就統計一次數據,開銷就大大地減小了。簡單來說,時間桶也就是滑動窗口的 N 分之一。

代碼方式下熔斷器的設置可以使用 HystrixCommandProperties.Setter() 配置器來完成,參考 6.5.1 節的實例,把自定義的 TakeTimeDemoCommand 中 Setter() 配置器的相關參數配置如下:

/**
 * 命令參數配置
 */
HystrixCommandProperties.Setter propertiesSetter =
    HystrixCommandProperties.Setter()
    //至少有3個請求, 熔斷器才達到熔斷觸發的次數閾值
    .withCircuitBreakerRequestVolumeThreshold(3)
    //熔斷器中斷請求5秒後會進入half-open狀態,進行嘗試放行
    .withCircuitBreakerSleepWindowInMilliseconds(5000)
    //錯誤率超過60%,快速失敗
    .withCircuitBreakerErrorThresholdPercentage(60)
    //啓用超時
    .withExecutionTimeoutEnabled(true)
    //執行的超時時間,默認爲 1000毫秒,這裏設置爲500毫秒
    .withExecutionTimeoutInMilliseconds(500)
    //可統計的滑動窗口內的buckets數量,用於熔斷器和指標發佈
    .withMetricsRollingStatisticalWindowBuckets(10)
    //可統計的滑動窗口的時間長度
    //這段時間內的執行數據用於熔斷器和指標發佈
    .withMetricsRollingStatisticalWindowInMilliseconds(10000);

在以上配置中,與熔斷器的滑動窗口相關的配置的具體含義爲:

1)滑動窗口中,最少 3 個請求才會觸發斷路,默認值爲 20 個。

2)錯誤率達到 60% 時纔可能觸發斷路,默認值爲 50%。

3)斷路之後的 5000 毫秒內,所有請求都直接調用 getFallback() 進行回退降級,不會調用 run() 方法;5000 毫秒過後,熔斷器變爲 half-open 狀態。

以上 TakeTimeDemoCommand 的熔斷器滑動窗口的狀態轉換關係如圖 6-11 所示。

圖 6-11 TakeTimeDemoCommand 的熔斷器健康統計滑動窗口的狀態轉換關係圖

大家已經知道,Hystrix 熔斷器的配置除了代碼方式,還有 properties 文本屬性配置的方式;

另外 Hystrix 熔斷器相關的滑動窗口不止一個基礎的健康統計滑動窗口,還包含一個百分比命令執行時間統計滑動窗口,兩個窗口都可以進行配置。

下面以文本屬性配置方式爲主,詳細介紹 Hystrix 基礎健康統計滑動窗口的配置:

(1)hystrix.command.default.metrics.rollingStats.timeInMilliseconds

設置健康統計滑動窗口的持續時間(以毫秒爲單位),默認值爲 10000 毫秒。熔斷器的狀態會根據滑動窗口的統計值來計算,若滑動窗口時間內的錯誤率超過閾值,熔斷器將進入 open 開啓狀態,滑動窗口將被進一步細分爲時間桶,滑動窗口的統計值等於窗口內所有時間桶的統計信息的累加,每個時間桶的統計信息包含請求的成功(success)、失敗(failure)、超時(timeout)、被拒(rejection)的次數。

此選項通過代碼方式配置時所對應的函數如下:

HystrixCommandProperties.Setter().withMetricsRollingStatisticalWindowInMilliseconds(int)

(2)hystrix.command.default.metrics.rollingStats.numBuckets

設置健康統計滑動窗口被劃分爲時間桶的數量,默認值爲 10。若滑動窗口的持續時間爲默認的 10000 毫秒,則一個時間桶(bucket)的時間即 1 秒。如果要做定製化的配置,則所設置的 numBuckets(時間桶數量)值和 timeInMilliseconds(滑動窗口時長)值有關聯關係,必須符合 timeInMilliseconds % numberBuckets == 0 的規則,否則會拋出異常。例如二者的關聯關係爲 70000(滑動窗口 70 秒)% 700(桶數)==0 是可以的,但是 70000(70 秒)% 600(桶數)== 400 將拋出異常。

此選項通過代碼方式配置時所對應的函數如下:

HystrixCommandProperties.Setter().withMetricsRollingStatisticalWindowBuckets (int)

(3)hystrix.command.default.metrics.healthSnapshot.intervalInMilliseconds

設置健康統計滑動窗口拍攝運行狀況統計指標的快照的時間間隔。什麼是拍攝運行狀況統計指標的快照呢?就是計算成功和錯誤百分比這些影響熔斷器狀態的統計數據。

拍攝快照的時間間隔的單位爲毫秒,默認值爲 500 毫秒。由於統計指標的計算是一個耗 CPU 的操作(CPU 密集型操作),也就是說,高頻率地計算錯誤百分比等健康統計數據會佔用很多 CPU 資源,所以,在高併發 RPC 流量大的場景下,可以適當調大拍攝快照的時間間隔。

此選項通過代碼方式配置時所對應的函數如下:

HystrixCommandProperties.Setter().withMetricsHealthSnapshotIntervalInMilliseconds (int)

Hystrix 熔斷器相關的滑動窗口不止一個基礎的健康統計滑動窗口,還包含一個 “百分比命令執行時間” 統計滑動窗口。什麼是 “百分比命令執行時間” 統計滑動窗口呢?該滑動窗口主要用於統計 1%、10%、50%、90%、99% 等一系列比例的命令執行平均耗時,主要用以生成統計圖表。

帶 hystrix.command.default.metrics.rollingPercentile 前綴的配置項,專門用於配置百分比命令執行時間統計窗口。

下面以文本屬性配置方式爲主,詳細介紹 Hystrix 執行時間百分比統計滑動窗口的配置:

(1)hystrix.command.default.metrics.rollingPercentile.enabled:

該配置項用於設置百分比命令執行時間統計窗口是否生效,命令的執行時間是否被跟蹤,並且計算各個百分比如 1%、10%、50%、90%、99.5% 等的平均時間。該配置項默認爲 true。

(2)hystrix.command.default.metrics.rollingPercentile.timeInMilliseconds

設置百分比命令執行時間統計窗口的持續時間(以毫秒爲單位),默認值爲 60000 毫秒,當然,此滑動窗口也會被進一步細分爲時間桶,以便提高統計的效率。

本選項通過代碼方式配置時所對應的函數如下:

HystrixCommandProperties.Setter().withMetricsRollingPercentileWindowInMilliseconds(int)

(3)hystrix.command.default.metrics.rollingPercentile.numBuckets

設置百分比命令執行時間統計窗口被劃分爲時間桶的數量,默認值爲 6。此滑動窗口的默認持續時間爲默認的 60000 毫秒,即默認情況下,一個時間桶的時間爲 10 秒。如果要做定製化的配置,此窗口所設置的 numBuckets(時間桶數量)值和 timeInMilliseconds(滑動窗口時長)值有關聯關係,必須符合 timeInMilliseconds(滑動窗口時長)% numberBuckets == 0 的規則,否則將拋出異常。

此選項通過代碼方式配置時所對應的函數如下:

HystrixCommandProperties.Setter().withMetricsRollingPercentileWindowBuckets (int)

(4)hystrix.command.default.metrics.rollingPercentile.bucketSize

設置百分比命令執行時間統計窗口的時間桶內最大的統計次數,如果 bucketSize 爲 100,而桶的時長爲 1 秒,若這 1 秒裏有 500 次執行,則只有最後 100 次執行的信息會被統計到桶裏去。增加此配置項的值會導致內存開銷及其他計算開銷的上升,該配置項的默認值爲 100。

此選項通過代碼方式配置時所對應的函數如下:

HystrixCommandProperties.Setter().withMetricsRollingPercentileBucketSize (int)

以上是 Hystrix 熔斷器相關的滑動窗口的配置,接下來是熔斷器本身的配置。

帶 hystrix.command.default.circuitBreaker 前綴的配置項專門用於對熔斷器本身進行配置。

下面以文本屬性配置方式爲主,對 Hystrix 熔斷器的配置進行一下詳細介紹:

(1)hystrix.command.default.circuitBreaker.enabled

該配置用來確定是否啓用熔斷器,默認值爲 true。

此選項通過代碼方式配置時所對應的函數如下:

HystrixCommandProperties.Setter().withCircuitBreakerEnabled (boolean)

(2)hystrix.command.default.circuitBreaker.requestVolumeThreshold

該配置用於設置熔斷器觸發熔斷的最少請求次數。如果設爲 20,那麼當一個滑動窗口時間內(比如 10 秒)收到 19 個請求,即使 19 個請求都失敗,熔斷器也不會打開變成 open 狀態。默認值爲 20。

此選項通過代碼方式配置時所對應的函數如下:

HystrixCommandProperties.Setter().withCircuitBreakerRequestVolumeThreshold (int)

(3)hystrix.command.default.circuitBreaker.errorThresholdPercentage

該配置用於設置錯誤率閾值,當健康統計滑動窗口的錯誤率超過此值時,熔斷器進入 open 開啓狀態,所有請求都會觸發失敗回退(fallback)。錯誤率閾值百分比的默認值爲 50。

此選項通過代碼方式配置時所對應的函數如下:

HystrixCommandProperties.Setter().withCircuitBreakerErrorThresholdPercentage (int)

(4)hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds

此配置項指定了熔斷器打開後經過多長時間允許一次請求嘗試執行。熔斷器打開時,Hystrix 會在經過一段時間後就放行一條請求,如果這條請求執行成功了,說明此時服務很可能已經恢復了正常,那麼就會關閉熔斷器;如果此請求執行失敗,則認爲目標服務依然不可用,熔斷器繼續保持打開狀態。

該配置用於配置熔斷器的睡眠窗口,具體指的是熔斷器打開之後過多長時間才允許一次請求嘗試執行,默認值爲 5000 毫秒,表示當熔斷器開啓(open)後,5000 毫秒內會拒絕所有的請求,5000 毫秒之後,熔斷器纔會進行入 half-open 狀態。

此選項通過代碼方式配置時所對應的函數如下:

HystrixCommandProperties.Setter().withCircuitBreakerSleepWindowInMilliseconds (int)

(5)hystrix.command.default.circuitBreaker.forceOpen

如果配置爲 true,則熔斷器將被強制打開,所有請求將被觸發失敗回退(fallback)。此配置的默認值爲 false。

此選項通過代碼方式配置時所對應的函數如下:

HystrixCommandProperties.Setter().withCircuitBreakerForceOpen (boolean)

下面是本書隨書實例中 demo-provider 中的有關熔斷器的配置,節選如下:

hystrix:
  ...
  command:
    ... 
    default:          #全局默認配置
      circuitBreaker:        #熔斷器相關配置
          enabled: true        #是否啓動熔斷器,默認爲true
          requestVolumeThreshold: 20      #啓用熔斷器功能窗口時間內的最小請求數
          sleepWindowInMilliseconds: 5000  #指定熔斷器打開後多長時間內允許一次請求嘗試執行
          errorThresholdPercentage: 50    #窗口時間內超過50%的請求失敗後就會打開熔斷器
      metrics:
          rollingStats:
              timeInMilliseconds: 6000
              numBuckets: 10
    UserClient#detail(Long):       #獨立接口配置,格式爲: 類名#方法名(參數類型列表)
       circuitBreaker:        #熔斷器相關配置
          enabled: true        #是否使用熔斷器,默認爲true
          requestVolumeThreshold: 20       #窗口時間內的最小請求數
          sleepWindowInMilliseconds: 5000  #打開後允許一次嘗試的睡眠時間,默認配置爲5秒
          errorThresholdPercentage: 50     #窗口時間內熔斷器開啓的錯誤比例,默認配置爲50
       metrics:
          rollingStats:
             timeInMilliseconds: 10000      #滑動窗口時間
             numBuckets: 10        #滑動窗口的時間桶數

使用文本格式配置時,可以對熔斷器的參數值做默認配置,也可以對特定的 RPC 接口做個性化配置。對熔斷器的參數值做默認配置時,使用 hystrix.command.default 默認前綴;對特定的 RPC 接口做個性化配置時,使用 hystrix.command.FeignClient#Method 格式的前綴。上面的演示例子中,對遠程客戶端 Feign 接口 UserClient 中的 detail(Long) 方法做了個性化的熔斷器配置,其配置項的前綴爲:

hystrix.command. UserClient#detail(Long)

Hystrix 命令的執行流程

在獲取 HystrixCommand 命令的執行結果時,無論是調用 execute() 和 toObservable() 方法,還是調用 observe() 方法,最終都會通過 HystrixCommand.toObservable() 訂閱執行結果和返回。

在 Hystrix 內部,調用 toObservable() 方法返回一個觀察的主題,當 Subscriber 訂閱者訂閱主題後,HystrixCommand 會彈射一個事件,然後通過一系列的判斷(順序依次是緩存是否命中、熔斷器是否打開、線程池是否佔滿),開始執行實際的 HystrixCommand.run() 方法,該方法的實現主要爲異步處理的業務邏輯,如果這其中任何一個環節出現錯誤或者拋出異常,它都會回退到 getFallback() 方法進行服務降級處理,當降級處理完成之後,它會將結果返回給實際的調用者。

HystrixCommand 的工作流程,總結起來大致如下:

1)判斷是否使用緩存響應請求,若啓用了緩存,且緩存可用,則直接使用緩存響應請求。Hystrix 支持請求緩存,但需要用戶自定義啓動。

2)判斷熔斷器是否開啓,如果熔斷器處於 open 狀態,則跳到第 5 步。

3)如果使用線程池進行請求隔離,則判斷線程池是否已滿,已滿則跳到第 5 步;如果使用信號量進行請求隔離,則判斷信號量是否耗盡,耗盡則跳到第 5 步。

4)執行 HystrixCommand.run() 方法執行具體業務邏輯,如果執行失敗或者超時,則跳到第 5 步,否則跳到第 6 步。

5)執行 HystrixCommand.getFallback() 服務降級處理邏輯。

6)返回請求響應。

以上流程如圖 6-12 所示。

圖 6-12 HystrixCommand 的執行流程示意圖

什麼場景下會觸發 fallback 方法呢?請見表 6-2。

表 6-2 觸發 fallback 方法的場景

gbNDhK

Sentinel 限流降級

Sentinel 限流降級的流量控制 (Flow Control) 策略,原理是監控應用流量的 QPS 或併發線程數等指標,當達到指定閾值時對流量進行控制,避免系統被瞬時的流量高峯沖垮,保障應用高可用性。

通過流控規則來指定允許該資源通過的請求次數,例如下面的代碼定義了資源 HelloWorld 每秒最多隻能通過 20 個請求。

參考的規則定義如下:

private static void initFlowRules(){
    List<FlowRule> rules = new ArrayList<>();
    FlowRule rule = new FlowRule();
    rule.setResource("HelloWorld");
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    // Set limit QPS to 20.
    rule.setCount(20);
    rules.add(rule);
    FlowRuleManager.loadRules(rules);
}

一條限流規則主要由下面幾個因素組成,我們可以組合這些元素來實現不同的限流效果:

基本的參數

資源名:唯一名稱,默認請求路徑

針對來源:Sentinel 可以針對調用者進行限流,填寫微服務名,默認爲 default(不區分來源)

閾值類型 / 單機閾值:

  1. QPS:每秒請求數,當前調用該 api 的 QPS 到達閾值的時候進行限流

  2. 線程數:當調用該 api 的線程數到達閾值的時候,進行限流

是否集羣:是否爲集羣

流控的幾種 strategy:

  1. 直接:當 api 大達到限流條件時,直接限流

  2. 關聯:當關聯的資源到達閾值,就限流自己

  3. 鏈路:只記錄指定路上的流量,指定資源從入口資源進來的流量,如果達到閾值,就進行限流,api 級別的限流

直接失敗模式 限流

..... ,

由於公號字數限制,這裏 公號放不下, 此處省略  100 字 +

完整內容,請參見  《尼恩 Java 面試寶典》V86 版本,pdf 免費找尼恩獲取

Sentinel 關聯模式限流

..... ,

由於公號字數限制,這裏 公號放不下, 此處省略  100 字 +

完整內容,請參見  《尼恩 Java 面試寶典》V86 版本,pdf 免費找尼恩獲取

Warm up(預熱)模式 限流

..... ,

由於公號字數限制,這裏 公號放不下, 此處省略  100 字 +

完整內容,請參見  《尼恩 Java 面試寶典》V86 版本,pdf 免費找尼恩獲取

Sentinel 熔斷降級

什麼是 Sentinel 熔斷降級

..... ,

由於公號字數限制,這裏 公號放不下, 此處省略  100 字 +

完整內容,請參見  《尼恩 Java 面試寶典》V86 版本,pdf 免費找尼恩獲取

Sentinel 熔斷降級規則

..... ,

由於公號字數限制,這裏 公號放不下, 此處省略  100 字 +

完整內容,請參見  《尼恩 Java 面試寶典》V86 版本,pdf 免費找尼恩獲取

幾種降級策略

..... ,

由於公號字數限制,這裏 公號放不下, 此處省略  100 字 +

完整內容,請參見  《尼恩 Java 面試寶典》V86 版本,pdf 免費找尼恩獲取

熔斷降級代碼實現

..... ,

由於公號字數限制,這裏 公號放不下, 此處省略  100 字 +

完整內容,請參見  《尼恩 Java 面試寶典》V86 版本,pdf 免費找尼恩獲取

控制檯降級規則

..... ,

由於公號字數限制,這裏 公號放不下, 此處省略  100 字 +

Sentinel 與 Hystrix 對比

1、資源模型和執行模型上的對比

..... ,

由於公號字數限制,這裏 公號放不下, 此處省略  100 字 +

2、隔離設計上的對比

..... ,

由於公號字數限制,這裏 公號放不下, 此處省略  100 字 +

3、熔斷降級的對比

..... ,

由於公號字數限制,這裏 公號放不下, 此處省略  100 字 +

Sentinel 與 Hystrix 的不足

無論是 Sentinel 與 Hystrix, 都無法做到  分層分級細粒度  熔斷限流,

所以,對於 微信中的 億級 QPS 吞吐量規模過載場景,   沒法直接使用  Sentinel 與 Hystrix 進行 , 沒有辦法進行 細粒度、 高精準 熔斷保護(過載保護)  。

分層分級細粒度、高精準熔斷限流降級策略

微信後臺億級 QPS 吞吐量的過載場景

微信作爲當之無愧的國民級應用,系統複雜程度超乎想象:

微服務採用統一的 RPC 框架搭建一個個獨立的服務,服務之間互相調用,實現各種各樣的功能,這也是現代服務的基本架構。

微信的服務是分三層:接入服務、邏輯服務、基礎服務。

大多數服務屬於邏輯服務,接入服務如登錄、發消息、支付服務,每日請求量在 10 億 - 100 億之間,入口協議觸發對邏輯服務和基礎服務更多的請求,核心服務每秒要處理上億次的請求,qps> 1 億。

在大規模微服務場景下,過載會變得比較複雜。

如果是單體服務,一個事件只用一個請求。

但微服務下,一個事件可能要請求很多的服務,任何一個服務過載失敗,就會造成其他的請求都是無效的。

如下圖所示:

比如在一個轉賬服務下,需要查詢分別兩者的卡號,再查詢 A 時成功了,但查詢 B 失敗,對於查卡號這個事件就算失敗了,比如查詢成功率只有 50%,那對於查詢兩者卡號這個成功率只有 50% * 50% = 25% 了,一個事件調用的服務次數越多,那成功率就會越低。

微信後臺如何判斷過載

通常,判斷過載的方式很多,比如:

微信並沒有使用 以上的常用方式,而是使用一種特殊的方式:

微信爲啥不使用響應時間?

因爲響應時間是跟服務相關的,很多微服務是鏈式調用,響應時間是不可控的,也是無法標準化的,很難作爲一個統一的判斷依據。

爲微信爲啥也不使用 CPU 負載作爲判斷標準呢?

因爲 CPU 負載高不代表服務過載,一個服務請求處理及時,CPU 處於高位反而是比較良好的表現。

實際上 CPU 負載高,監控服務是會告警出來,但是並不會直接進入過載處理流程。

什麼是  請求在隊列中的平均等待時間  呢?

請求在隊列中的等待時間 就是從  請求到達,到開始處理的時間。   平均等待時間的計算範圍,以時間窗口(如 s)劃分時間範圍,或者以 一定數量的請求劃分範圍(如每 2000 個請求)。

以超時時間爲基礎,騰訊微服務通過計算每秒或每 2000 個請求的平均等待時間是否超過 20ms,判斷是否過載,這個 20ms 是根據微信後臺 5 年摸索出來的門檻值。默認的超時時間是 500ms,

採用平均等待時間還有一個好處是:

這個是獨立於服務的,可以應用於任何場景,而不用關聯於業務,可以直接在框架上進行改造。

微信後臺的限流降級策略(過載保護策略)

當平均等待時間大於 20ms 時,以一定的降速因子過濾調部分請求。 開始進行 限流降級 。

如果判斷平均等待時間小於 20ms,則以一定的速率提升通過率。 開始 進行流量的恢復。

一般採用快降慢升的策略,防止大的服務波動。

整個策略相當於一個負反饋電路。

分層分級細粒度、高精準熔斷限流降級策略

一旦檢測到服務過載,需要按照一定的策略對請求進行過濾。

那麼,有哪些進行流量過濾的策略呢?

對於鏈式調用的微服務場景,使用策略一進行隨機丟棄請求,最終會導致整體服務的成功率很低。

所以,使用分層分級高精準細粒度限流降級策略,請求是按照優先級進行控制的, 優先級低的請求會優先丟棄。

什麼是使用分層分級高精準細粒度限流降級策略?

具體來說:

1)業務分層

對於不同的業務場景優先的層級是不同的。

比如:登錄場景是最重要的業務,也是最爲核心的業務,如果不能登錄,一切都白費。

另外:支付消息也比普通消息優先級高,因爲用戶對金錢是更敏感的。

再比如說:普通消息,又比朋友圈消息優先級高。

所以在微信內是天然存在業務層級的。

每個請求,從業務維度來說,都會分配一個業務層級。

在微服務的鏈式調用下,後端的請求業務層級,從請求鏈路的前段進行繼承的。

比如我請求登錄,那麼後端的請求業務都是繼承登錄的業務層級。如檢查賬號密碼等一系列的後續請求都是繼承登錄優先級的,這就保證了業務層級的一致性。

用一個 hash 表維護重要性很高的 top N 的業務層級,每個後臺服務維護了業務層級的 hash 表。

當然,微信的業務太多,並非每個業務都記錄在 hash 表裏,不在 hash 表裏的業務就是 低層級業務。限流的時候, 首先被限制。

hash 表中的業務,都是高層級業務。限流的時候, 放在最後限制。

2)用戶分級

每個業務的請求量很大,整塊業務請求全部被限制, 那一定會造成負載的大幅波動。

所以不可能因爲負載高,丟棄或允許通過一整個業務的請求。

很明顯,只基於業務層級的控制是不夠的。

解決這個問題,可以引入用戶分級

實際上,很多網站的用戶天然是分級的,VIP 用戶的訪問,需要優先保證。

微信如何對用戶進行分級呢?

一個 10 億級用戶的 APP,從業務維度來說,用戶分級的方案,非常複雜。

除了從業務維度分級完成之後,按照二八定律, 普通人佔 80%, 這個依然是一個龐大的數字。

對於普通人來說,還需要繼續進一步細分,這時候,可以通過 hash 用戶唯一 ID,計算用戶優先級。

3)分層分級二維限流降級控制

引入了用戶優先級,那就和業務優先級組成了一個二維限流降級控制。

根據負載情況,決定這臺服務器的准入優先級 (B,U),二維限流降級控制具體爲:

兩個條件,滿足一個即可放行。

4)RPC 組件客戶端限流

爲了進一步減輕過載機器的壓力,能不能在 upstream 後端過載的情況下,不把請求發到 後端呢?

否則 後端還是要接受請求、解包、丟棄請求,白白浪費帶寬也加重了 後端的負載。

爲了實現這個能力,進行 RPC 組件客戶端限流:

微信整個負載控制的流程

微信整個負載控制的流程如圖所示:

當用戶從微信發起請求,請求被路由到接入層服務,分配統一的業務和用戶優先級,所有到的 upstream 上游子請求都繼承相同的優先級。根據業務邏輯調用 1 個或多個 upstream 上游服務。

當服務收到請求,首先根據自身服務准入優先級判斷請求是接受還是丟棄。

服務本身根據負載情況週期性的調整准入優先級。

當服務通過 RPC 客戶端需要再向 upstream 上游發起請求時,判斷本地記錄的 upstream 上游服務准入優先級。如果小於則丟棄,如果沒有記錄或優先級大於記錄則向 upstream 上游發起請求。

upstream 上游服務返回需要的信息,並且在信息中攜帶自身准入優先級。downstream 下游接受到返回後解析信息,並更新本地記錄的 upstream 服務准入優先級。

說在最後

熔斷,降級,防止雪崩,是面試的重點和高頻點

(1) 什麼是熔斷,降級?如何實現?

(2) 服務熔斷,解決災難性雪崩效應的有效利器

(3 )說一下限流、熔斷、高可用

等等等等.....

參照上文的答案,如果大家能對答如流,最終,讓面試官愛到 “不能自已、口水直流”。  offer, 也就來了。

作者介紹

本文 1 作: Andy,資深架構師,  《Java 高併發核心編程  加強版》作者之 1 。

本文 2 作: 尼恩,41 歲資深老架構師,  《Java 高併發核心編程  加強版  卷 1、卷 2、卷 3》創世作者, 著名博主 。 《K8S 學習聖經》《Docker 學習聖經》《Go 學習聖經》等 11 個 PDF 聖經的作者。 也是一個 架構轉化 導師, 已經指導了大量小夥伴成功 轉型架構師, 最高的年薪拿到近 100W。

參考文獻

[1] Overload Control for Scaling WeChat Microservices(http://www.52im.net/thread-3930-1-1.html%2316)

[2] 羅神解讀 “Overload Control for Scaling WeChat Microservices”(http://zhuanlan.zhihu.com/p/84415217)

[3] 2W 臺服務器、每秒數億請求,微信如何不 “失控”?(http://baijiahao.baidu.com/s%3Fid%3D1617827040372564403%26wfr%3Dspider%26for%3Dpc)

[4] DAGOR:微信微服務過載控制系統 (http://www.infoq.cn/article/bavev*b7gth123tlwydr)

[5] 月活 12.8 億的微信是如何防止崩潰的?(http://mp.weixin.qq.com/s/xmQCKVkqhhTKd1k6GmkZfA)

[6] 微信朋友圈千億訪問量背後的技術挑戰和實踐總結 (https://www.52im.net/thread-1569-1-1.html)

[7] QQ 18 年:解密 8 億月活的 QQ 後臺服務接口隔離技術 (https://www.52im.net/thread-1093-1-1.html)

[8] 微信後臺基於時間序的海量數據冷熱分級架構設計實踐 (https://www.52im.net/thread-895-1-1.html)

[9] 架構之道:3 個程序員成就微信朋友圈日均 10 億發佈量 (https://www.52im.net/thread-177-1-1.html)》

[10] 快速裂變:見證微信強大後臺架構從 0 到 1 的演進歷程(一)(https://www.52im.net/thread-168-1-1.html)

[11] 一份微信後臺技術架構的總結性筆記 (https://www.52im.net/thread-179-1-1.html)》

https://juejin.cn/post/6844904006259572749

https://cloud.tencent.com/developer/article/1815254

https://blog.csdn.net/qq_27184497/article/details/119993725

https://blog.csdn.net/sh210106sh/article/details/116495124

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