如何使用原生的Hystrix

什麼是Hystrix

前面已經講完了 Feign 和 Ribbon,今天我們來研究 Netflix 團隊開發的另一個類庫--Hystrix。

從抽象層面看,Hystrix 是一個保護器。它可以保護我們的應用不會因爲某個依賴的故障而 down 掉。

目前,官方已不再迭代 Hystrix,一方面是認爲 Hystrix 已經足夠穩定了,另一方面是轉向了更具彈性的保護器(而不是根據預先配置來啓用保護),例如 resilience4j。當然,停止迭代並不是說 Hystrix 已經沒有價值,它的很多思想仍值得學習和借鑑。

zzs_hystrix_001.png

和之前一樣,本文研究的 Hystrix 是原生的,而不是被 Spring 層層封裝的。

Hystrix解決了什麼問題

關於這個問題,官方已經給了詳細的答案(見文末鏈接的官方 wiki)。這裏我結合着給出自己的一些理解(下面的圖也是借用官方的)。

我們的應用經常需要去調用某些依賴。這裏說的依賴,一般是遠程服務,那爲什麼不直接說遠程服務呢?因爲 Hystrix 適用的場景要更寬泛一些,當我們學完 Hystrix 就會發現,即使是應用裏調用的普通方法也可以算是依賴。

zzs_hystrix_002.png

調用這些依賴,有可能會遇到異常:調用失敗或調用超時

先說說調用失敗。當某個依賴 down 掉時,我們的應用調用它都會失敗。針對這種情況,我們會考慮快速失敗,從而減少大量調用失敗的開銷。

zzs_hystrix_003.png

再說說調用超時。不同於調用失敗,這個時候依賴還是可用的,只是需要花費更多的時間來獲取我們想要的東西。當流量比較大時,線程池將很快被耗盡。在大型的項目中,一個依賴的超時帶來的影響會被放大,甚至會導致整個系統癱瘓。所以,調用失敗也需要快速失敗。

zzs_hystrix_004.png

針對上面說的的異常,Hystrix 可以及時將故障的依賴隔離開,後續的調用都會快速失敗,直到依賴恢復正常。

如何實現

調用失敗或超時到達一定的閾值後,Hystrix 的保護器將被觸發開啓。

調用依賴之前,Hystrix 會檢查保護器是否開啓,如果開啓會直接走 fall back,如果沒有開啓,纔會執行調用操作。

另外,Hystrix 會定時地去檢查依賴是否已經恢復,當依賴恢復時,將關閉保護器,整個調用鏈路又恢復正常。

當然,實際流程要更復雜一些,還涉及到了緩存、線程池等。官方提供了一張圖,並給出了較爲詳細的描述。zzs_hystrix_005.png

如何使用

這裏我用具體例子來說明各個節點的邏輯,項目代碼見文末鏈接。

包裝爲command

首先,要使用 Hystrix,我們需要將對某個依賴的調用請求包裝成一個 command,具體通過繼承HystrixCommandHystrixObservableCommand進行包裝。繼承後我們需要做三件事:

  1. 在構造中指定 commandKey 和 commandGroupKey。需要注意的是,相同 commandGroupKey 的 command 會共用一個線程池,相同 commandKey 的會共用一個保護器和緩存。例如,我們需要根據用戶 id 從 UC 服務獲取用戶對象,可以讓所有 UC 接口共用一個 commandGroupKey,而不同的接口採用不同的 commandKey。
  2. 重寫 run 或 construct 方法。這個方法裏放的是我們調用某個依賴的代碼。我可以放調用遠程服務的代碼,也可以隨便打印一句話,因此,我前面說過,依賴的定義可以更寬泛一些,而不僅限於遠程服務。
  3. 重寫 getFallback 方法。當快速失敗時,就會走這個方法。
public class CommandGetUserByIdFromUserService extends HystrixCommand<DataResponse<User>> {
    
    private final String userId;
    
    public CommandGetUserByIdFromUserService(String userId) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserService")) // 相同command group共用一個ThreadPool
                .andCommandKey(HystrixCommandKey.Factory.asKey("UserService_GetUserById"))// 相同command key共用一個CircuitBreaker、requestCache
               );
        this.userId = userId;
    }

    /**
     * 執行最終任務,如果繼承的是HystrixObservableCommand則重寫construct()
     */
    @Override
    protected DataResponse<User> run() {
        return userService.getUserById(userId);
    }
    
    /**
     * 該方法在以下場景被調用
     * 1. 最終任務執行時拋出異常;
     * 2. 最終任務執行超時;
     * 3. 斷路器開啓時,請求短路;
     * 4. 連接池、隊列或信號量耗盡
     */
    @Override
    protected DataResponse<User> getFallback() {
        return DataResponse.buildFailure("fail or timeout");
    }
}

執行command

然後,只有執行 command,上面的圖就“動起來”了。有四種方法執行 command,調用 execute() 或 observe() 會馬上執行,而調用 queue() 或 toObservable() 不會馬上執行,要等 future.get() 或 observable.subscribe() 時纔會被執行。

    @Test
    public void testExecuteWays() throws Exception {
        
        DataResponse<User> response = new CommandGetUserByIdFromUserService("1").execute();// execute()=queue().get() 同步
        LOG.info("command.execute():{}", response);
        
        Future<DataResponse<User>> future = new CommandGetUserByIdFromUserService("1").queue();//queue()=toObservable().toBlocking().toFuture() 同步
        LOG.info("command.queue().get():{}", future.get());
        
        Observable<DataResponse<User>> observable = new CommandGetUserByIdFromUserService("1").observe();//hot observable 異步
        
        observable.subscribe(x -> LOG.info("command.observe():{}", x));
        
        Observable<DataResponse<User>> observable2 = new CommandGetUserByIdFromUserService("1").toObservable();//cold observable 異步
        
        observable2.subscribe(x -> LOG.info("command.toObservable():{}", x));
    }

是否使用緩存

接着,進入 command 的邏輯後,Hystrix 會先判斷是否使用緩存。

默認情況下,緩存是禁用的,我們可以通過重寫 command 的 getCacheKey() 來開啓(只要返回非空,都會開啓)。

    @Override
    protected String getCacheKey() {
        return userId;
    }

需要注意一點,用到緩存(HystrixRequestCache)、請求日誌(HystrixRequestLog)、批處理(HystrixCollapser)時需要初始化HystrixRequestContext,並按以下 try...finally 格式調用:

    @Test
    public void testCache() {
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        try {
            CommandGetUserByIdFromUserService command1 = new CommandGetUserByIdFromUserService("1");
            command1.execute();
            // 第一次調用時緩存裏沒有
            assertFalse(command1.isResponseFromCache());
            
            CommandGetUserByIdFromUserService command2 = new CommandGetUserByIdFromUserService("1");
            command2.execute();
            // 第二次調用直接從緩存拿結果
            assertTrue(command2.isResponseFromCache());
        } finally {
            context.shutdown();
        }
        // zzs001
    }

保護器是否開啓

接着,Hystrix 會判斷保護器是否開啓。

這裏我在 command 的 run 方法中手動製造 fail 或 time out。另外,我們可以通過 HystrixCommandProperties 調整保護器開啓的閾值。

    public CommandGetUserByIdFromUserService(String userId) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserService")) // 相同command group共用一個ThreadPool
                .andCommandKey(HystrixCommandKey.Factory.asKey("UserService_GetUserById"))// 相同command key共用一個CircuitBreaker、requestCache
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        .withCircuitBreakerRequestVolumeThreshold(10)
                        .withCircuitBreakerErrorThresholdPercentage(50)
                        .withMetricsHealthSnapshotIntervalInMilliseconds(1000)
                        .withExecutionTimeoutInMilliseconds(1000)
                        ));
        this.userId = userId;
    }
    @Override
    protected DataResponse<User> run() {
        LOG.info("執行最終任務,線程爲:{}", Thread.currentThread());
        // 手動製造超時
        /*try {
            Thread.sleep(1200);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }*/
        // 手動製造異常
        throw new RuntimeException("");
        //return UserService.instance().getUserById(userId);
    }

這個時候,當調用失敗達到一定閾值後,保護器被觸發開啓,後續的請求都會直接走 fall back。

    @Test
    public void testCircuitBreaker() {
        CommandGetUserByIdFromUserService command;
        int count = 1;
        do {
            command = new CommandGetUserByIdFromUserService("1");
            command.execute();
            count++;
        } while(!command.isCircuitBreakerOpen());
        LOG.info("調用{}次之後,斷路器開啓", count);
        
        // 這個時候再去調用,會直接走fall back
        command = new CommandGetUserByIdFromUserService("1");
        command.execute();
        assertTrue(command.isCircuitBreakerOpen());
    }

連接池、隊列或信號量是否耗盡

即使保護器是關閉狀態,我們也不能馬上調用依賴,需要先檢查連接池或信號量是否耗盡(通過 HystrixCommandProperties 可以配置使用線程池還是信號量)。

因爲默認的線程池比較大,所以,這裏我通過 HystrixThreadPoolProperties 調小了線程池。

    public CommandGetUserByIdFromUserService(String userId) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserService")) // 相同command group共用一個ThreadPool
                .andCommandKey(HystrixCommandKey.Factory.asKey("UserService_GetUserById"))// 相同command key共用一個CircuitBreaker、requestCache
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
                        .withCoreSize(2)
                        .withMaxQueueSize(5)
                        .withQueueSizeRejectionThreshold(5)
                        ));
        this.userId = userId;
    }

這個時候,當線程池耗盡後,後續的請求都會直接走 fall back,而保護器並沒有開啓。

    @Test
    public void testThreadPoolFull() throws InterruptedException {
        
        int maxRequest = 100;
        
        int i = 0;
        do {
            CommandGetUserByIdFromUserService command = new CommandGetUserByIdFromUserService("1");
            command.toObservable().subscribe(v -> LOG.info("non-blocking command.toObservable():{}", v));
            LOG.info("是否線程池、隊列或信號量耗盡:{}", command.isResponseRejected());
            
        } while(i++ < maxRequest - 1);
        
        
        // 這個時候再去調用,會直接走fall back
        CommandGetUserByIdFromUserService command = new CommandGetUserByIdFromUserService("1");
        command.execute();
        // 線程池、隊列或信號量耗盡
        assertTrue(command.isResponseRejected());
        assertFalse(command.isCircuitBreakerOpen());
        
        Thread.sleep(10000);
        // zzs001
    }

結語

以上簡單地講完了 Hystrix。閱讀官方的 wiki,再結合上面的幾個例子,相信大家可以對 Hystrix 有較深的瞭解。

最後,感謝閱讀,歡迎私信交流。

參考資料

Home · Netflix/Hystrix Wiki · GitHub

相關源碼請移步:https://github.com/ZhangZiSheng001/hystrix-demo

本文爲原創文章,轉載請附上原文出處鏈接:https://www.cnblogs.com/ZhangZiSheng001/p/15567420.html

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