Hystrix 如何在不引入 Archaius 的前提下實現動態設定更新

2023-04-24 12:01:39

Hystrix 簡介

Hystrix 是 Netflix 開源的一個限流熔斷降級元件,防止依賴服務發生錯誤後,將呼叫方的服務拖垮。這裡對 Hystrix 本身不做過多介紹。

Hystrix 目前處於維護狀態(不再更新),但是還有大量專案對它進行了使用,因此仍然非常重要。

基本用法

在 Hystrix 中,HystrixCommand 是非常重要的一個類,用於對目標服務進行保護。

在 Hystrix 的基本用法中:

  • 首先,我們需要建立一個自定義類 CustomHystrixCommand 來繼承 Hystrix 提供的 HystrixCommand 類。
  • 然後,每次呼叫服務的時候,我們需要建立一個 CustomHystrixCommand 範例,將呼叫的邏輯封裝在該 Command 之內,然後 Hystrix 就會幫我們根據設定,自動對系統進行保護,在適當的時候進行限流、熔斷降級的操作。

例如,在 Hystrix 官方檔案 中有個很簡單的 Hello World 例子:

首先,建立自定義類 CommandHelloWorld

public class CommandHelloWorld extends HystrixCommand<String> {

    private final String name;

    public CommandHelloWorld(String name) {
    	// 這裡是當前 Command 的設定資訊
        super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
        this.name = name;
    }

    @Override
    protected String run() {
        // a real example would do work like a network call here
        return "Hello " + name + "!";
    }
}

如果需要呼叫,則直接執行

String s = new CommandHelloWorld("World").execute();

就可以了。

如何使用設定

作為熔斷和降級的元件,Hystrix 當然必須提供一些設定,例如:在多少秒內服務異常次數超過多少次時,會觸發熔斷,以及熔斷多少秒後,重新嘗試請求服務,等等。

這些設定決定了 Hystrix 該如何工作,我們可以在 HystrixCommand 的構造過程中,對這些設定進行修改(當然,不修改的話,也是有預設設定的)。為了達成以上目的,Hystrix 需要在服務的維度上,記錄時間、請求數量、是否錯誤等資訊,從而使自己有足夠的資訊來判斷是否應該熔斷。

上面提到服務的維度,那麼 Hystrix 是如何區分服務的呢?

在 Hystrix 中有兩個用於對服務進行命令和區分的設定:Group key 和 Command key,分別可以理解為組關鍵字和命令關鍵字,一個組關鍵字中可以有多個命令關鍵字。

例如,可以將上面 CommandHello 中的建構函式修改為:

    public CommandHelloWorld(String name) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld")));
        this.name = name;
    }

其中 Setter 中就指定了當前命令的組關鍵字和命令關鍵字。

當然,上面提到的熔斷等,也是可以在這個 Setter 裡面進行設定的。

    public CommandHelloWorld(String name) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"))
                .andCommandPropertiesDefaults(propSetter)); // 這裡設定熔斷等的資訊
        this.name = name;
    }

這裡的 propSetter 是一個 HystrixCommandProperties.Setter 類的欄位,其中包含了熔斷等的設定,這裡就不過多展開了,感興趣的可以直接看這個類的原始碼。

動態設定更新的問題

什麼是動態設定更新?即在應用程式執行時,對熔斷等設定進行動態修改,並使得修改可以立即生效。

舉個例子,對於一個服務,我本來設定的是 60s 內有 3 次請求出錯就熔斷,現在我想改成 20s 內有 5 次請求出錯才熔斷,並且需要該修改立即生效,那麼 Hystrix 可以做到嗎?

首先給出結論:Hystrix 可以做到。按照官方的說法,Hystrix 支援使用 Archaius 進行動態設定,詳情可見官方檔案 https://github.com/Netflix/Hystrix/wiki/Configuration

但是,這種用法需要 Archaius 進行配合,如果在生產環境中使用,那你又要引入一個新的依賴元件 Archauis,未免有點得不償失了。那麼我們有沒有辦法在不引入任何新的元件前提下,從程式碼的角度上實現設定動態更新呢?

答案是可以的,只是需要一些騷操作。僅僅修改 HystrixCommand 構造時 Setter 裡面的 CommandPropertiesDefaults 裡的熔斷設定,是沒有用的!

那麼為啥僅修改以上設定沒用呢?這裡涉及了 Hystrix 裡兩個地方,使用了快取,導致動態修改無法生效,仍然會使用快取的值(也就是修改之前的值)。這兩個快取分別為:

  • HystrixPropertiesFactory 裡面的 commandProperties 欄位,這裡儲存了HystrixCommand 的基本屬性。
  • HystrixCircuitBreaker$Factory 裡面的 circuitBreakersByCommand 欄位,這裡儲存了 HystrixCommand 的熔斷器(用於判斷何時熔斷以及開啟/關閉熔斷)。

這兩個欄位都是 ConcurrentHashMap 型別,其 key 為 HystrixCommand 的 commandKey。我們知道 commandKey 一般會設定為服務名稱,那麼也就是說:對於同一個服務,即使修改了其熔斷設定,仍然會因為快取原因,使用修改之前的設定以及熔斷器,那麼動態更新就無法生效了。

如何解決

找到了快取的位置,也就找到了動態設定更新不生效的根本原因,接下來去解決就好了,解決思路很直接:當檢測到設定發生改變時,主動刪掉快取 Map 中的相關項。

但是,Hystrix 似乎並沒有考慮動態設定更新這一需求,以上兩個快取使用的 Map,都是靜態私有欄位,我們在外部理論上是不能獲取並修改它們的。。

如何解決?實際也很簡單,利用反射即可,我們知道利用反射可以存取到類的私有欄位/方法,那麼問題就可以解決了。

例如,對於 HystrixCircuitBreaker$Factory 裡的 circuitBreakersByCommand ,可以使用以下方式進行快取項清除:

try {
    Field field = HystrixCircuitBreaker.Factory.class.getDeclaredField("circuitBreakersByCommand");
    field.setAccessible(true);
    // 由於是 static 欄位,直接 get(null) 即可
    ConcurrentHashMap<String, HystrixCircuitBreaker> circuitBreakersByCommand = (ConcurrentHashMap<String, HystrixCircuitBreaker>) field.get(null);
    // 這裡 commandKey 就是待更新的服務名
    circuitBreakersByCommand.remove(commandKey);
} catch (NoSuchFieldException | IllegalAccessException e) {
    log.error("Remove cache in HystrixCircuitBreaker.Factory failed, commandKey: {}", commandKey, e);
}

至於另一個快取項,也是同樣的方法,這裡就不贅述了。

總之,注意以上兩個地方的快取,在需要動態設定更新時,手動將以上兩個地方的快取清除掉,就可以使得 Hystrix 輕輕鬆鬆具備動態設定更新的能力了。