no-image

防雪崩利器:熔斷器 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,我們能方便的防止雪崩效應, 同時使系統具有自動降級和自動恢復服務的效果.

關聯文章