《微服務治理之熔斷降級Hystrix》一、基礎知識

 

0. Hystrix是什麼?

Hystrix的本意是指 豪豬 的動物,它身上長滿了很長的較硬的空心尖刺,當受到攻擊時,通過後退的方式使其尖刺刺入敵方的身體。作爲這種特徵的引申,Netflix公司在分佈式微服務架構的踐行下,將其保護服務的穩定性而設計的客戶端熔斷和斷路器的解決方案,稱之爲Hystrix

圖片來源:百度百科


Hystrix的設計目的是將應用中的遠程系統訪問服務調用第三方依賴包的調用入口,通過資源控制的方式隔離開,避免了在分佈式系統中失敗的級聯塌方式傳遞,提升系統的彈性和健壯性。

 

hystrix.png

Hystrix的現狀--官方社區已死

Hystrix 當前已經進入爲維護階段,Netflix 認爲Hystrix的定位和使命在功能上,當前已經完全滿足了既有的內部系統,所以後期不再有新的開發和新的特性出現。團隊由於精力的原因,在Github上,不再review issue,不再接受Merge request,也不再發布新的版本,版本定格在1.5.8

問題:官方社區已死,還有必要學習嗎?

Hystrix雖然官方社區不再維護,但是其客戶端熔斷保護,斷路器設計理念,有非常高的學習價值,爲我們在服務保護的設計上,提供了非常好的設計思路;除了官方不再維護之外,hystrix目前對於一般的分佈式服務調度,甚至本地服務保護上,完全可以勝任,在短期內可以正常使用。
文章末尾會介紹兩種替換方案,供參考


1. Hystrix模型基礎

  • 設計模式:命令模式(Command Pattern)
    命令模式 將客戶端對服務直接調用,封裝成一個待執行的請求,客戶端和請求被封裝爲一個對象,對於服務方而言,每個不同的請求就是不同的參數,從而讓我們可用不同的請求對客戶進行參數化;命令模式的最大特徵就是把客戶端和服務端直接關係,通過命令對象進行解耦,在執行上,可以對請求排隊或者記錄請求日誌,以及支持可撤銷的操作。
    【本文的主旨不是介紹命令模式,讀者請參考其他博文進行了解】。
  • 線程池和信號量隔離
    計算機系統中,線程作爲系統運行的基本單位,可以通過劃分指定的線程池資源的使用目的,對系統資源進行分離,具備資源限定的能力,進而保護系統;另外在Java中,Semaphore的信號量機制,也能提供資源競爭的隔離作用。

2. Hystrix工作原理

如下圖所示,Hystrix的工作流程上大概會有如下9個步驟,下文將詳細介紹每個流程:

Hystrix工作原理圖

 

2.1 創建HystrixCommand 或者HystrixObservableCommand

在使用Hystrix的過程中,會對依賴服務的調用請求封裝成命令對象,Hystrix 對 命令對象抽象了兩個抽象類:HystrixCommandHystrixObservableCommand
HystrixCommand 表示的命令對象會返回一個唯一返回值:

 

public class QueryOrderCommand extends HystrixCommand<Order> {
    private String orderId;
    public QueryOrderCommand(String orderId){
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("hystrix-order-group"))
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("hystrix-thread-order"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("hystrix-pay-order"))
                .andCommandPropertiesDefaults(HystrixCommandProperties.defaultSetter())
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.defaultSetter()
                        .withCoreSize(10)
                        .withQueueSizeRejectionThreshold(15)
                )
        );
        this.orderId = orderId;
    }
    @Override
    protected Order run() throws Exception {
        System.out.println("fetching order info via service call");
        return new Order();
    }
}

class Order{
    private String orderId;
    private String productId;
    private String status;
}

HystrixObservableCommand 表示的命令對象 會返回多個返回值:

2.2. 執行命令

Hystrix中共有4種方式執行命令,如下所示:

執行方式 說明 可用對象
execute() 阻塞式同步執行,返回依賴服務的單一返回結果(或者拋出異常) HystrixCommand
queue() 基於Future的異步方式執行,返回依賴服務的單一返回結果(或者拋出異常) HystrixCommand
observe() 基於Rxjava的Observable方式,返回通過Observable表示的依賴服務返回結果,代調用代碼先執行(Hot Obserable) HystrixObservableCommand
toObvsevable 基於Rxjava的Observable方式,返回通過Observable表示的依賴服務返回結果,執行代碼等到真正訂閱的時候纔會執行(cold observable) HystrixObservableCommand

這四種命令中,exeucte()queue()observe()的表示也是通過toObservable()實現的,其轉換關係如下圖所示:

HystrixCommand執行方式

 

 

K             value   = command.execute();
// 等價語句:
K             value = command.execute().queue().get();


Future<K>     fValue  = command.queue();
//等價語句:
Future<K>     fValue = command.toObservable().toBlocking().toFuture();


Observable<K> ohValue = command.observe();         //hot observable,立刻訂閱,命令立刻執行
//等價語句:
Observable<K> ohValue = command.toObservable().subscribe(subject);         

// 上述執行最終實現還是基於`toObservable()`
Observable<K> ocValue = command.toObservable();    //cold observable,延後訂閱,訂閱發生後,執行才真正執行

2.3. 返回結果是否被緩存?

如果當前命令對象配置了允許從結果緩存中取返回結果,並且在結果緩存中已經緩存了請求結果,則緩存的請求結果會立刻通過Observable的格式返回。具體Hystrix的緩存策略,請參考``

2.4. 斷路器是否打開?

如果第3步沒有緩存沒有命中,則判斷一下當前斷路器的斷路狀態是否打開。如果斷路器狀態爲打開狀態,則Hystrix將不會執行此Command命令,直接執行步驟8 調用Fallback;
如果斷路器狀態是關閉,則執行 步驟5 檢查是否有足夠的資源運行 Command命令

2.5. 資源(線程池/隊列/信號量)是否已滿?

如果當前要執行的Command命令 先關連的線程池 和隊列(或者信號量)資源已經滿了,Hystrix將不會運行 Command命令,直接執行 步驟8的Fallback降級處理;如果未滿,表示有剩餘的資源執行Command命令,則執行步驟6

2.6. 執行 HystrixObservableCommand.construct() 或者 HystrixCommand.run()

當經過步驟5 判斷,有足夠的資源執行Command命令時,本步驟將調用Command命令運行方法,基於不同類型的Command,有如下兩種兩種運行方式:

運行方式 說明
HystrixCommand.run() 返回一個處理結果或者拋出一個異常
HystrixObservableCommand.construct() 返回一個Observable表示的結果(可能多個),或者 基於onError的錯誤通知

如果run() 或者construct()方法 的真實執行時間超過了Command設置的超時時間閾值, 則當前則執行線程(或者是獨立的定時器線程)將會拋出TimeoutException。拋出超時異常TimeoutException,後,將執行步驟8的Fallback降級處理。即使run()或者construct()執行沒有被取消或中斷,最終能夠處理返回結果,但在降級處理邏輯中,將會拋棄run()construct()方法的返回結果,而返回Fallback降級處理結果。

注意事項
需要注意的是,Hystrix無法強制 將正在運行的線程停止掉--Hystrix能夠做的最好的方式就是在JVM中拋出一個InterruptedException。如果Hystrix包裝的工作不拋出中斷異常InterruptedException, 則在Hystrix線程池中的線程將會繼續執行,儘管調用的客戶端已經接收到了TimeoutException。這種方式會使Hystrix 的線程池處於飽和狀態。大部分的Java Http Client 開源庫並不會解析 InterruptedException。所以確認HTTP client 相關的連接和讀/寫相關的超時時間設置。
如果Command命令沒有拋出任何異常,並且有返回結果,則Hystrix將會在做完日誌記錄和統計之後會將結果返回。 如果是通過run()方式運行,則返回一個Obserable對象,包含一個唯一值,並且發送一個onCompleted通知;如果是通過consturct()方式運行 ,則返回一個Observable對象

2.7. 計算斷路器的健康狀況

Hystrix 會統計Command命令執行執行過程中的成功數失敗數拒絕數超時數,將這些信息記錄到斷路器(Circuit Breaker)中。斷路器將上述統計按照時間窗的形式記錄到一個定長數組中。斷路器根據時間窗內的統計數據去判定請求什麼時候可以被熔斷,熔斷後,在接下來一段恢復週期內,相同的請求過來後會直接被熔斷。當再次校驗,如果健康監測通過後,熔斷開關將會被關閉。

2.8. 獲取Fallback

當以下場景出現後,Hystrix將會嘗試觸發Fallback:

  • 步驟6 Command執行時拋出了任何異常;
  • 步驟4 斷路器已經被打開
  • 步驟5 執行命令的線程池、隊列或者信號量資源已滿
  • 命令執行的時間超過閾值

2.9. 返回成功結果

如果 Hystrix 命令對象執行成功,將會返回結果,或者以Observable形式包裝的結果。根據步驟2的command 調用方式,返回的Observable 會按照如下圖說是的轉換關係進行返回:

Hystrix獲取返回結果

 


3. 斷路器工作原理

image.png

  1. 斷路器時間窗內的請求數 是否超過了請求數斷路器生效閾值circuitBreaker.requestVolumeThreshold,如果超過了閾值,則將會觸發斷路,斷路狀態爲開啓
    例如,如果當前閾值設置的是20,則當時間窗內統計的請求數共計19個,即使19個全部失敗了,都不會觸發斷路器。
  2. 並且請求錯誤率超過了請求錯誤率閾值errorThresholdPercentage
  3. 如果兩個都滿足,則將斷路器由關閉遷移到開啓
  4. 如果斷路器開啓,則後續的所有相同請求將會被斷路掉;
  5. 直到過了沉睡時間窗sleepWindowInMilliseconds後,再發起請求時,允許其通過(此時的狀態爲半開起狀態)。如果請求失敗了,則保持斷路器狀態爲開啓狀態,並更新沉睡時間窗。如果請求成功了,則將斷路器狀態改爲關閉狀態;

核心的邏輯如下:

 

 @Override
                        public void onNext(HealthCounts hc) {
                            // check if we are past the statisticalWindowVolumeThreshold
                            if (hc.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
                                // we are not past the minimum volume threshold for the stat window,
                                // so no change to circuit status.
                                // if it was CLOSED, it stays CLOSED
                                // if it was half-open, we need to wait for a successful command execution
                                // if it was open, we need to wait for sleep window to elapse
                            } else {
                                if (hc.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
                                    //we are not past the minimum error threshold for the stat window,
                                    // so no change to circuit status.
                                    // if it was CLOSED, it stays CLOSED
                                    // if it was half-open, we need to wait for a successful command execution
                                    // if it was open, we need to wait for sleep window to elapse
                                } else {
                                    // our failure rate is too high, we need to set the state to OPEN
                                    if (status.compareAndSet(Status.CLOSED, Status.OPEN)) {
                                        circuitOpened.set(System.currentTimeMillis());
                                    }
                                }
                            }
                        }

3.1 斷路器相關配置:

key值 說明 默認值
circuitBreaker.enabled 是否開啓斷路器 true
circuitBreaker.requestVolumeThreshold 斷路器啓用請求數閾值 20
circuitBreaker.sleepWindowInMilliseconds 斷路器啓用後的睡眠時間窗 5000(ms)
circuitBreaker.errorThresholdPercentage 斷路器啓用失敗率閾值 50(%)
circuitBreaker.forceOpen 是否強制將斷路器設置成開啓狀態 false
circuitBreaker.forceClosed 是否強制將斷路器設置成關閉狀態 false

Key值的配置問題
默認配置:上述Key值之前要加上hystrix.command.default.前綴拼接
實例配置:上述Key值之前要加上hystrix.command.<command-key>. 前綴拼接

3.2 系統指標

Hystrix對系統指標的統計是基於時間窗模式的:

時間窗:最近的一個時間區間內,比如前一小時到現在,那麼時間窗的長度就是1小時
:桶是在特定的時間窗內,等分的指標收集的統計集合;比如時間窗的長度爲1小時,而桶的數量爲10,那麼每個桶在時間軸上依次排開,時間由遠及近,每個桶統計的時間分片爲 1h / 10 = 6 min 6分鐘。一個桶中,包含了成功數失敗數超時數拒絕數 四個指標。

在系統內,時間窗會隨着系統的運行逐漸向前移動,而時間窗的長度和桶的數量是固定不變的,那麼隨着時間的移動,會出現較久的過期的桶被移除出去,新的桶被添加進來,如下圖所示:

image.png

key值 說明 默認值
metrics.rollingStats.timeInMilliseconds 時間窗的長度 10000(ms)
metrics.rollingStats.numBuckets 桶的數量,需要保證timeInMilliseconds % numBuckets =0 10
metrics.rollingPercentile.enabled 是否統計運行延遲的佔比 true
metrics.rollingPercentile.timeInMilliseconds 運行延遲佔比統計的時間窗 60000(ms)
metrics.rollingPercentile.numBuckets 運行延遲佔比統計的桶數 6
metrics.rollingPercentile.bucketSize 百分比統計桶的容量,桶內最多保存的運行時間統計 100
metrics.healthSnapshot.intervalInMilliseconds 統計快照刷新間隔 500 (ms)

4. 資源隔離技術

  • 基於線程池的隔離
    如下圖所示,由於計算機系統的基本執行單位就是線程,線程具備獨立的執行能力,所以,爲了做到資源保護,需要對系統的線程池進行劃分,對於外部調用方User Request的請求,調用各個線程池的服務,各個線程池獨立完成調用,然後將結果返回調用方。在調用服務的過程中,如果服務提供方執行時間過長,則調用方可以直接以超時的方式直接返回,快速失敗。

    image.png


    線程池隔離的幾點好處
  1. 使用超時返回的機制,避免同步調用服務時,調用時間過長,無法釋放,導致資源耗盡的情況
  2. 服務方可以控制請求數量,請求過多,可以直接拒絕,達到快速失敗的目的;
  3. 請求排隊,線程池可以維護執行隊列,將請求壓到隊列中處理

舉個例子,如下代碼段,模擬了同步調用服務的過程:

 

        //服務提供方,執行服務的時候模擬2分鐘的耗時
        Callable<String> callableService  = ()->{
            long start = System.currentTimeMillis();
            while(System.currentTimeMillis()-start> 1000 * 60 *2){
               //模擬服務執行時間過長的情況
            }
            return "OK";
        };

        //模擬10個客戶端調用服務
        ExecutorService clients = Executors.newFixedThreadPool(10);
        //模擬給10個客戶端提交處理請求
        for (int i = 0; i < 20; i++) {
            clients.execute(()->{
                //同步調用
                try {
                    String result = callableService.call();
                    System.out.println("當前客戶端:"+Thread.currentThread().getName()+"調用服務完成,得到結果:"+result);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }

在此環節中,客戶端 clients必須等待服務方返回結果之後,才能接收新的請求。如果用吞吐量來衡量系統的話,會發現系統的處理能力比較低。爲了提高相應時間,可以藉助線程池的方式,設置超時時間,這樣的話,客戶端就不需要必須等待服務方返回,如果時間過長,可以提前返回,改造後的代碼如下所示:

 

 //服務提供方,執行服務的時候模擬2分鐘的耗時
        Callable<String> callableService  = ()->{
            long start = System.currentTimeMillis();
            while(System.currentTimeMillis()-start> 1000 * 60 *2){
               //模擬服務執行時間過長的情況
            }
            return "OK";
        };

        //創建線程池作爲服務方
        ExecutorService executorService = Executors.newFixedThreadPool(30);


        //模擬10個客戶端調用服務
        ExecutorService clients = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            clients.execute(()->{
                //同步調用
                    //將請求提交給線程池執行,Callable 和 Runnable在某種意義上,也是Command對象
                    Future<String> future = executorService.submit(callableService::call);
                    //在指定的時間內獲取結果,如果超時,調用方可以直接返回
                    try {
                        String result = future.get(1000, TimeUnit.SECONDS);
                        //客戶端等待時間之後,快速返回
                        System.out.println("當前客戶端:"+Thread.currentThread().getName()+"調用服務完成,得到結果:"+result);
                    }catch (TimeoutException timeoutException){
                        System.out.println("服務調用超時,返回處理");
                    } catch (InterruptedException e) {
                        
                    } catch (ExecutionException e) {
                    }
            });
        }

如果我們將服務方的線程池設置爲:

 

ThreadPoolExecutor executorService = new ThreadPoolExecutor(10,1000,TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.DiscardPolicy() // 提交請求過多時,可以丟棄請求,避免死等阻塞的情況。
)

線程池隔離模式的弊端

線程池隔離模式,會根據服務劃分出獨立的線程池,系統資源的線程併發數是有限的,當線程數過多,系統話費大量的CPU時間來做線程上下文切換的無用操作,反而降低系統性能;如果線程池隔離的過多,會導致真正用於接收用戶請求的線程就相應地減少,系統吞吐量反而下降;
在實踐上,應當對像遠程方法調用,網絡資源請求這種服務時間不太可控的場景下使用線程池隔離模式處理
如下圖所示,是線程池隔離模式的三種場景:

image.png

  • 基於信號量的隔離
    由於基於線程池隔離的模式佔用系統線程池資源,Hystrix還提供了另外一個隔離技術:基於信號量的隔離。
    基於信號量的隔離方式非常地簡單,其核心就是使用共用變量semaphore進行原子操作,控制線程的併發量,當併發量達到一定量級時,服務禁止調用。如下圖所示:信號量本身不會消耗多餘的線程資源,所以就非常輕量。

    image.png


    基於信號量隔離的利弊

利:基於信號量的隔離,利用JVM的原子性CAS操作,避免了資源鎖的競爭,省去了線程池開銷,效率非常高;
弊:本質上基於信號量的隔離是同步行爲,所以無法做到超時熔斷,所以服務方自身要控制住執行時間,避免超時。
應用場景:業務服務上,有併發上限限制時,可以考慮此方式
Alibaba Sentinel開源框架,就是基於信號量的熔斷和斷路器框架。


5. Spring Cloud 下 Hystrix使用要注意的問題

  • Hystrix配置無法動態調節生效。Hystrix框架本身是使用的Archaius框架完成的配置加載和刷新,但是集成自 Spring Cloud下,無法有效地根據實時監控結果,動態調整熔斷和系統參數
  • 線程池和Command之間的配置比較複雜,在Spring Cloud在做feigin-hystrix集成的時候,還有些BUG,對command的默認配置沒有處理好,導致所有command佔用公共的command線程池,沒有細粒度控制,還需要做框架適配調整

 

public interface SetterFactory {

  /**
   * Returns a hystrix setter appropriate for the given target and method
   */
  HystrixCommand.Setter create(Target<?> target, Method method);

  /**
   * Default behavior is to derive the group key from {@link Target#name()} and the command key from
   * {@link Feign#configKey(Class, Method)}.
   */
  final class Default implements SetterFactory {

    @Override
    public HystrixCommand.Setter create(Target<?> target, Method method) {
      String groupKey = target.name();
      String commandKey = Feign.configKey(target.type(), method);
      return HystrixCommand.Setter
          .withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
          .andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));
          //沒有處理好default配置項的加載
    }
  }
}

6. Hystrix官方已死,還有什麼替代方案嗎?

  • resilience4j
    Hystrix雖然官方宣佈不再維護,其推薦另外一個框架:resilience4j, 這個框架是是爲Java 8 和 函數式編程設計的一個輕量級的容錯框架,該框架充分利用函數式編程的概念,爲函數式接口lamda表達式方法引用高階函數進行包裝,(本質上是裝飾者模式的概念),通過包裝實現斷路限流重試艙壁功能。
    這個框架整體而言比較輕量,沒有控制檯,不太好做系統級監控;
  • Alibaba Sentinel
    Sentinel 是 阿里巴巴開源的輕量級的流量控制、熔斷降級 Java 庫,該庫的核心是使用的是信號量隔離的方式做流量控制和熔斷,其優點是其集成性和易用性,幾乎能和當前主流的Spring Cloud, dubbo ,grpc ,nacos, zookeeper做集成,如下圖所示:

    sentinel-features-overview-en.png


    Sentinel的目標生態圈:

    sentinel-opensource-eco-landscape-en.png


    sentinel 一個強大的功能,就是它有一個流控管理控制檯,你可以實時地監控每個服務的流控情況,並且可以實時編輯各種流控、熔斷規則,有效地保證了服務保護的及時性。下圖是內部試用的sentinel控制檯:

    image.png


    另外,sentinel還可以和 ctrip apollo 分佈式配置系統進行集成,將流控規降級等各種規則先配置在apollo中,然後服務啓動自動加載流控規則。

本文不是介紹sentinel的重點,關於sentinel的設計原理和使用方式,將另起博文介紹,有興趣的同學可以先關注下我。


[題外話]

alibaba 近期的開源支持力度比較大,感覺應該是爲了增加阿里巴巴在雲原生的大生態,藉助spring-cloud-alibaba,集合nacos,sentinel,dubbo,seatarocketmq,提高其影響力。在投入度上來看,sentinel的社區活躍度較好,並且緊跟spring-cloud-alibaba, 如果使用的技術體系偏 阿里系的話,這是不錯的選擇。

參考資料

  1. https://github.com/Netflix/Hystrix
  2. https://github.com/resilience4j/resilience4j
  3. https://github.com/ctripcorp/apollo
  4. Comparison to Netflix Hystrix
  5. Alibaba Sentinel
  6. spring-cloud-alibaba
  7. 百度詞條-豪豬解釋



作者:亦山札記
鏈接:https://www.jianshu.com/p/684b04b6c454
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章