SpringCloud 源碼系列(16)— 熔斷器Hystrix 之 基礎入門篇

專欄系列文章:SpringCloud系列專欄java

系列文章:mysql

SpringCloud 源碼系列(1)— 註冊中心Eureka 之 啓動初始化git

SpringCloud 源碼系列(2)— 註冊中心Eureka 之 服務註冊、續約github

SpringCloud 源碼系列(3)— 註冊中心Eureka 之 抓取註冊表web

SpringCloud 源碼系列(4)— 註冊中心Eureka 之 服務下線、故障、自我保護機制redis

SpringCloud 源碼系列(5)— 註冊中心Eureka 之 EurekaServer集羣spring

SpringCloud 源碼系列(6)— 註冊中心Eureka 之 總結篇sql

SpringCloud 源碼系列(7)— 負載均衡Ribbon 之 RestTemplate數據庫

SpringCloud 源碼系列(8)— 負載均衡Ribbon 之 核心原理緩存

SpringCloud 源碼系列(9)— 負載均衡Ribbon 之 核心組件與配置

SpringCloud 源碼系列(10)— 負載均衡Ribbon 之 HTTP客戶端組件

SpringCloud 源碼系列(11)— 負載均衡Ribbon 之 重試與總結篇

SpringCloud 源碼系列(12)— 服務調用Feign 之 基礎使用篇

SpringCloud 源碼系列(13)— 服務調用Feign 之 掃描@FeignClient註解接口

SpringCloud 源碼系列(14)— 服務調用Feign 之 構建@FeignClient接口動態代理

SpringCloud 源碼系列(15)— 服務調用Feign 之 結合Ribbon進行負載均衡請求

熔斷器 Hystrix

分佈式系統高可用問題

在分佈式系統中,服務與服務之間的依賴錯綜複雜,某些服務出現故障,可能致使依賴於它們的其餘服務出現級聯阻塞故障。

例以下圖,某個請求要調用 Service-A,Service-A 又要調用 Service-B、Service-D,Service-B 又要再調用 Service-C,Service-C 又依賴於數據庫、Redis等中間件。若是在調用 Service-C 時,因爲 Service-C 自己業務問題或數據庫宕機等狀況,就可能會致使 Service-B 的工做線程所有佔滿致使不可用,進而又致使級聯的 Service-A 資源耗盡不可用,這時就會致使整個系統出現大面積的延遲或癱瘓。

在高併發的狀況下,單個服務的延遲會致使整個請求都處於延遲阻塞狀態,最終的結果就是整個服務的線程資源消耗殆盡。服務的依賴性會致使依賴於該故障服務的其餘服務也處於線程阻塞狀態,最終致使這些服務的線程資源消耗殆盡,直到不可用,從而致使整個微服務系統都不可用,即雪崩效應。

例如,對於依賴 30 個服務的應用程序,每一個服務的正常運行時間爲 99.99%,對於單個服務來講,99.99% 的可用是很是完美的。有 99.99^30 = 99.7% 的可正常運行時間和 0.3% 的不可用時間,那麼 10 億次請求中就有 3000000 次失敗,實際的狀況可能比這更糟糕。

若是不設計整個系統的韌性,即便全部依賴關係表現良好,單個服務只有 0.01% 的不可用,因爲整個系統的服務相互依賴,最終對整個系統的影響是很是大的。

Hystrix 簡介

一、簡介

Hystrix是由Netflix開源的一個針對分佈式系統容錯處理的開源組件,旨在隔離遠程系統、服務和第三方庫,阻止級聯故障,在複雜的分佈式系統中實現恢復能力,從而提升了整個分佈式系統的彈性。

例如在上圖中,若是在 Service-B 調用 Service-C 時,引入 Hystrix 進行資源隔離,Service-C 故障或調用超時就自動降級返回,從而隔離了 Service-C 對 Service-B 的級聯影響,進而保證了整個系統的穩定性。

二、Hystrix的設計原則

總的來講Hystrix 的設計原則以下:

  • 防止單個服務的故障耗盡整個服務的Servlet容器(例如Tomcat)的線程資源。
  • 快速失敗機制,若是某個服務出現了故障,則調用該服務的請求快速失敗,而不是線程等待。
  • 提供回退(fallback)方案,在請求發生故障時,提供設定好的回退方案。
  • 使用熔斷機制,防止故障擴散到其餘服務。
  • 提供熔斷器的監控組件Hystrix Dashboard,能夠實時監控熔斷器的狀態。

三、官方文檔

GitHub:github.com/Netflix/Hys…

官方文檔:github.com/Netflix/Hys…

注意:Hystrix 再也不開發新功能,目前處於維護模式,最新穩定版本是 1.5.18

使用前在 spring cloud 項目中引入 netflix 相關依賴:我本地使用的 spring-cloud-netflix-hystrix 版本爲 2.2.5.RELEASE

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
複製代碼

四、參考書籍:

  • 《深刻理解 Spring Cloud 與微服務構建(第 2 版)》
  • 《從新定義Spring Cloud實戰》

Hystrix的工做機制

Hystrix的工做機制能夠參考官方【How-it-Works】。下面這幅圖展現了 Hystrix 的核心工做原理。

核心的原理以下:

  • ① 將對依賴服務的請求調用封裝到 HystrixCommand 或者 HystrixObservableCommand 中,這個訪問請求通常會在獨立的線程中執行。區別在於:

    • HystrixCommand 是用來獲取一條數據的。
    • HystrixObservableCommand 是設計用來獲取多條數據的,返回類型是 Observable。
  • ② 執行請求,有四個方法能夠調用:execute()、queue()、observe()、toObservable(),HystrixCommand 四個均可以調用,HystrixObservableCommand 只能調用 observe()toObservable()

  • ③ 若是這個命令啓用了緩存,且緩存中存在,就直接返回緩存中的數據。

  • ④ 檢查斷路器是否打開了,若是斷路器打開了就直接走快速失敗的邏輯返回。

  • ⑤ 檢查線程池(線程池隔離)或隊列(信號量隔離)是否已滿,滿了就直接走快速失敗的邏輯返回。

  • ⑥ 執行請求調用遠程服務,若是執行執行失敗或超時,就走降級邏輯返回。不然成功返回數據。

  • ⑦ 每次執行請求時,線程池是否拒絕、執行是否失敗、超時等,都會進行統計,而後計算是否打開斷路器。

    • 當某個接口的失敗次數超過設定的閾值時,Hystrix 斷定該 API 接口出現了故障,打開熔斷器,這時請求該 API 接口會執行快速失敗的邏輯,不執行業務邏輯,請求的線程不會處於阻塞狀態。
    • 處於打開狀態的熔斷器,一段時間後會處於半打開狀態,並放行必定數量的請求執行正常邏輯。剩餘的請求會執行快速失敗。
    • 若執行正常邏輯的請求失敗了,則熔斷器繼續打開;若成功了,則將熔斷器關閉。這樣熔斷器就具備了自我修復的能力。
  • ⑧ 執行快速失敗的邏輯,也就是降級邏輯,若是降級邏輯執行成功,則返回;若是失敗須要自行處理,或拋出異常。

HystrixCommand

關於 HystrixCommand 或者 HystrixObservableCommand 的用法官方文檔【How To Use】已經介紹得很詳細了,這裏就跟着官方文檔中提供的例子來了解下 HystrixCommand 的使用方式以及 Hystrix 的特性、配置等。

構建 HystrixCommand

一、構建 HystrixCommand

繼承 HystrixCommand,將業務邏輯封裝到 HystrixCommand 的 run() 方法中,在 getFallback() 方法中實現錯誤回調的邏輯。

class CommandHello extends HystrixCommand<String> {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final String name;
    private final long timeout;

    protected CommandHello(String group, String name, long timeout) {
        // 指定命令的分組,同一組使用同一個線程池
        super(HystrixCommandGroupKey.Factory.asKey(group));
        this.name = name;
        this.timeout = timeout;
    }

    // 要封裝的業務請求
    @Override
    protected String run() throws Exception {
        logger.info("hystrix command execute");
        if (name == null) {
            throw new RuntimeException("data exception");
        }
        Thread.sleep(timeout); // 休眠
        return "hello " + name;
    }

    // 快速失敗的降級邏輯
    @Override
    protected String getFallback() {
        logger.info("return fallback data");
        return "error";
    }
}
複製代碼

二、構建 HystrixObservableCommand

繼承 HystrixObservableCommand,將業務邏輯封裝到 HystrixObservableCommand 的 construct() 方法中,在 resumeWithFallback() 方法中實現錯誤回調的邏輯。

class CommandObservableHello extends HystrixObservableCommand<String> {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    protected CommandObservableHello(String group) {
        // 指定命令的分組,同一組使用同一個線程池
        super(HystrixCommandGroupKey.Factory.asKey(group));
    }

    @Override
    protected Observable<String> construct() {
        logger.info("hystrix command execute");
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> subscriber) {
                // 發送多條數據
                subscriber.onNext("hello world");
                subscriber.onNext("hello hystrix");
                subscriber.onNext("hello command");
                subscriber.onCompleted();
            }
        });
    }

    // 快速失敗的降級邏輯
    @Override
    protected Observable<String> resumeWithFallback() {
        logger.info("return fallback data");
        return Observable.just("error");
    }
}
複製代碼

執行 HystrixCommand

一、執行請求的方法

每次執行 HystrixCommand 都須要從新建立一個 HystrixCommand 命令,而後調用 execute()queue()observe()toObservable() 中的一個方法執行請求。HystrixCommand 能夠調用四個方法,HystrixObservableCommand 只能調用 observe()、toObservable() 兩個方法。

四個方法的區別在於:

  • execute():同步調用,直到返回單條結果,或者拋出異常
  • queue():異步調用,返回一個 Future,能夠異步的作其它事情,後面能夠經過 Future 獲取單條結果
  • observe():返回一個訂閱對象 Observable,當即執行。能夠經過 Observable.toBlocking() 執行同步請求。
  • toObservable():返回一個 Observable,只有訂閱後纔會執行

進入 execute() 方法能夠發現,execute() 調用了 queue().get();再進入 queue() 方法能夠發現,queue() 實際又調用了 toObservable().toBlocking().toFuture(),也就是說,不管是哪一種方式執行command,最終都是依賴 toObservable() 去執行的。

二、HystrixCommand 執行請求

能夠看到 HystrixCommand 中的業務執行是在一個單獨的線程中執行的,線程名稱爲 hystrix-ExampleGroup-1,這就實現了資源的隔離了。

/** * 運行結果: * * 14:56:03.699 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.CommandHello - hystrix command execute * 14:56:04.206 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - execute result is: hello hystrix */
@Test
public void test_HystrixCommand_execute() {
    CommandHello command = new CommandHello("ExampleGroup", "hystrix", 500);
    // 同步執行
    String result = command.execute();
    logger.info("execute result is: {}", result);
}

/** * 運行結果: * * 14:56:19.269 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - do something... * 14:56:19.279 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.CommandHello - hystrix command execute * 14:56:19.785 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - queue result is: hello hystrix */
@Test
public void test_HystrixCommand_queue() throws Exception {
    CommandHello command = new CommandHello("ExampleGroup", "hystrix", 500);
    // 異步執行,返回 Future
    Future<String> future = command.queue();
    logger.info("do something...");
    logger.info("queue result is: {}", future.get());
}

/** * 運行結果 * * 14:59:56.748 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.CommandHello - hystrix command execute * 14:59:57.252 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - observe result is: hello hystrix */
@Test
public void test_HystrixCommand_observe_single() {
    CommandHello command = new CommandHello("ExampleGroup", "hystrix", 500);
    Observable<String> observable = command.observe();
    // 獲取請求結果,toBlocking() 是爲了同步執行,不加 toBlocking() 就是異步執行
    String result = observable.toBlocking().single();
    logger.info("observe result is: {}", result);
}

/** * 運行結果: * * 15:00:58.921 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.CommandHello - hystrix command execute * 15:00:59.424 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - subscribe result is: hello hystrix * 15:00:59.425 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - completed */
@Test
public void test_HystrixCommand_observe_subscribe() {
    CommandHello command = new CommandHello("ExampleGroup", "hystrix", 500);
    Observable<String> observable = command.observe();
    // 訂閱結果處理
    observable.toBlocking().subscribe(new Subscriber<String>() {
        @Override
        public void onCompleted() {
            logger.info("completed");
        }

        @Override
        public void onError(Throwable e) {
            logger.info("error", e);
        }

        @Override
        public void onNext(String s) {
            logger.info("subscribe result is: {}", s);
        }
    });
}
複製代碼

三、HystrixObservableCommand 執行請求

HystrixObservableCommand 與 HystrixCommand 是相似的,區別在於 HystrixObservableCommand 的業務邏輯是封裝到 construct() 方法中,且 HystrixObservableCommand 只能調用 observe()toObservable() 兩個方法執行命令。

/** * 運行結果: * * 15:22:49.306 [main] INFO com.lyyzoo.hystrix.CommandObservableHello - hystrix command execute * 15:22:49.316 [main] INFO com.lyyzoo.hystrix.Demo02_HystrixObservableCommand - last result: hello command */
@Test
public void test_HystrixObservableCommand_observe() {
    CommandObservableHello command = new CommandObservableHello("ExampleGroup");

    String result = command.observe().toBlocking().last();
    logger.info("last result: {}", result);
}

/** * 運行結果: * * 15:23:08.685 [main] INFO com.lyyzoo.hystrix.CommandObservableHello - hystrix command execute * 15:23:08.691 [main] INFO com.lyyzoo.hystrix.Demo02_HystrixObservableCommand - result data: hello world * 15:23:08.691 [main] INFO com.lyyzoo.hystrix.Demo02_HystrixObservableCommand - result data: hello hystrix * 15:23:08.691 [main] INFO com.lyyzoo.hystrix.Demo02_HystrixObservableCommand - result data: hello command * 15:23:08.691 [main] INFO com.lyyzoo.hystrix.Demo02_HystrixObservableCommand - completed */
@Test
public void test_HystrixObservableCommand_observe_subscribe() {
    CommandObservableHello command = new CommandObservableHello("ExampleGroup");

    command.observe().subscribe(new Observer<String>() {
        @Override
        public void onCompleted() {
            logger.info("completed");
        }

        @Override
        public void onError(Throwable e) {
            logger.info("error");
        }

        @Override
        public void onNext(String o) {
            logger.info("result data: {}", o);
        }
    });
}
複製代碼

容錯和降級回調

一、容錯模式

HystrixCommand 執行有兩種容錯模式,fail-fastfail-silent

  • fail-fast:不給fallback降級邏輯,run() 方法報錯後直接拋異常,拋出到主工做線程。
  • fail-silent:給一個fallback降級邏輯,run() 報錯了會走fallback降級。

基本不會用 fail-fast 模式,通常來講都會實現降級邏輯,在拋出異常後作一些兜底的事情。

二、實現降級方法

HystrixCommand 能夠經過 getFallback() 方法返回降級的數據,HystrixObservableCommand 能夠經過 resumeWithFallback() 返回降級的數據。當快速失敗、超時、斷路器打開的時候,就能夠進入降級回調方法中,例如從本地緩存直接返回數據,避免業務異常、阻塞等狀況。

經過文檔能夠了解到,有5中狀況會進入降級回調方法中,分別是 業務異常、業務超時、斷路器打開、線程池拒絕、信號量拒絕

三、Examples:

/** * 運行結果: * * 14:44:43.199 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - hystrix command execute * 14:44:43.203 [hystrix-ExampleGroup-1] DEBUG com.netflix.hystrix.AbstractCommand - Error executing HystrixCommand.run(). Proceeding to fallback logic ... * java.lang.RuntimeException: data exception * at com.lyyzoo.hystrix.Demo01_HystrixCommand$CommandHello.run(Demo01_HystrixCommand.java:75) * at com.lyyzoo.hystrix.Demo01_HystrixCommand$CommandHello.run(Demo01_HystrixCommand.java:61) * at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:302) * ........ * 14:44:43.210 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - return fallback data * 14:44:43.214 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - result is: error */
@Test
public void test_HystrixCommand_exception_fallback() {
    CommandHello command = new CommandHello("ExampleGroup", null, 500);
    // 拋出異常,返回降級邏輯中的數據
    String result = command.execute();
    logger.info("result is: {}", result);
}

/** * 運行結果: * * 14:51:27.114 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.CommandHello - hystrix command execute * 14:51:28.113 [HystrixTimer-1] INFO com.lyyzoo.hystrix.CommandHello - return fallback data * 14:51:28.119 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - result is: error */
@Test
public void test_HystrixCommand_timeout_fallback() {
    CommandHello command = new CommandHello("ExampleGroup", "hystrix", 1500);
    // 請求超時,返回降級邏輯中的數據
    String result = command.execute();
    logger.info("result is: {}", result);
}
複製代碼

四、多級降級

多級降級其實就是在降級邏輯中再嵌套一個 command,command 嵌套 command 的形式。好比先從 MySQL 獲取數據,拋出異常時,降級從 redis 獲取數據。另外,不一樣的降級策略建議使用不一樣的線程池,由於若是 command 調用遠程服務時耗盡了線程池,降級 command 使用一樣的線程池時就會被拒絕。

Examples:

能夠看到,首先調用 CommandMySQL,run() 方法正常時就直接返回結果,拋出異常後就進入降級方法中,在降級方法中又用 CommandRedis 執行請求。

public class Demo03_HystrixCommand_MultiFallback {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    /** * 請求結果: * 22:28:46.313 [hystrix-MySqlPool-1] INFO com.lyyzoo.hystrix.CommandMySQL - get data from mysql * 22:28:46.319 [main] INFO com.lyyzoo.hystrix.Demo03_HystrixCommand_MultiFallback - result: mysql-number-1 * 22:28:46.320 [hystrix-MySqlPool-2] INFO com.lyyzoo.hystrix.CommandMySQL - get data from mysql * 22:28:46.324 [hystrix-MySqlPool-2] DEBUG com.netflix.hystrix.AbstractCommand - Error executing HystrixCommand.run(). Proceeding to fallback logic ... * java.lang.RuntimeException: data not found in mysql * at com.lyyzoo.hystrix.CommandMySQL.run(Demo03_HystrixCommand_MultiFallback.java:50) * at com.lyyzoo.hystrix.CommandMySQL.run(Demo03_HystrixCommand_MultiFallback.java:29) * at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:302) * ...... * 22:28:46.332 [hystrix-MySqlPool-2] INFO com.lyyzoo.hystrix.CommandMySQL - coming mysql fallback * 22:28:46.344 [hystrix-RedisPool-1] INFO com.lyyzoo.hystrix.CommandRedis - get data from redis * 22:28:46.344 [main] INFO com.lyyzoo.hystrix.Demo03_HystrixCommand_MultiFallback - result: redis-number-2 */
    @Test
    public void test_HystrixCommand_multi_fallback() {
        CommandMySQL command = new CommandMySQL("ExampleGroup", 1);
        logger.info("result: {}", command.execute());

        CommandMySQL command2 = new CommandMySQL("ExampleGroup", 2);
        logger.info("result: {}", command2.execute());
    }

}

class CommandMySQL extends HystrixCommand<String> {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final String group;
    private final Integer id;

    public CommandMySQL(String group, Integer id) {
        super(
            HystrixCommand.Setter
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey(group))
                // 指定不一樣的線程池
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("MySqlPool"))
        );
        this.group = group;
        this.id = id;
    }

    @Override
    protected String run() throws Exception {
        logger.info("get data from mysql");
        if (id % 2 == 0) {
            throw new RuntimeException("data not found in mysql");
        }
        return "mysql-number-" + id;
    }

    // 快速失敗的降級邏輯
    @Override
    protected String getFallback() {
        logger.info("coming mysql fallback");
        // 嵌套 Command
        HystrixCommand<String> command = new CommandRedis(group, id);
        return command.execute();
    }
}

class CommandRedis extends HystrixCommand<String> {
    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final Integer id;

    public CommandRedis(String group, Integer id) {
        super(
            HystrixCommand.Setter
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey(group))
                // 指定不一樣的線程池
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("RedisPool"))
        );
        this.id = id;
    }

    @Override
    protected String run() throws Exception {
        logger.info("get data from redis");
        return "redis-number-" + id;
    }

    // 快速失敗的降級邏輯
    @Override
    protected String getFallback() {
        logger.info("coming redis fallback");
        return "error";
    }
}
複製代碼

HystrixCommand 維度劃分

一、配置分組名稱

默認狀況下,就是經過 command group 來定義一個線程池,同一個 command group 中的請求,都會進入同一個線程池中,並且還會經過command group來聚合一些監控和報警信息。經過 HystrixCommandGroupKey.Factory.asKey(group) 指定組名。

class CommandHello extends HystrixCommand<String> {

    protected CommandHello(String group) {
        super(HystrixCommandGroupKey.Factory.asKey(group));
    }
}
複製代碼

二、配置命令名稱

command 名稱不配置默認就是類名,能夠經過 andCommandKey(HystrixCommandKey.Factory.asKey("CommandName")) 配置。

例如 CommandHystrixConfig:

public CommandHystrixConfig() {
    super(
        Setter
            // 分組名稱
            .withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
            // 命令名稱,默認爲類名 getClass().getSimpleName()
            //.andCommandKey(HystrixCommandKey.Factory.asKey("ExampleName"))
    );
}
複製代碼

未配置名稱時,默認就是類名:

將註釋放開,就是指定的名稱:

三、配置線程池名稱

線程池名稱默認是分組名稱,若是不想用分組名稱,能夠手動設置線程池名稱,配置了線程名稱後,經過 .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ExampleThread")) 配置。

public CommandHystrixConfig() {
    super(
        Setter
            // 分組名稱
            .withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
            // 命令名稱,默認爲類名 getClass().getSimpleName()
            .andCommandKey(HystrixCommandKey.Factory.asKey("ExampleName"))
            // 線程名稱
            .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ExampleThread"))
    );
}
複製代碼

四、線程池隔離劃分

綜上來看,一個 command 的執行劃分爲三個維度:command threadpool -> command group -> command key。

  • command key:通常對應到一個依賴服務的接口調用。
  • command group:通常對應到一個服務的全部接口,包含同一個服務的多個接口,便於對同一個服務的接口調用狀況聚合統計。
  • command threadpool:command 執行的線程池,通常一個 group 對應一個 threadpool,但若是想細粒度拆分,command 能夠指定不一樣的線程池。

配置資源隔離策略

Hystrix 核心的一項功能就是資源隔離,要解決的最核心的問題,就是將多個依賴服務的調用分別隔離到各自本身的資源池內,避免對某一個依賴服務的調用,由於依賴服務的接口調用的延遲或者失敗,致使服務全部的線程資源所有耗費在這個服務的接口調用上,一旦某個服務的線程資源所有耗盡,就可能致使服務崩潰,甚至故障會不斷蔓延。

Hystrix 有線程池隔離信號量隔離兩種資源隔離技術:

  • 線程池隔離:適合絕大多數的場景,依賴服務調用、網絡請求,被調用方可能會不可用、超時等,用線程池隔離
  • 信號量隔離:適合不是對外部依賴的訪問,而是對內部的一些比較複雜的業務邏輯的訪問,不涉及任何的網絡請求,只要作信號量的普通限流就能夠了,在高併發的狀況下作限流。

一、啓用線程池隔離

public CommandHystrixConfig() {
    super(
        Setter
            // 分組名稱
            .withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
            .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                    // 設置隔離策略
                    .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)
                    // 超時時間,默認 1000 毫秒
                    .withExecutionTimeoutInMilliseconds(1000)
                    // 是否啓用降級策略
                    .withFallbackEnabled(true)
                    // 是否啓用緩存
                    .withRequestCacheEnabled(true)
            )
    );
}
複製代碼

二、啓用信號量隔離

在設置 .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD) 時,只須要將 THREAD 改成 SEMAPHORE 便可,這個時候 run() 方法將在主線程中執行。

三、資源容量大小控制

  • withCoreSize(10):設置線程池的大小,默認是10,通常設置10個就足夠了
  • withQueueSizeRejectionThreshold(5):command 在提交到線程池以前會先進入一個隊列中,這個隊列滿了以後,纔會reject,這個參數能夠控制隊列拒絕的閾值。
public CommandHystrixConfig() {
    super(
        Setter
            .withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
            .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("DemoPool"))
            .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                    // 設置隔離策略
                    .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)
                    // 超時時間,默認 1000 毫秒
                    .withExecutionTimeoutInMilliseconds(1000)
            )
            .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
                    // 核心線程池大小,默認10個
                    .withCoreSize(10)
                    // 最大線程數,默認10個
                    .withMaximumSize(10)
                    // 隊列數量達到多少後拒絕請求
                    .withQueueSizeRejectionThreshold(5)
            )
    );
}
複製代碼
  • withExecutionIsolationSemaphoreMaxConcurrentRequests(10):信號量隔離時,設置信號量併發數,與線程池大小的設置相似。
public CommandHystrixConfig() {
    super(
        Setter
            .withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
            .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("DemoPool"))
            .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                    // 設置隔離策略
                    .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
                    // 信號量隔離時最大併發量,默認10
                    .withExecutionIsolationSemaphoreMaxConcurrentRequests(10)
            )
    );
}
複製代碼

開啓請求緩存

在一次請求上下文中,可能會建立多個 command 去執行遠程調用,若是調用的參數同樣、接口同樣,hystrix 支持緩存來減小執行一樣的 command,從而提高性能。

一、開啓緩存

開啓緩存只須要在 Command 中實現 getCacheKey() 方法返回緩存的 key 就能夠開啓緩存了。

// 指定緩存的key
@Override
protected String getCacheKey() {
    return name;
}
複製代碼

二、Examples

Hystrix 的緩存須要本身管理緩存上下文,須要本身初始化緩存上下文 HystrixRequestContext.initializeContext(),請求結束後須要關閉緩存上下文 HystrixRequestContext.getContextForCurrentThread().shutdown()。通常在web應用中,能夠經過增長前置過濾器來開啓 Hystrix 請求上下文,增長後置過濾器來關閉 Hystrix 上下文。

從驗證結果來看,最後兩個 command 執行時,因爲緩存已經存在一樣的 key 了,就沒有進入 run 方法了,並且能夠驗證就算是不一樣組,只要key相同就會被緩存。

/** * 運行結果: * * 20:55:43.759 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.CommandHello - hystrix command execute * 20:55:43.878 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - result: hello hystrix * 20:55:43.879 [hystrix-ExampleGroup-2] INFO com.lyyzoo.hystrix.CommandHello - hystrix command execute * 20:55:43.985 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - result: hello ribbon * 20:55:43.989 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - result: hello hystrix * 20:55:43.989 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - result: hello hystrix */
@Test
public void test_HystrixCommand_cache() {
    // 先初始化上下文
    HystrixRequestContext context = HystrixRequestContext.initializeContext();

    try {
        CommandHello command1 = new CommandHello("ExampleGroup", "hystrix", 100);
        CommandHello command2 = new CommandHello("ExampleGroup", "ribbon", 100);
        CommandHello command3 = new CommandHello("ExampleGroup", "hystrix", 100);
        CommandHello command4 = new CommandHello("ExampleGroupTwo", "hystrix", 100);

        logger.info("result: {}", command1.execute());
        logger.info("result: {}", command2.execute());
        logger.info("result: {}", command3.execute());
        logger.info("result: {}", command4.execute());
    } finally {
        //HystrixRequestContext.getContextForCurrentThread().shutdown();
        context.shutdown();
    }
}
複製代碼

三、手動清除緩存

當數據更新時,咱們指望可以手動清理掉緩存,能夠經過以下方式清理指定 command 的緩存。

public static void flushCache(String key) {
    HystrixRequestCache.getInstance(HystrixCommandKey.Factory.asKey(CommandHello.class.getSimpleName()),
            HystrixConcurrencyStrategyDefault.getInstance()).clear(key);
}
複製代碼

開啓斷路器

一、Hystrix 斷路器的基本原理以下

  • 首先請求達到一個閾值後,纔會判斷是否開啓斷路器,經過 HystrixCommandProperties.circuitBreakerRequestVolumeThreshold() 設置,默認爲 20。
  • 超過閾值後,判斷錯誤比率是否超過閾值,經過 HystrixCommandProperties.circuitBreakerErrorThresholdPercentage() 設置,默認爲 50%。
  • 超過 50% 後就會打開斷路器,從 CLOSE 狀態變爲 OPEN
  • 當斷路器打開了以後,全部的請求都會被拒絕,直接進入降級邏輯。
  • 休眠一段時間後,經過 HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds() 設置,默認 5000毫秒;會進入半打開(half-open)狀態,放一個請求過去,若是這個請求仍是失敗,斷路器就繼續維持打開的狀態;若是請求成功,就關閉斷路器,自動恢復,轉到 CLOSE 狀態。

二、下面用一個 Demo 簡單測試下

斷路器配置以下:

// 超時時間1000毫秒
.withExecutionTimeoutInMilliseconds(1000)
// 啓用斷路器
.withCircuitBreakerEnabled(true)
// 限流閾值,超過這個值後纔會去判斷是否限流,默認20
.withCircuitBreakerRequestVolumeThreshold(4)
// 請求失敗百分比閾值,默認50
.withCircuitBreakerErrorThresholdPercentage(50)
// 斷路器打開後休眠多久,默認5000毫秒
.withCircuitBreakerSleepWindowInMilliseconds(5000)
複製代碼

Examples:

經過測試能夠發現,請求閾值設置爲4,第五個請求會繼續執行,不過也失敗了,這個時候就會判斷失敗比率超過50%了,斷路器打開,以後的全部請求就直接進入降級;休眠5秒後,進入半打開狀態,放一個請求過去,仍是失敗,斷路器繼續打開。

public class Demo05_HystrixCommand_CircuitBreaker {
    @Test
    public void test_CircuitBreaker() throws InterruptedException {
        for (int i = 1; i <= 10; i++) {
            CommandCircuitBreaker command = new CommandCircuitBreaker(1500L, i);
            command.execute();
            if (i == 7) {
                Thread.sleep(5000); // 休眠5秒
            }
        }
    }
}

/** * 運行結果: * * 10:53:30.910 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [1] execute command * 10:53:31.920 [HystrixTimer-1] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [1] execute fallback * 10:53:31.925 [hystrix-ExampleGroup-2] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [2] execute command * 10:53:32.936 [HystrixTimer-1] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [2] execute fallback * 10:53:32.937 [hystrix-ExampleGroup-3] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [3] execute command * 10:53:33.940 [HystrixTimer-2] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [3] execute fallback * 10:53:33.942 [hystrix-ExampleGroup-4] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [4] execute command * 10:53:34.950 [HystrixTimer-1] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [4] execute fallback * 10:53:34.952 [hystrix-ExampleGroup-5] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [5] execute command * 10:53:35.962 [HystrixTimer-3] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [5] execute fallback * 10:53:35.964 [main] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [6] execute fallback * 10:53:35.965 [main] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [7] execute fallback * 10:53:40.977 [hystrix-ExampleGroup-6] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [8] execute command * 10:53:41.986 [HystrixTimer-2] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [8] execute fallback * 10:53:41.988 [main] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [9] execute fallback * 10:53:41.989 [main] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [10] execute fallback */
class CommandCircuitBreaker extends HystrixCommand<String> {
    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final Long timeoutMill;
    private final Integer id;

    protected CommandCircuitBreaker(Long timeoutMill, Integer id) {
        super(
            Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                    // 超時時間1000毫秒
                    .withExecutionTimeoutInMilliseconds(1000)
                    // 啓用斷路器
                    .withCircuitBreakerEnabled(true)
                    // 限流閾值,超過這個值後纔會去判斷是否限流,默認20
                    .withCircuitBreakerRequestVolumeThreshold(4)
                    // 請求失敗百分比閾值,默認50
                    .withCircuitBreakerErrorThresholdPercentage(50)
                    // 斷路器打開後休眠多久,默認5000毫秒
                    .withCircuitBreakerSleepWindowInMilliseconds(5000)
                )
        );
        this.timeoutMill = timeoutMill;
        this.id = id;
    }

    @Override
    protected String run() throws Exception {
        logger.info("[{}] execute command", id);
        if (timeoutMill != null) {
            Thread.sleep(timeoutMill);
        }
        return "number-" + id;
    }

    @Override
    protected String getFallback() {
        logger.info("[{}] execute fallback", id);
        return "error-" + id;
    }
}
複製代碼

基於註解的方式使用 HystrixCommand

在 springboot 程序中,還能夠在組件方法上使用 @HystrixCommand 等註解將方法調用封裝到 hystrix 中去執行。同時還須要在啓動類上加上 @EnableHystrix 來啓用 Hystrix 的功能。

@Component
public class ProducerService {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @HystrixCommand( groupKey = "ExampleGroup", commandKey = "demo-producer", threadPoolKey = "ExamplePool", fallbackMethod = "queryFallback" )
    public String query(Integer id) {
        logger.info("execute query");
        if (id % 2 == 0) {
            throw new RuntimeException("execution error");
        }
        return "number-" + id;
    }

    // 回調方法參數要和原方法參數一致
    public String queryFallback(Integer id) {
        return "error-" + id;
    }
}
複製代碼

測試結果:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ConsumerApplication.class)
public class ProducerServiceTest {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ProducerService producerService;

    /** * 執行結果: * * INFO 10:49:13 [hystrix-ExamplePool-1] c.l.s.r.h.ProducerService.query 25 :execute query * INFO 10:49:13 [main] c.l.h.ProducerServiceTest.test_query 29 :result: number-1 * INFO 10:49:13 [hystrix-ExamplePool-2] c.l.s.r.h.ProducerService.query 25 :execute query * INFO 10:49:13 [main] c.l.h.ProducerServiceTest.test_query 30 :result: error-2 */
    @Test
    public void test_query() {
        logger.info("result: {}", producerService.query(1));
        logger.info("result: {}", producerService.query(2));
    }
}
複製代碼
相關文章
相關標籤/搜索