天天看點

微服務-技術專區-熔斷器 Hystrix 的原理與使用

前言

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

服務雪崩效應的定義

服務雪崩效應是一種因 服務提供者 的不可用導緻 服務調用者 的不可用,并将不可用 逐漸放大 的過程

服務雪崩效應形成的原因

我把服務雪崩的參與者簡化為 服務提供者和服務調用者, 并将服務雪崩産生的過程分為以下三個階段來分析形成的原因:
  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的内部處理邏輯

  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的實作非常重要.

之前的滑動視窗實作

  Hystrix在這些版本中的使用自己定義的滑動視窗資料結構來記錄目前時間窗的各種事件(成功,失敗,逾時,線程池拒絕等)的計數.

  事件産生時, 資料結構根據目前時間确定使用舊桶還是建立新桶來計數, 并在桶中對計數器經行修改. 

  這些修改是多線程并發執行的, 代碼中有不少加鎖操作,邏輯較為複雜.

之後的滑動視窗實作

       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);
}           

極限就是為了超越而存在的