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

前言

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

服務雪崩效應的定義

服務雪崩效應是一種因 服務提供者 的不可用致使 服務調用者 的不可用,並將不可用 逐漸放大 的過程git

服務雪崩效應造成的緣由

我把服務雪崩的參與者簡化爲 服務提供者 和 服務調用者, 並將服務雪崩產生的過程分爲如下三個階段來分析造成的緣由:github

  1. 服務提供者不可用web

  2. 重試加大流量spring

  3. 服務調用者不可用apache

服務雪崩的每一個階段均可能由不一樣的緣由形成, 好比形成 服務不可用 的緣由有:segmentfault

  • 硬件故障後端

  • 程序Bug緩存

  • 緩存擊穿tomcat

  • 用戶大量請求

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

而造成 重試加大流量 的緣由有:

  • 用戶重試

  • 代碼邏輯重試

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

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

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

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

服務雪崩的應對策略

針對形成服務雪崩的不一樣緣由, 可使用不一樣的應對策略:

  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的實現很是重要.

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

簡單測試:

引入jar包:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-hystrix</artifactId>
            <version>1.4.3.RELEASE</version>
        </dependency>

導入配置:

server:
  port: 11111
  
#default可替換
hystrix:
  command:
    default:
      execution:
        isolation:
          #線程池隔離仍是信號量隔離 默認是THREAD 信號量是SEMAPHORE
          strategy: THREAD
          semaphore:
            #使用信號量隔離時,支持的最大併發數 默認10
            maxConcurrentRequests: 10
          thread:
            #command的執行的超時時間 默認是1000
            timeoutInMilliseconds: 1000
            #HystrixCommand.run()執行超時時是否被打斷 默認true
            interruptOnTimeout: true
            #HystrixCommand.run()被取消時是否被打斷 默認false
            interruptOnCancel: false
        timeout:
          #command執行時間超時是否拋異常 默認是true
          enabled: true
        fallback:
          #當執行失敗或者請求被拒絕,是否會嘗試調用hystrixCommand.getFallback()
          enabled: true
          isolation:
            semaphore:
              #若是併發數達到該設置值,請求會被拒絕和拋出異常而且fallback不會被調用 默認10
              maxConcurrentRequests: 10
      circuitBreaker:
        #用來跟蹤熔斷器的健康性,若是未達標則讓request短路 默認true
        enabled: true
        #一個rolling window內最小的請求數。若是設爲20,那麼當一個rolling window的時間內
        #(好比說1個rolling window是10秒)收到19個請求,即便19個請求都失敗,也不會觸發circuit break。默認20
        requestVolumeThreshold: 5
        # 觸發短路的時間值,當該值設爲5000時,則當觸發circuit break後的5000毫秒內
        #都會拒絕request,也就是5000毫秒後纔會關閉circuit,放部分請求過去。默認5000
        sleepWindowInMilliseconds: 5000
        #錯誤比率閥值,若是錯誤率>=該值,circuit會被打開,並短路全部請求觸發fallback。默認50
        errorThresholdPercentage: 50
        #強制打開熔斷器,若是打開這個開關,那麼拒絕全部request,默認false
        forceOpen: false
        #強制關閉熔斷器 若是這個開關打開,circuit將一直關閉且忽略
        forceClosed: false
      metrics:
        rollingStats:
          #設置統計的時間窗口值的,毫秒值,circuit break 的打開會根據1個rolling window的統計來計算。若rolling window被設爲10000毫秒,
          #則rolling window會被分紅n個buckets,每一個bucket包含success,failure,timeout,rejection的次數的統計信息。默認10000
          timeInMilliseconds: 10000
          #設置一個rolling window被劃分的數量,若numBuckets=10,rolling window=10000,
          #那麼一個bucket的時間即1秒。必須符合rolling window % numberBuckets == 0。默認10
          numBuckets: 10
        rollingPercentile:
          #執行時是否enable指標的計算和跟蹤,默認true
          enabled: true
          #設置rolling percentile window的時間,默認60000
          timeInMilliseconds: 60000
          #設置rolling percentile window的numberBuckets。邏輯同上。默認6
          numBuckets: 6
          #若是bucket size=100,window=10s,若這10s裏有500次執行,
          #只有最後100次執行會被統計到bucket裏去。增長該值會增長內存開銷以及排序的開銷。默認100
          bucketSize: 100
        healthSnapshot:
          #記錄health 快照(用來統計成功和錯誤綠)的間隔,默認500ms
          intervalInMilliseconds: 500
      requestCache:
        #默認true,須要重載getCacheKey(),返回null時不緩存
        enabled: true
      requestLog:
        #記錄日誌到HystrixRequestLog,默認true
        enabled: true
  collapser:
    default:
      #單次批處理的最大請求數,達到該數量觸發批處理,默認Integer.MAX_VALUE
      maxRequestsInBatch: 2147483647
      #觸發批處理的延遲,也能夠爲建立批處理的時間+該值,默認10
      timerDelayInMilliseconds: 10
      requestCache:
        #是否對HystrixCollapser.execute() and HystrixCollapser.queue()的cache,默認true
        enabled: true
  threadpool:
    default:
      #併發執行的最大線程數,默認10
      coreSize: 10
      #Since 1.5.9 能正常運行command的最大支付併發數
      maximumSize: 10
      #BlockingQueue的最大隊列數,當設爲-1,會使用SynchronousQueue,值爲正時使用LinkedBlcokingQueue。
      #該設置只會在初始化時有效,以後不能修改threadpool的queue size,除非reinitialising thread executor。
      #默認-1。
      maxQueueSize: -1
      #即便maxQueueSize沒有達到,達到queueSizeRejectionThreshold該值後,請求也會被拒絕。
      #由於maxQueueSize不能被動態修改,這個參數將容許咱們動態設置該值。if maxQueueSize == -1,該字段將不起做用
      queueSizeRejectionThreshold: 5
      #Since 1.5.9 該屬性使maximumSize生效,值須大於等於coreSize,當設置coreSize小於maximumSize
      allowMaximumSizeToDivergeFromCoreSize: false
      #若是corePoolSize和maxPoolSize設成同樣(默認實現)該設置無效。
      #若是經過plugin(https://github.com/Netflix/Hystrix/wiki/Plugins)使用自定義實現,該設置纔有用,默認1.
      keepAliveTimeMinutes: 1
      metrics:
        rollingStats:
          #線程池統計指標的時間,默認10000
          timeInMilliseconds: 10000
          #將rolling window劃分爲n個buckets,默認10
          numBuckets: 10

其中execution:isolation:strategy有些區別:

       資源隔離。就是多個依賴服務的調用分別隔離到各自本身的資源池內。避免說對一個依賴服務的調用,由於依賴服務接口調用的失敗或者延遲,致使全部的線程資源
都所有耗費在這個接口上。一旦某個服務的線程資源所有耗盡量致使服務的崩潰,甚至故障蔓延。    2.資源隔離的方法
       信號量semaphore,最多能容納10個請求。一旦超過10個信號量最大容量,那麼就會拒絕其餘請求。
信號量與線程池資源隔離的區別:
      線程池隔離技術並不是控制tomcat等web容器的線程。更準確的說就是控制tomcat線程的執行。tomcat接到請求以後會調用hystrix線程池的線程去執行。當線程池滿了以後會調用fallback降級。
tomcat其餘的線程不會卡死,快速返回,而後能夠支撐其餘事情。同時hystrix處理timeout超時問題。
    信號量隔離只是一個關卡,經過個人關卡的線程是固定的。容量滿了以後。fallback降級。
    區別:線程池隔離技術是用本身的線程去執行調用。信號量是直接讓tomcat線程去執行依賴服務。

上圖是默認的配置,咱們能夠對本身的配置進行分組:

    針對不一樣的組在配置文件裏面加上不一樣的配置就行了,在@MyCommand註解裏面指定group爲abc就行;其餘的配置也是這個規則,還有默認的配置是default;這樣能夠把一個組的配置獨立出來,便於配置,並且開發者也會方便不少,代碼簡潔;

下面是代碼:

package cn.chinotan.controller;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @program: test
 * @description: hystrix控制器
 * @author: xingcheng
 * @create: 2018-11-03 19:27
 **/
@RestController
@RequestMapping("/hystrix")
public class HystrixController {

    @HystrixCommand(fallbackMethod = "helloFallback")
    @RequestMapping("/sayHello")
    public String sayHello(String name, Integer time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "Hello, " + name;
    }

    @HystrixCommand(fallbackMethod = "hiFallback")
    @RequestMapping("/sayHi")
    public String sayHi(String name) {
        if (StringUtils.isBlank(name)) {
            throw new RuntimeException("name不能爲空");
        }
        return "Good morning, " + name;
    }

    /**
     * fallback
     */
    public String helloFallback(String name, Integer time) {
        System.out.println("helloFallback: " + name);
        return "helloFallback" + name;
    }

    /**
     * fallback
     */
    public String hiFallback(String name) {
        System.out.println("hiFallback: " + name);
        return "hiFallback" + name;
    }
}
package cn.chinotan.config;

import com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect;
import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @program: test
 * @description: HystrixConfig
 * @author: xingcheng
 **/
@Configuration
public class HystrixConfig {

    /**
     * 用來攔截處理HystrixCommand註解
     * @return
     */
    @Bean
    public HystrixCommandAspect hystrixAspect() {
        return new HystrixCommandAspect();
    }

    /**
     * 用來像監控中心Dashboard發送stream信息
     * @return
     */
    @Bean
    public ServletRegistrationBean hystrixMetricsStreamServlet() {
        ServletRegistrationBean registration = new ServletRegistrationBean(new HystrixMetricsStreamServlet());
        registration.addUrlMappings("/hystrix.stream");
        return registration;
    }
    
}

配置監控後臺Hystrix-Dashboard

1.github上下載源碼https://github.com/kennedyoliveira/standalone-hystrix-dashboard

2.參考其wiki文檔,部署成功後,默認端口是7979;

3.點擊 http://127.0.0.1:7979/hystrix-dashboard 打開頁面

能夠看到sayHello接口請求3次成功,熔斷器如今是關閉狀態,當咱們調整time參數,使得接口超時後,看看接口的返回:

能夠看到接口返回Fallback方法的內容,證實接口超時後跳到這個方法中去了,可是熔斷器尚未打開,接下來進行屢次頻率高的接口訪問:

能夠看到熔斷器打開了,此時接口會很快就返回失敗回調方法的內容,若是過一會再次請求這個接口,time參數變小於超時時間,結果以下:

能夠看到熔斷器又關閉了,接口能夠正常訪問,這是爲何呢:

能夠看到這個參數,rolling window內最小的請求數。若是設爲20,那麼當一個rolling window的時間內好比說1個rolling window是10秒)收到19個請求,即便19個請求都失敗,也不會觸發circuit break。默認20

總結

  • 雪崩效應緣由:硬件故障、硬件故障、程序Bug、重試加大流量、用戶大量請求。
  • 雪崩的對策:限流、改進緩存模式(緩存預加載、同步調用改異步)、自動擴容、降級。
  • Hystrix設計原則:
    • 資源隔離:Hystrix經過將每一個依賴服務分配獨立的線程池進行資源隔離, 從而避免服務雪崩。
    • 熔斷開關:服務的健康情況 = 請求失敗數 / 請求總數,經過閾值設定和滑動窗口控制開關。
    • 命令模式:經過繼承 HystrixCommand 來包裝服務調用邏輯。

參考文章:https://segmentfault.com/a/1190000005988895

相關文章
相關標籤/搜索