如何使用原生的Hystrix

雙面神像發表於2021-11-18

什麼是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

相關文章