Spring Cloud 之 Hystrix.

JMCui發表於2019-07-15

一、概述

 在微服務架構中,我們將系統拆分成了很多服務單元,各單元的應用間通過服務註冊與訂閱的方式互相依賴。由於每個單元都在不同的程式中執行,依賴通過遠端呼叫的方式執行,這樣就有可能因為網路原因或是依賴服務自身間題出現呼叫故障或延遲,而這些問題會直接導致呼叫方的對外服務也出現延遲,若此時呼叫方的請求不斷增加,最後就會因等待出現故障的依賴方響應形成任務積壓,最終導致自身服務的癱瘓。

 所以我們引入了斷路器,類似於物理上的電路,當電流過載時,就斷開電路,就是我們俗稱的“跳閘”。同理,服務間的呼叫也是如此,當不斷的出現服務延遲、故障等影響到系統效能的呼叫,就把這個服務呼叫切斷!

 Spring Cloud Hystrix 實現了斷路器、執行緒隔離等一系列服務保護功能。它也是基於 Netflix 的開源框架 Hystrix 實現的,該框架的目標在於通過控制那些訪問遠端系統、服務和第三方庫的節點,從而對延遲和故障提供更強大的容錯能力。Hystrix 具備服務降級、服務熔斷、執行緒和訊號隔離、請求快取、請求合併以及服務監控等強大功能。

二、Hystrix 工作流程

Spring Cloud 之 Hystrix.

  1. 建立 HystrixCommand(用在依賴的服務返回單個操作結果的時候) 或 HystrixObserableCommand(用在依賴的服務返回多個操作結果的時候) 物件。
  2. 命令執行。其中 HystrixComand 實現了下面前兩種執行方式;而 HystrixObservableCommand 實現了後兩種執行方式:

    • execute():同步執行,從依賴的服務返回一個單一的結果物件, 或是在發生錯誤的時候丟擲異常。
    • queue():非同步執行, 直接返回 一個Future物件, 其中包含了服務執行結束時要返回的單一結果物件。
    • observe():返回 Observable 物件,它代表了操作的多個結果,它是一個 Hot Obserable(不論 "事件源" 是否有 "訂閱者",都會在建立後對事件進行釋出,所以對於 Hot Observable 的每一個 "訂閱者" 都有可能是從 "事件源" 的中途開始的,並可能只是看到了整個操作的區域性過程)。
    • toObservable(): 同樣會返回 Observable 物件,也代表了操作的多個結果,但它返回的是一個Cold Observable(沒有 "訂閱者" 的時候並不會釋出事件,而是進行等待,直到有 "訂閱者" 之後才釋出事件,所以對於 Cold Observable 的訂閱者,它可以保證從一開始看到整個操作的全部過程)。
  3. 若當前命令的請求快取功能是被啟用的, 並且該命令快取命中, 那麼快取的結果會立即以 Observable 物件的形式 返回。
  4. 檢查斷路器是否為開啟狀態。如果斷路器是開啟的,那麼Hystrix不會執行命令,而是轉接到 fallback 處理邏輯(第 8 步);如果斷路器是關閉的,檢查是否有可用資源來執行命令(第 5 步)。
  5. 執行緒池/請求佇列/訊號量是否佔滿。如果命令依賴服務的專有執行緒池和請求佇列,或者訊號量(不使用執行緒池的時候)已經被佔滿, 那麼 Hystrix 也不會執行命令, 而是轉接到 fallback 處理邏輯(第8步)。
  6. Hystrix 會根據我們編寫的方法來決定採取什麼樣的方式去請求依賴服務。

    • HystrixCommand.run() :返回一個單一的結果,或者丟擲異常。
    • HystrixObservableCommand.construct(): 返回一個Observable 物件來發射多個結果,或通過 onError 傳送錯誤通知。
  7. Hystrix會將 "成功"、"失敗"、"拒絕"、"超時" 等資訊報告給斷路器, 而斷路器會維護一組計數器來統計這些資料。斷路器會使用這些統計資料來決定是否要將斷路器開啟,來對某個依賴服務的請求進行 "熔斷/短路"。
  8. 當命令執行失敗的時候, Hystrix 會進入 fallback 嘗試回退處理, 我們通常也稱該操作為 "服務降級"。而能夠引起服務降級處理的情況有下面幾種:

    • 第4步: 當前命令處於"熔斷/短路"狀態,斷路器是開啟的時候。
    • 第5步: 當前命令的執行緒池、 請求佇列或 者訊號量被佔滿的時候。
    • 第6步:HystrixObservableCommand.construct() 或 HystrixCommand.run() 丟擲異常的時候。
  9. 當Hystrix命令執行成功之後, 它會將處理結果直接返回或是以Observable 的形式返回。

tips:如果我們沒有為命令實現降級邏輯或者在降級處理邏輯中丟擲了異常, Hystrix 依然會返回一個 Observable 物件, 但是它不會發射任何結果資料, 而是通過 onError 方法通知命令立即中斷請求,並通過onError()方法將引起命令失敗的異常傳送給呼叫者。

三、Hystrix 熔斷保護機制

Spring Cloud 之 Hystrix.

  1. 假設大量的請求數量超過了 HystrixCommandProperties.circuitBreakerRequestVolumeThreshold() 的閾值,熔斷器將會從關閉狀態變成開啟狀態;
  2. 假設依賴呼叫失敗的百分比超過了 HystrixCommandProperties.circuitBreakerErrorThresholdPercentage() 的閾值,熔斷器將會從關閉狀態變成開啟狀態;
  3. 在熔斷器處於開啟狀態的期間,所有對這個依賴進行的呼叫都會短路,即不進行真正的依賴呼叫,返回失敗;
  4. 在等待(冷卻)的時間超過 HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds() 的值後,熔斷器將處於半開的狀態,將允許單個請求去呼叫依賴,如果這次的依賴呼叫還是失敗,熔斷器狀態將再次變成開啟,這個開啟狀態持續時間是
    HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds() 配置的值;如果這次的依賴呼叫成功,熔斷器狀態將變成關閉,後續依賴呼叫可正常執行。

四、Hystrix 依賴隔離機制

執行緒池隔離

Hystrix 則使用“艙壁模式”實現執行緒池的隔離,它會為每一個依賴服務建立 一個獨立的執行緒池,這樣就算某個依賴服務出現延遲過高的情況,也只是對該依賴服務的呼叫產生影響,而不會拖慢其他的依賴服務。缺點是涉及到執行緒切換的效能損耗,但是官方給出的結果是效能損耗是可以接受的。

訊號量隔離

訊號量隔離可實現對依賴呼叫最高併發請求數的限制,每次依賴呼叫都會先判斷訊號量是否達到閾值,如果達到極限值則拒絕呼叫。訊號量的開銷遠比執行緒池的開銷小,但是它不能設定超時和實現非同步訪問。所以,只有在依賴服務是足夠可靠的情況下才使用訊號量 。以下是兩種配置訊號量隔離的方式:

  • Setter 方式
HystrixCommand.Setter setter = HystrixCommand.Setter
        .withGroupKey(HystrixCommandGroupKey.Factory.asKey("strGroupCommand"))
        .andCommandKey(HystrixCommandKey.Factory.asKey("strCommand"))
        .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("strThreadPool"));
// 配置訊號量隔離
HystrixCommandProperties.Setter commandPropertiesSetter = HystrixCommandProperties.Setter().withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE);
setter.andCommandPropertiesDefaults(commandPropertiesSetter);
  • 註解方式
    @HystrixCommand(fallbackMethod = "fallbackMethod", 
            commandProperties = {
                    // 設定隔離策略,THREAD 表示執行緒池 SEMAPHORE:訊號池隔離
                    @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
                    // 當隔離策略選擇訊號池隔離的時候,用來設定訊號池的大小(最大併發數)
                    @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10"),
            }
    )

五、hystrix 實戰

SpringBoot 版本號:2.1.6.RELEASE
SpringCloud 版本號:Greenwich.RELEASE

1. pom.xml

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

2. 在 SpringBoot 的啟動類上引入 @EnableCircuitBreaker 註解,開啟斷路器功能。

3. 上面 hystrix 工作原理中提到斷路器有四種執行方式:

  • execute() - 同步執行
    @HystrixCommand(fallbackMethod = "fallbackMethod")
    public String strConsumer() {
        ResponseEntity<String> result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class);
        return result.getBody();
    }

fallbackMethod —— 回撥方法,在服務呼叫異常、斷路器開啟、執行緒池/請求佇列/訊號量佔滿時會走回撥邏輯。必須和服務方法定義在同一個類中,對修飾符沒有特定的要求,定義為 private、 protected、 public 均可。

  • queue() - 非同步執行
    @HystrixCommand(fallbackMethod = "fallbackMethod", ignoreExceptions = {IllegalAccessException.class})
    public Future<String> asyncStrConsumer() {

        Future<String> asyncResult = new AsyncResult<String>() {
            @Override
            public String invoke() {
                ResponseEntity<String> result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class);
                return result.getBody();
            }
        };
        return asyncResult;
    }

ignoreExceptions 表示丟擲該異常時不走降級回撥邏輯,忽略此異常。

  • observe () 執行方式
    @HystrixCommand(observableExecutionMode = ObservableExecutionMode.EAGER)
    protected Observable<String> construct() {
        ResponseEntity<String> result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class);
        return Observable.just(result.getBody());
    }
  • toObservable() 執行方式
    @HystrixCommand(observableExecutionMode = ObservableExecutionMode.LAZY)
    protected Observable<String> construct() {
        ResponseEntity<String> result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class);
        return Observable.just(result.getBody());
    }

4. 命令名稱、分組以及執行緒池劃分

  • Hystrix 會根據組來組織和統計命令的告警、儀表盤等資訊。
  • 預設情況下,Hystrix 會讓相同組名的命令使用同一個執行緒池。
  • 通常情況下,儘量通過 HystrixThreadPoolKey 的方式來指定執行緒池的劃分,而不是通過組名的預設方式實現劃分,因為多個不同的命令可能從業務邏輯上來看屬於同一個組,但是往往從實現本身上需要跟其他命令進行隔離。
    @HystrixCommand(fallbackMethod = "fallbackMethod", groupKey = "strGroupCommand", commandKey = "strCommand", threadPoolKey = "strThreadPool")
    public String strConsumer(@CacheKey Long id) {
        ResponseEntity<String> result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class);
        return result.getBody();
    }

groupKey 預設是類名,commandKey 預設是方法名 ,threadPoolKey 預設和 groupKey 一致。

5. 請求快取和請求合併

5.1 請求快取

快取的作用和好處,真的是無需多言了。請求快取,顧名思義,就是將對同一個 key 的請求結果,快取下來。那麼下次對這個 key 的請求,資料就直接在快取中返回,減少響應時間;

在 Hystrix 中使用快取,主要是三個註解:@CacheResult、@CacheKey、@CacheRemove

  • @CacheResult:用來標記請求結果應該被快取,必須與 @HystrixCommand 一起使用。
  • @CacheKey:用來修飾方法引數,表示快取的 key 名,優先順序高於 @CacheResult 設定的 key 名。
  • @CacheRemove:當資料更新的時候,為了資料的一致性,我們需要使快取失效。@CacheRemove 就是用來標記請求結果的快取失效(commandKey 是必填引數,表示要失效的快取 key)。
    @CacheResult(cacheKeyMethod = "getCacheKey")
    @HystrixCommand(fallbackMethod = "fallbackMethod")
    public String strConsumer(@CacheKey Long id) {
        ResponseEntity<String> result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class);
        return result.getBody();
    }
5.2 請求合併

 微服務架構中的依賴通常通過遠端呼叫實現,而遠端呼叫中最常見的問題就是通訊消耗與連線數佔用。在高併發的情況之下,因通訊次數的增加,總的通訊時間消耗將會變得不那麼理想。同時,因為依賴服務的執行緒池資源有限,將出現排隊等待與響應延遲的清況。為了優化這兩個問題,Hystrix 提供了 HystrixCollapser 來實現請求的合併,以減少通訊消耗和執行緒數的佔用。

@RestController
public class UserConsumer {

    @Autowired
    private UserService userService;

    /**
     * 通過 id 獲取使用者介面
     *
     * @param id
     * @return
     */
    @HystrixCollapser(batchMethod = "getByIds", collapserProperties = {
            // 10ms 內的請求合併為一次批量請求
            @HystrixProperty(name = "timerDelayInMilliseconds", value = "10"),
            // 批處理過程中是否開啟快取
            @HystrixProperty(name = "requestCache.enabled", value = "10"),
    })
    public String getById(Long id) {
        return userService.getUserById(id);
    }

    /**
     * 通過 ids 批量獲取使用者資訊介面
     *
     * @param ids id 集合
     * @return
     */
    @HystrixCommand
    public Set<String> getByIds(List<Long> ids) {
        return userService.getUserByIds(ids);
    }
}

雖然通過請求合併可以減少請求的數量以緩解依賴服務執行緒池的資源,但是在使用的時候也需要注意它所帶來的額外開銷:用於請求合併的延遲時間窗會使得依賴服務的請求延遲增高。

是否開啟快取合併,我們一般考慮下面兩個因素:

  • 如果依賴服務的請求命令本身是一個高延遲的命令,那麼可以使用請求合併器,因為高延遲,時間窗的時間消耗顯得微不足道了。
  • 如果一個時間窗內只有1-2個請求,那麼這樣的依賴服務不適合使用請求合併器。這種情況不但不能提升系統效能,反而會成為系統瓶頸;相反,如果一個時間窗內具有很高的併發量,並且服務提供方也實現了批量處理介面,那麼使用請求合併器可以有效減少網路連線數量並極大提升系統吞吐量。

6. HystrixCommand 屬性介紹

    @HystrixCommand(fallbackMethod = "fallbackMethod", groupKey = "strGroupCommand", commandKey = "strCommand", threadPoolKey = "strThreadPool",
            commandProperties = {
                    // 設定隔離策略,THREAD 表示執行緒池 SEMAPHORE:訊號池隔離
                    @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
                    // 當隔離策略選擇訊號池隔離的時候,用來設定訊號池的大小(最大併發數)
                    @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10"),
                    // 配置命令執行的超時時間
                    @HystrixProperty(name = "execution.isolation.thread.timeoutinMilliseconds", value = "10"),
                    // 是否啟用超時時間
                    @HystrixProperty(name = "execution.timeout.enabled", value = "true"),
                    // 執行超時的時候是否中斷
                    @HystrixProperty(name = "execution.isolation.thread.interruptOnTimeout", value = "true"),
                    // 執行被取消的時候是否中斷
                    @HystrixProperty(name = "execution.isolation.thread.interruptOnCancel", value = "true"),

                    // 允許回撥方法執行的最大併發數
                    @HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "10"),
                    // 服務降級是否啟用,是否執行回撥函式
                    @HystrixProperty(name = "fallback.enabled", value = "true"),

                    // 是否啟用斷路器
                    @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
                    // 該屬性用來設定在滾動時間窗中,斷路器熔斷的最小請求數。例如,預設該值為 20 的時候,如果滾動時間窗(預設10秒)內僅收到了19個請求, 即使這19個請求都失敗了,斷路器也不會開啟。
                    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
                    // 該屬性用來設定在滾動時間窗中,表示在滾動時間窗中,在請求數量超過 circuitBreaker.requestVolumeThreshold 的情況下,如果錯誤請求數的百分比超過50, 就把斷路器設定為 "開啟" 狀態,否則就設定為 "關閉" 狀態。
                    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
                    // 該屬性用來設定當斷路器開啟之後的休眠時間窗。 休眠時間窗結束之後,會將斷路器置為 "半開" 狀態,嘗試熔斷的請求命令,如果依然失敗就將斷路器繼續設定為 "開啟" 狀態,如果成功就設定為 "關閉" 狀態。
                    @HystrixProperty(name = "circuitBreaker.sleepWindowinMilliseconds", value = "5000"),
                    // 斷路器強制開啟
                    @HystrixProperty(name = "circuitBreaker.forceOpen", value = "false"),
                    // 斷路器強制關閉
                    @HystrixProperty(name = "circuitBreaker.forceClosed", value = "false"),

                    // 滾動時間窗設定,該時間用於斷路器判斷健康度時需要收集資訊的持續時間
                    @HystrixProperty(name = "metrics.rollingStats.timeinMilliseconds", value = "10000"),
                    // 該屬性用來設定滾動時間窗統計指標資訊時劃分"桶"的數量,斷路器在收集指標資訊的時候會根據設定的時間窗長度拆分成多個 "桶" 來累計各度量值,每個"桶"記錄了一段時間內的採集指標。
                    // 比如 10 秒內拆分成 10 個"桶"收集這樣,所以 timeinMilliseconds 必須能被 numBuckets 整除。否則會拋異常
                    @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"),
                    // 該屬性用來設定對命令執行的延遲是否使用百分位數來跟蹤和計算。如果設定為 false, 那麼所有的概要統計都將返回 -1。
                    @HystrixProperty(name = "metrics.rollingPercentile.enabled", value = "false"),
                    // 該屬性用來設定百分位統計的滾動視窗的持續時間,單位為毫秒。
                    @HystrixProperty(name = "metrics.rollingPercentile.timeInMilliseconds", value = "60000"),
                    // 該屬性用來設定百分位統計滾動視窗中使用 “ 桶 ”的數量。
                    @HystrixProperty(name = "metrics.rollingPercentile.numBuckets", value = "60000"),
                    // 該屬性用來設定在執行過程中每個 “桶” 中保留的最大執行次數。如果在滾動時間窗內發生超過該設定值的執行次數,
                    // 就從最初的位置開始重寫。例如,將該值設定為100, 滾動視窗為10秒,若在10秒內一個 “桶 ”中發生了500次執行,
                    // 那麼該 “桶” 中只保留 最後的100次執行的統計。另外,增加該值的大小將會增加記憶體量的消耗,並增加排序百分位數所需的計算時間。
                    @HystrixProperty(name = "metrics.rollingPercentile.bucketSize", value = "100"),
                    // 該屬性用來設定採集影響斷路器狀態的健康快照(請求的成功、 錯誤百分比)的間隔等待時間。
                    @HystrixProperty(name = "metrics.healthSnapshot.intervalinMilliseconds", value = "500"),

                    // 是否開啟請求快取
                    @HystrixProperty(name = "requestCache.enabled", value = "true"),
                    // HystrixCommand的執行和事件是否列印日誌到 HystrixRequestLog 中
                    @HystrixProperty(name = "requestLog.enabled", value = "true"),

            },
            threadPoolProperties = {
                    // 該引數用來設定執行命令執行緒池的核心執行緒數,該值也就是命令執行的最大併發量
                    @HystrixProperty(name = "coreSize", value = "10"),
                    // 該引數用來設定執行緒池的最大佇列大小。當設定為 -1 時,執行緒池將使用 SynchronousQueue 實現的佇列,否則將使用 LinkedBlockingQueue 實現的佇列。
                    @HystrixProperty(name = "maxQueueSize", value = "-1"),
                    // 該引數用來為佇列設定拒絕閾值。 通過該引數, 即使佇列沒有達到最大值也能拒絕請求。
                    // 該引數主要是對 LinkedBlockingQueue 佇列的補充,因為 LinkedBlockingQueue 佇列不能動態修改它的物件大小,而通過該屬性就可以調整拒絕請求的佇列大小了。
                    @HystrixProperty(name = "queueSizeRejectionThreshold", value = "5"),
            }
    )
    public String strConsumer() {
        ResponseEntity<String> result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class);
        return result.getBody();
    }

演示原始碼 :https://github.com/JMCuixy/spring-cloud-demo

內容參考:《Spring Cloud 微服務實戰》

相關文章