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 輕輕鬆鬆具備動態設定更新的能力了。