防雪崩利器:熔斷器 Hystrix 的原理與使用

前言

分佈式系統中經常會出現某個基礎服務不可用造成整個系統不可用的情況, 這種現象被稱爲服務雪崩效應. 爲了應對服務雪崩, 一種常見的做法是手動服務降級. 而Hystrix的出現,給我們提供了另一種選擇.

服務雪崩效應的定義

服務雪崩效應是一種因 服務提供者 的不可用導致 服務調用者 的不可用,並將不可用 逐漸放大 的過程.如果所示:

上圖中, A爲服務提供者, B爲A的服務調用者, C和D是B的服務調用者. 當A的不可用,引起B的不可用,並將不可用逐漸放大C和D時, 服務雪崩就形成了.

服務雪崩效應形成的原因

我把服務雪崩的參與者簡化爲 服務提供者 和 服務調用者, 並將服務雪崩產生的過程分爲以下三個階段來分析形成的原因:

  1. 服務提供者不可用

  2. 重試加大流量

  3. 服務調用者不可用

服務雪崩的每個階段都可能由不同的原因造成, 比如造成 服務不可用 的原因有:

  • 硬件故障

  • 程序Bug

  • 緩存擊穿

  • 用戶大量請求

硬件故障可能爲硬件損壞造成的服務器主機宕機, 網絡硬件故障造成的服務提供者的不可訪問. 
緩存擊穿一般發生在緩存應用重啓, 所有緩存被清空時,以及短時間內大量緩存失效時. 大量的緩存不命中, 使請求直擊後端,造成服務提供者超負荷運行,引起服務不可用. 
在秒殺和大促開始前,如果準備不充分,用戶發起大量請求也會造成服務提供者的不可用.

而形成 重試加大流量 的原因有:

  • 用戶重試

  • 代碼邏輯重試

在服務提供者不可用後, 用戶由於忍受不了界面上長時間的等待,而不斷刷新頁面甚至提交表單.
服務調用端的會存在大量服務異常後的重試邏輯. 
這些重試都會進一步加大請求流量.

最後, 服務調用者不可用 產生的主要原因是:

  • 同步等待造成的資源耗盡

當服務調用者使用 同步調用 時, 會產生大量的等待線程佔用系統資源. 一旦線程資源被耗盡,服務調用者提供的服務也將處於不可用狀態, 於是服務雪崩效應產生了.

服務雪崩的應對策略

針對造成服務雪崩的不同原因, 可以使用不同的應對策略:

  1. 流量控制

  2. 改進緩存模式

  3. 服務自動擴容

  4. 服務調用者降級服務

流量控制 的具體措施包括:

  • 網關限流

  • 用戶交互限流

  • 關閉重試

因爲Nginx的高性能, 目前一線互聯網公司大量採用Nginx+Lua的網關進行流量控制, 由此而來的OpenResty也越來越熱門.

用戶交互限流的具體措施有: 1. 採用加載動畫,提高用戶的忍耐等待時間. 2. 提交按鈕添加強制等待時間機制.

改進緩存模式 的措施包括:

  • 緩存預加載

  • 同步改爲異步刷新

服務自動擴容 的措施主要有:

  • AWS的auto scaling

服務調用者降級服務 的措施包括:

  • 資源隔離

  • 對依賴服務進行分類

  • 不可用服務的調用快速失敗

資源隔離主要是對調用服務的線程池進行隔離.

我們根據具體業務,將依賴服務分爲: 強依賴和若依賴. 強依賴服務不可用會導致當前業務中止,而弱依賴服務的不可用不會導致當前業務的中止.

不可用服務的調用快速失敗一般通過 超時機制熔斷器 和熔斷後的 降級方法 來實現.

使用Hystrix預防服務雪崩

Hystrix [hɪst'rɪks]的中文含義是豪豬, 因其背上長滿了刺,而擁有自我保護能力. Netflix的 Hystrix 是一個幫助解決分佈式系統交互時超時處理和容錯的類庫, 它同樣擁有保護系統的能力.

Hystrix的設計原則包括:

  • 資源隔離

  • 熔斷器

  • 命令模式

資源隔離

貨船爲了進行防止漏水和火災的擴散,會將貨倉分隔爲多個, 如下圖所示:

這種資源隔離減少風險的方式被稱爲:Bulkheads(艙壁隔離模式). Hystrix將同樣的模式運用到了服務調用者上.

在一個高度服務化的系統中,我們實現的一個業務邏輯通常會依賴多個服務,比如: 商品詳情展示服務會依賴商品服務, 價格服務, 商品評論服務. 如圖所示:

調用三個依賴服務會共享商品詳情服務的線程池. 如果其中的商品評論服務不可用, 就會出現線程池裏所有線程都因等待響應而被阻塞, 從而造成服務雪崩. 如圖所示:

Hystrix通過將每個依賴服務分配獨立的線程池進行資源隔離, 從而避免服務雪崩. 如下圖所示, 當商品評論服務不可用時, 即使商品服務獨立分配的20個線程全部處於同步等待狀態,也不會影響其他依賴服務的調用.

熔斷器模式

熔斷器模式定義了熔斷器開關相互轉換的邏輯:

服務的健康狀況 = 請求失敗數 / 請求總數. 熔斷器開關由關閉到打開的狀態轉換是通過當前服務健康狀況和設定閾值比較決定的.

  1. 當熔斷器開關關閉時, 請求被允許通過熔斷器. 如果當前健康狀況高於設定閾值, 開關繼續保持關閉. 如果當前健康狀況低於設定閾值, 開關則切換爲打開狀態.

  2. 當熔斷器開關打開時, 請求被禁止通過.

  3. 當熔斷器開關處於打開狀態, 經過一段時間後, 熔斷器會自動進入半開狀態, 這時熔斷器只允許一個請求通過. 當該請求調用成功時, 熔斷器恢復到關閉狀態. 若該請求失敗, 熔斷器繼續保持打開狀態, 接下來的請求被禁止通過.

熔斷器的開關能保證服務調用者在調用異常服務時, 快速返回結果, 避免大量的同步等待. 並且熔斷器能在一段時間後繼續偵測請求執行結果, 提供恢復服務調用的可能.

命令模式

Hystrix使用命令模式(繼承HystrixCommand類)來包裹具體的服務調用邏輯(run方法), 並在命令模式中添加了服務調用失敗後的降級邏輯(getFallback).同時我們在Command的構造方法中可以定義當前服務線程池和熔斷器的相關參數. 如下代碼所示:

public class Service1HystrixCommand extends HystrixCommand<Response> {
  private Service1 service;
  private Request request;

  public Service1HystrixCommand(Service1 service, Request request){
    supper(
      Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ServiceGroup"))
          .andCommandKey(HystrixCommandKey.Factory.asKey("servcie1query"))
          .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("service1ThreadPool"))
          .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
            .withCoreSize(20))//服務線程池數量
          .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
            .withCircuitBreakerErrorThresholdPercentage(60)//熔斷器關閉到打開閾值
            .withCircuitBreakerSleepWindowInMilliseconds(3000)//熔斷器打開到關閉的時間窗長度
      ))
      this.service = service;
      this.request = request;
    );
  }

  @Override
  protected Response run(){
    return service1.call(request);
  }

  @Override
  protected Response getFallback(){
    return Response.dummy();
  }
}

在使用了Command模式構建了服務對象之後, 服務便擁有了熔斷器和線程池的功能. 

Hystrix的內部處理邏輯

下圖爲Hystrix服務調用的內部邏輯: 

  1. 構建Hystrix的Command對象, 調用執行方法.

  2. Hystrix檢查當前服務的熔斷器開關是否開啓, 若開啓, 則執行降級服務getFallback方法.

  3. 若熔斷器開關關閉, 則Hystrix檢查當前服務的線程池是否能接收新的請求, 若超過線程池已滿, 則執行降級服務getFallback方法.

  4. 若線程池接受請求, 則Hystrix開始執行服務調用具體邏輯run方法.

  5. 若服務執行失敗, 則執行降級服務getFallback方法, 並將執行結果上報Metrics更新服務健康狀況.

  6. 若服務執行超時, 則執行降級服務getFallback方法, 並將執行結果上報Metrics更新服務健康狀況.

  7. 若服務執行成功, 返回正常結果.

  8. 若服務降級方法getFallback執行成功, 則返回降級結果.

  9. 若服務降級方法getFallback執行失敗, 則拋出異常.

Hystrix Metrics的實現

Hystrix的Metrics中保存了當前服務的健康狀況, 包括服務調用總次數和服務調用失敗次數等. 根據Metrics的計數, 熔斷器從而能計算出當前服務的調用失敗率, 用來和設定的閾值比較從而決定熔斷器的狀態切換邏輯. 因此Metrics的實現非常重要.

1.4之前的滑動窗口實現

Hystrix在這些版本中的使用自己定義的滑動窗口數據結構來記錄當前時間窗的各種事件(成功,失敗,超時,線程池拒絕等)的計數. 
事件產生時, 數據結構根據當前時間確定使用舊桶還是創建新桶來計數, 並在桶中對計數器經行修改. 
這些修改是多線程併發執行的, 代碼中有不少加鎖操作,邏輯較爲複雜.

1.5之後的滑動窗口實現

Hystrix在這些版本中開始使用RxJava的Observable.window()實現滑動窗口.
RxJava的window使用後臺線程創建新桶, 避免了併發創建桶的問題.
同時RxJava的單線程無鎖特性也保證了計數變更時的線程安全. 從而使代碼更加簡潔. 
以下爲我使用RxJava的window方法實現的一個簡易滑動窗口Metrics, 短短几行代碼便能完成統計功能,足以證明RxJava的強大:

@Test
public void timeWindowTest() throws Exception{
  Observable<Integer> source = Observable.interval(50, TimeUnit.MILLISECONDS).map(i -> RandomUtils.nextInt(2));
  source.window(1, TimeUnit.SECONDS).subscribe(window -> {
    int[] metrics = new int[2];
    window.subscribe(i -> metrics[i]++,
      InternalObservableUtils.ERROR_NOT_IMPLEMENTED,
      () -> System.out.println("窗口Metrics:" + JSON.toJSONString(metrics)));
  });
  TimeUnit.SECONDS.sleep(3);
}

總結

通過使用Hystrix,我們能方便的防止雪崩效應, 同時使系統具有自動降級和自動恢復服務的效果.

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