Hystrix 在不引入 Archaius 的前提下實現(xiàn)動態(tài)配置更新

Hystrix 簡介

Hystrix 是 Netflix 開源的一個限流熔斷降級組件,防止依賴服務(wù)發(fā)生錯誤后,將調(diào)用方的服務(wù)拖垮。這里對 Hystrix 本身不做過多介紹。

Hystrix 目前處于維護(hù)狀態(tài)(不再更新),但是還有大量項目對它進(jìn)行了使用,因此仍然非常重要。

基本用法

在 Hystrix 中,HystrixCommand 是非常重要的一個類,用于對目標(biāo)服務(wù)進(jìn)行保護(hù)。

在 Hystrix 的基本用法中:

  • 首先,我們需要創(chuàng)建一個自定義類 CustomHystrixCommand 來繼承 Hystrix 提供的 HystrixCommand 類。
  • 然后,每次調(diào)用服務(wù)的時候,我們需要創(chuàng)建一個 CustomHystrixCommand 實例,將調(diào)用的邏輯封裝在該 Command 之內(nèi),然后 Hystrix 就會幫我們根據(jù)配置,自動對系統(tǒng)進(jìn)行保護(hù),在適當(dāng)?shù)臅r候進(jìn)行限流、熔斷降級的操作。

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

首先,創(chuàng)建自定義類 CommandHelloWorld

public class CommandHelloWorld extends HystrixCommand<String> {

    private final String name;

    public CommandHelloWorld(String name) {
        // 這里是當(dāng)前 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 + "!";
    }
}

如果需要調(diào)用,則直接執(zhí)行

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

就可以了。

如何使用配置

作為熔斷和降級的組件,Hystrix 當(dāng)然必須提供一些配置,例如:在多少秒內(nèi)服務(wù)異常次數(shù)超過多少次時,會觸發(fā)熔斷,以及熔斷多少秒后,重新嘗試請求服務(wù),等等。

這些配置決定了 Hystrix 該如何工作,我們可以在 HystrixCommand 的構(gòu)造過程中,對這些配置進(jìn)行修改(當(dāng)然,不修改的話,也是有默認(rèn)配置的)。為了達(dá)成以上目的,Hystrix 需要在服務(wù)的維度上,記錄時間、請求數(shù)量、是否錯誤等信息,從而使自己有足夠的信息來判斷是否應(yīng)該熔斷。

上面提到服務(wù)的維度,那么 Hystrix 是如何區(qū)分服務(wù)的呢?

在 Hystrix 中有兩個用于對服務(wù)進(jìn)行命令和區(qū)分的配置:Group key 和 Command key,分別可以理解為組關(guān)鍵字和命令關(guān)鍵字,一個組關(guān)鍵字中可以有多個命令關(guān)鍵字。

例如,可以將上面 CommandHello 中的構(gòu)造函數(shù)修改為:

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

其中 Setter 中就指定了當(dāng)前命令的組關(guān)鍵字和命令關(guān)鍵字。

當(dāng)然,上面提到的熔斷等,也是可以在這個 Setter 里面進(jìn)行配置的。

    public CommandHelloWorld(String name) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"))
                .andCommandPropertiesDefaults(propSetter)); // 這里配置熔斷等的信息
        this.name = name;
    }

這里的 propSetter 是一個 HystrixCommandProperties.Setter 類的字段,其中包含了熔斷等的配置,這里就不過多展開了,感興趣的可以直接看這個類的源碼。

動態(tài)配置更新的問題

什么是動態(tài)配置更新?即在應(yīng)用程序運行時,對熔斷等配置進(jìn)行動態(tài)修改,并使得修改可以立即生效。

舉個例子,對于一個服務(wù),我本來配置的是 60s 內(nèi)有 3 次請求出錯就熔斷,現(xiàn)在我想改成 20s 內(nèi)有 5 次請求出錯才熔斷,并且需要該修改立即生效,那么 Hystrix 可以做到嗎?

首先給出結(jié)論:Hystrix 可以做到。按照官方的說法,Hystrix 支持使用 Archaius 進(jìn)行動態(tài)配置,詳情可見官方文檔 https://github.com/Netflix/Hystrix/wiki/Configuration#intro。

但是,這種用法需要 Archaius 進(jìn)行配合,如果在生產(chǎn)環(huán)境中使用,那你又要引入一個新的依賴組件 Archauis,未免有點得不償失了。那么我們有沒有辦法在不引入任何新的組件前提下,從代碼的角度上實現(xiàn)配置動態(tài)更新呢?

答案是可以的,只是需要一些騷操作。僅僅修改 HystrixCommand 構(gòu)造時 Setter 里面的 CommandPropertiesDefaults 里的熔斷配置,是沒有用的!

那么為啥僅修改以上配置沒用呢?這里涉及了 Hystrix 里兩個地方,使用了緩存,導(dǎo)致動態(tài)修改無法生效,仍然會使用緩存的值(也就是修改之前的值)。這兩個緩存分別為:

  • HystrixPropertiesFactory 里面的 commandProperties 字段,這里存儲了HystrixCommand 的基本屬性。
  • HystrixCircuitBreaker$Factory 里面的 circuitBreakersByCommand 字段,這里存儲了 HystrixCommand 的熔斷器(用于判斷何時熔斷以及打開/關(guān)閉熔斷)。

這兩個字段都是 ConcurrentHashMap 類型,其 key 為 HystrixCommand 的 commandKey。我們知道 commandKey 一般會設(shè)置為服務(wù)名稱,那么也就是說:對于同一個服務(wù),即使修改了其熔斷配置,仍然會因為緩存原因,使用修改之前的配置以及熔斷器,那么動態(tài)更新就無法生效了。

如何解決

找到了緩存的位置,也就找到了動態(tài)配置更新不生效的根本原因,接下來去解決就好了,解決思路很直接:當(dāng)檢測到配置發(fā)生改變時,主動刪掉緩存 Map 中的相關(guān)項。

但是,Hystrix 似乎并沒有考慮動態(tài)配置更新這一需求,以上兩個緩存使用的 Map,都是靜態(tài)私有字段,我們在外部理論上是不能獲取并修改它們的。。

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

例如,對于 HystrixCircuitBreaker$Factory 里的 circuitBreakersByCommand ,可以使用以下方式進(jìn)行緩存項清除:

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 就是待更新的服務(wù)名
    circuitBreakersByCommand.remove(commandKey);
} catch (NoSuchFieldException | IllegalAccessException e) {
    log.error("Remove cache in HystrixCircuitBreaker.Factory failed, commandKey: {}", commandKey, e);
}

至于另一個緩存項,也是同樣的方法,這里就不贅述了。

總之,注意以上兩個地方的緩存,在需要動態(tài)配置更新時,手動將以上兩個地方的緩存清除掉,就可以使得 Hystrix 輕輕松松具備動態(tài)配置更新的能力了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容