Hystrix介紹和使用指南

一、Hystrix
Hystrix是Netflix開源的一款容錯系統,能幫助使用者碼出具備強大的容錯能力和魯棒性的程序。提供降級,熔斷等功能,並且熔斷開關打開之後,會在服務可用之後,自動關閉。spring cloud中有用到。
如果你的服務依賴於多個服務,並且不想因爲某個服務掛掉,而影響你服務。比如hbase掛掉了,你可以通過降級策略返回默認值,或者直接熔斷。
Hystrix提供了服務隔離,每個服務使用獨立的線程池實現。

二、 工作流程
流程

下面將更詳細的解析每一個步驟都發生哪些動作:

構建一個HystrixCommand或者HystrixObservableCommand對象。

第一步就是構建一個HystrixCommand或者HystrixObservableCommand對象,該對象將代表你的一個依賴請求,向構造函數中傳入請求依賴所需要的參數。

如果構建HystrixCommand中的依賴返回單個響應,例如:

public class HelloWorldHystrixCommand extends HystrixCommand {
    private final String name;

    public HelloWorldHystrixCommand(String name) {
        super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
        this.name = name;
    }

    // 如果繼承的是HystrixObservableCommand,要重寫Observable construct()
    @Override
    protected String run() throws InterruptedException {
        return "Hello " + name + "! thread:" + Thread.currentThread().getName();
    }

    @Override
    protected String getFallback() {
        System.out.println("觸發了降級!");
        return "exeucute fallback";
    }

    public static void main(String[] args) {
        /* 調用程序對HelloWorldHystrixCommand實例化,執行execute()即觸發HelloWorldHystrixCommand.run()的執行 */
        Object result = new HelloWorldHystrixCommand("HLX").execute();
        System.out.println(result);  // 打印出Hello HLX
    }
}

有4種方式可以執行一個Hystrix命令。

execute()—該方法是阻塞的,從依賴請求中接收到單個響應(或者出錯時拋出異常)。
queue()—從依賴請求中返回一個包含單個響應的Future對象。
observe()—訂閱一個從依賴請求中返回的代表響應的Observable對象。
toObservable()—返回一個Observable對象,只有當你訂閱它時,它纔會執行Hystrix命令併發射響應。

K             value   = command.execute();
Future<K>     fValue  = command.queue();
Observable<K> ohValue = command.observe();         //hot observable
Observable<K> ocValue = command.toObservable();    //cold observable

在這裏插入圖片描述

三、降級(fallback)
使用fallback機制很簡單,繼承HystrixCommand只需重寫getFallback(),繼承HystrixObservableCommand只需重寫resumeWithFallback()。

根據上面的工作流程圖所示,當出現以下幾種情況時會觸發降級:

名字 描述
FAILURE 執行拋出異常
TIMEOUT 執行開始,但沒有在允許的時間內完成
SHORT_CIRCUITED 斷路器打開,不嘗試執行
THREAD_POOL_REJECTED 線程池拒絕,不嘗試執行
SEMAPHORE_REJECTED 信號量拒絕,不嘗試執行

如果沒有實現降級方法,默認會拋出異常。降級方法也可能會超時,或者拋出異常

四、隔離
hystrix提供了兩種隔離策略:線程池隔離和信號量隔離。hystrix默認採用線程池隔離。
我認爲採用信號量隔離並不是真正的隔離,因爲還是在調用者的線程中執行,如果服務掛了,還是會拖垮調用線程。使用信號量的超時機制,是在結果返回之後才計算是否超時的,服務掛了之後,肯定很長時間才返回,這時候再採取降級已經沒有意義了。

在這裏插入圖片描述

線程池隔離:不同服務通過使用不同線程池,彼此間將不受影響,達到隔離效果。**代碼是在hystrix的線程中執行的,如果超時,可以中斷。**以demo爲例,我們通過andThreadPoolKey配置使用命名爲ThreadPoolTest的線程池,實現與其他命名的線程池天然隔離,如果不配置andThreadPoolKey則使用withGroupKey配置來命名線程池。

public class HystrixCommand4ThreadPoolTest extends HystrixCommand<String> {

    private final String name;

    public HystrixCommand4ThreadPoolTest(String name) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ThreadPoolTestGroup"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("testCommandKey"))
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ThreadPoolTest"))
                .andCommandPropertiesDefaults(
                        HystrixCommandProperties.Setter()
                                .withExecutionTimeoutInMilliseconds(1500)
                                .withExecutionIsolationThreadInterruptOnTimeout(true)
                                .withCircuitBreakerErrorThresholdPercentage(50)
                                .withCircuitBreakerRequestVolumeThreshold(5)
                )
                .andThreadPoolPropertiesDefaults(
                        HystrixThreadPoolProperties.Setter()
                                .withCoreSize(3)// 配置線程池裏的線程數
                                .withMaxQueueSize(20)
                                .withQueueSizeRejectionThreshold(20)
                )
        );
        this.name = name;
    }

    @Override
    protected String run() throws Exception {
        System.out.println("====command run : " + Thread.currentThread().getName() + " at: " + System.currentTimeMillis());
        TimeUnit.MILLISECONDS.sleep(3000);
//        throw  new RuntimeException("=== timeout ...");
        return name;

    }

    @Override
    protected String getFallback() {
        return "fallback: " + name + Thread.currentThread().getName() + " at : " + System.currentTimeMillis();
    }

    public static class UnitTest {

        @Test
        public void testSynchronous() throws IOException {
            for (int i = 0; i < 10; i++) {
                try {
                    new Thread(() -> System.out.println("===========" + new HystrixCommand4ThreadPoolTest("Hlx").execute())).start();
                    Thread.sleep(500);
                } catch (Exception e) {
                    System.out.println("run()拋出HystrixBadRequestException時,被捕獲到這裏" + e.getCause());
                }
            }

            try {
                TimeUnit.MILLISECONDS.sleep(5000);
            } catch (Exception e) {
            }

            for (int i = 0; i < 10; i++) {
                try {
                    new Thread(() -> System.out.println("===========" + new HystrixCommand4ThreadPoolTest("Hlx").execute())).start();
//                    futures.add(new HystrixCommand4ThreadPoolTest("Hlx"+i).queue());
                } catch (Exception e) {
                    System.out.println("run()拋出HystrixBadRequestException時,被捕獲到這裏" + e.getCause());
                }
            }
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

信號量隔離:線程隔離會帶來線程開銷,有些場景(比如無網絡請求場景)可能會因爲用開銷換隔離得不償失,爲此hystrix提供了信號量隔離,當服務的併發數大於信號量閾值時將進入fallback。

package com.step.jliang.hystrix;

import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.hystrix.HystrixThreadPoolKey;
import com.netflix.hystrix.HystrixThreadPoolProperties;

import java.util.concurrent.TimeUnit;

/**
 * @author jliang
 */
public class HystrixCommand4Semphore extends HystrixCommand<String> {
    private final String name;

    public HystrixCommand4Semphore(String name) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("SemphoreTestGroup"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("testCommandKey"))
                .andCommandPropertiesDefaults(
                        HystrixCommandProperties.Setter()
                                .withExecutionTimeoutInMilliseconds(1500)
                                .withCircuitBreakerErrorThresholdPercentage(50)
                                .withCircuitBreakerRequestVolumeThreshold(5)
                                .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
                                .withExecutionIsolationSemaphoreMaxConcurrentRequests(5)
                )
        );
        this.name = name;
    }

    @Override
    protected String run() throws Exception {
        System.out.println("====command run : " + Thread.currentThread().getName() + " at: " + System.currentTimeMillis());
        TimeUnit.MILLISECONDS.sleep(3000);
        return name;

    }

//    @Override
//    protected String getFallback() {
//        return "semphore fallback: " + name + Thread.currentThread().getName() + " at : " + System.currentTimeMillis();
//    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            try {
                new Thread(() -> System.out.println("===========" + new HystrixCommand4Semphore("Hlx").execute())).start();
                Thread.sleep(500);
            } catch (Exception e) {
                System.out.println("run()拋出HystrixBadRequestException時,被捕獲到這裏" + e.getCause());
            }
        }
    }
}

五、如何使用熔斷器(Circuit Breaker)

熔斷器,現實生活中有一個很好的類比,就是家庭電路中都會安裝一個保險盒,當電流過大的時候保險盒裏面的保險絲會自動斷掉,來保護家裏的各種電器及電路。Hystrix中的熔斷器(Circuit Breaker)也是起到這樣的作用,Hystrix在運行過程中會向每個commandKey對應的熔斷器報告成功、失敗、超時和拒絕的狀態,熔斷器維護計算統計的數據,根據這些統計的信息來確定熔斷器是否打開。如果打開,後續的請求都會被截斷。然後會隔一段時間默認是5s,嘗試半開,放入一部分流量請求進來,相當於對依賴服務進行一次健康檢查,如果恢復,熔斷器關閉,隨後完全恢復調用。

熔斷器的數據通路如下圖所示:
在這裏插入圖片描述

每個熔斷器默認維護10個bucket,每秒一個bucket,每個blucket記錄成功,失敗,超時,拒絕的狀態,默認錯誤超過50%且10秒內超過20個請求進行中斷攔截。

由於Hystrix是一個容錯框架,因此我們在使用的時候,要達到熔斷的目的只需配置一些參數就可以了。但我們要達到真正的效果,就必須要了解這些參數。Circuit Breaker一共包括如下6個參數。
1、circuitBreaker.enabled
是否啓用熔斷器,默認是TURE。
2、circuitBreaker.forceOpen
熔斷器強制打開,始終保持打開狀態。默認值FLASE。
3、circuitBreaker.forceClosed
熔斷器強制關閉,始終保持關閉狀態。默認值FLASE。
4、circuitBreaker.errorThresholdPercentage
設定錯誤百分比,默認值50%,例如一段時間(10s)內有100個請求,其中有55個超時或者異常返回了,那麼這段時間內的錯誤百分比是55%,大於了默認值50%,這種情況下觸發熔斷器-打開。
5、circuitBreaker.requestVolumeThreshold
默認值20.意思是至少有20個請求才進行errorThresholdPercentage錯誤百分比計算。比如一段時間(10s)內有19個請求全部失敗了。錯誤百分比是100%,但熔斷器不會打開,因爲requestVolumeThreshold的值是20. 這個參數非常重要,熔斷器是否打開首先要滿足這個條件。在自己測試的時候,如果熔斷器不起作用,要看下是否滿足這個條件。
6、circuitBreaker.sleepWindowInMilliseconds
半開試探休眠時間,默認值5000ms。當熔斷器開啓一段時間之後比如5000ms,會嘗試放過去一部分流量進行試探,確定依賴服務是否恢復。

具體的例子可以參考上面使用線程池隔離的代碼。

六、常見的參數及默認值
可以參考這篇博客

如果還有不明白的,可以參考官方文檔

參考鏈接:
https://www.jianshu.com/p/14958039fd15
https://github.com/Netflix/Hystrix/wiki/How-it-Works
https://segmentfault.com/a/1190000012439580
https://blog.csdn.net/tongtong_use/article/details/78611225
https://juejin.im/entry/59dbe2cc51882578d152bdf8
https://www.jianshu.com/p/b9af028efebb

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