Hystrix 實戰經驗分享

一、背景

Hystrix是Netlifx開源的一款容錯框架,防雪崩利器,具備服務降級,服務熔斷,依賴隔離,監控(Hystrix Dashboard)等功能。

儘管說Hystrix官方已不再維護,且有Alibaba Sentinel等新框架選擇,但從組件成熟度和應用案例等方面看,其實還是有很多項目在繼續使用Hystrix中,本人所參與的項目就是其一。故結合個人的Hystrix實戰經驗與大家分享交流。

二、經驗總結

2.1 隔離策略的選擇

Hystrix提供兩種資源隔離策略,線程池和信號量。它們之間的異同點如下:

而在使用緩存(本地內存緩存更適合該場景,Redis等網絡緩存需要評估)時,我們可以使用信號量隔離策略,因爲這類服務響應快,不會佔用容器線程太長時間,而且也減少了線程切換的一些開銷,提高了服務效率。

具體使用哪種策略,需根據業務場景綜合評估。一般情況下,推薦使用線程池隔離。

2.2 線程池大小與超時時間設置

在線程池隔離策略下,線程池大小及超時時間的設置至關重要,直接影響着系統服務的響應能力。如線程池大小若設置的太大會造成資源浪費及線程切換等開銷;若設置的太小又支撐不了用戶請求,造成請求排隊。而超時時間設置的太長會出現部分長耗時請求阻塞線程,造成其它正常請求排隊等待;若設置的太短又會造成太多正常請求被熔斷。

對此Hystrix官方給的建議如圖:

即轉換爲以下計算公式:

  • 線程池大小 = 服務TP99響應時長(單位秒) * 每秒請求量 + 冗餘緩衝值

  • 超時時間(單位毫秒) = 1000(毫秒) / 每秒請求量

例如某服務TP99情況下每秒鐘會接收30個請求,然後每個請求的響應時長是200ms,按如上公式計算可得:線程池大小 = 0.2 * 30 + 4(冗餘緩衝值)= 10,超時時間 = 300ms

2.3 註解疊加

在實際開發中可能會遇到某外部調用方法有Hystrix註解與其它註解一起使用的情況,例如查詢方法加上緩存註解。此時需特別注意註解間的執行順序,避免出現非預期的結果:

  • 緩存註解未生效

此時Hystrix註解切面的執行是在最外層,由於Hystrix內部執行是通過ProceedingJoinPoint.getTarget()獲取目標對象,使用反射調用的方式直接執行到目標對象方法上,從而造成中間其它註解邏輯丟失。可通過指定註解執行順序@Order解決保證Hystrix註解執行在最裏層。

  • 因緩存異常造成該查詢方法被熔斷

如果Hystrix註解切面的執行是在最外層,此時Hystrix熔斷管理的方法邏輯除了第三方服務遠程調用,也包括了緩存調用邏輯。如果緩存調用出現異常就會算作整個方法異常,從而引起整個方法被熔斷。

2.4 服務的異常處理

先給大家時間看如下代碼,檢查是否存在問題:

@HystrixCommand(fallbackMethod="queryUserByIdFallback")
public User queryUserById(String userId) {
  if(StringUtils.isEmpty(userId)) {
    throw new BizException("參數不合法");
  }
  
  Result<User> result;
  try {
    result = userFacade.queryById(userId);
  } catch(Exception e) {
    log.error("query user error. id={}", id, e);
  }
  
  if(result != null && result.isSuccess()) {
    return result.getData();
  }
  
  return null;
}

Hystrix在運行過程中會根據調用請求的成功率或失敗率信息來確定每個依賴命令的熔斷器是否打開。如果打開,後續的請求都會被拒絕。由此可見,對異常的控制是Hystrix運行效果起很大影響。

再回頭看上面的例子,會發現兩個異常處理問題:

  • 參數校驗不通過時的異常處理

非法參數校驗等非系統調用的異常失敗不應該影響熔斷邏輯,不應該算作失敗統計範圍內。對此優化建議是將參數校驗放到遠程調用封裝方法的外面,或者封裝成HystrixBadRequestException進行拋出。因爲在Hystrix內部邏輯中HystrixBadRequestException異常已默認爲不算作失敗統計範圍內。

  • try-catch遠程調用的異常處理

對遠程服務的直接調用進行try-catch會把異常直接“吞掉”,會直接造成Hystrix獲取不到網絡異常等服務不可用異常。建議在catch日誌記錄處理後將異常再throw出來。

2.5  fallback方法

Hystrix在依賴服務調用時通過增加fallback方法返回默認值的方式來支持服務優雅降級。但fallback的使用也有很多需要注意的地方,大致總結如下:

  1. fallback 方法訪問級別、參數等要與對應依賴服務一致

  2. fallback 方法中執行的邏輯儘量輕量,如用本地緩存或靜態默認值,避免遠程調用

  3. 如果fallback方法裏有遠程調用,建議也使用Hystrix包裝起來,且保證與主命令線程池的隔離

  4. 對於寫操作的遠程調用不建議使用fallback降級

2.6  groupKey、commandKey、threadPoolKey

在使用Hystrix開發中肯定都見過這三個key,但很多人並不理解這三個key的意義以及對Hystrix的作用,尤其是threadPooKey,故在此總結下:

groupKey

通過group key可以對命令方法進行分組,便於Hystrix數據統計、告警及dashboad展示。一般會根據遠程服務的業務類型進行區分,如賬戶服務定義一個group key,訂單服務定義另一個group key。

默認值是@HystrixCommand註解標註的方法所在的類名。

commandKey

具體命令方法的標識名稱,常用於對該命令進行動態參數設置。

默認值是@HystrixCommand註解標註的方法名。

threadPoolKey

用於標識命令所歸屬的線程池,具有相同threadPoolKey的命令使用同一個線程池。

若該key不指定,默認值就是groupKey,即@HystrixCommand註解標註的方法所在的類名。

在實際項目中,我們會建議儘量通過threadPoolKey來指定線程池, 而不是通過groupKey的默認方式劃分, 因爲會存在某個命令需要跟同組其他命令進行線程隔離的場景,以避免互相影響。

2.7 參數優先級

Hystrix默認提供4個級別的參數值配置方式:

全局默認值(Default Value)

Hystrix自身代碼默認值,寫死在源碼中的值,使用方不配置任何參數情況下生效。

例:execution.isolation.thread.timeoutInMilliseconds超時時間全局默認值是1000,單位毫秒

動態全局默認參數(Default Property)

此類配置參數可變更全局默認值。

例:通過屬性名hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds設置的超時時間值

實例初始值(Instant Value)

熔斷器實例初始值,配置此類參數後,不再使用默認值。即寫在代碼註解中的屬性值。

例:@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")

動態實例參數(Instant Property)

可動態調整一個熔斷器實例的參數值

例:通過屬性名hystrix.command.HystrixCommandKey.execution.isolation.thread.timeoutInMilliseconds設置的超時時間值

優先級關係:

動態實例參數(Instance Property) > 實例初始值 > 動態全局默認參數(Default Property) > 全局默認值(Default Value)

2.8  基於配置中心實現參數動態配置

Hystrix默認使用Archaius實現動態設置,而Archaius默認會加載classpath下的config.properties文件,可通過在配置文件中加入對應屬性key-value實現動態控制Hystrix行爲。在分佈式項目中使用配置中心進行統一配置管理是標配,因此需要基於配置中心的擴展實現Hystrix參數動態配置功能。

通過跟蹤HystrixCommand的創建,發現hystrix最終通過HystrixDynamicProperties實現類根據參數屬性名獲取值,而Hystrix本身提供了HystrixDynamicProperties類的擴展機制,見HystrixPlugins類367行代碼,可知Hystrix提供四種擴展方法:

  1. 通過系統參數

  2. 基於Java SPI機制

  3. Archaius動態屬性擴展實現類(默認)

  4. Hystrix內置基於System.getProperty的HystrixDynamicProperties實現;

2.8.1 基於Java SPI機制

基於spi機制的擴展實現依賴兩個類分別是HystrixDynamicProperties與HystrixDynamicProperty,其中HystrixDynamicProperties類是需要實現的Hystrix動態屬性擴展spi接口,提供了多個獲取動態屬性的方法,接口定義如下:

public interface HystrixDynamicProperties {
    
    /**
     * Requests a property that may or may not actually exist.
     * @param name property name, never <code>null</code>
     * @param fallback default value, maybe <code>null</code>
     * @return never <code>null</code>
     */
    public HystrixDynamicProperty<String> getString(String name, String fallback);
    /**
     * Requests a property that may or may not actually exist.
     * @param name property name, never <code>null</code>
     * @param fallback default value, maybe <code>null</code>
     * @return never <code>null</code>
     */
    public HystrixDynamicProperty<Integer> getInteger(String name, Integer fallback);
    /**
     * Requests a property that may or may not actually exist.
     * @param name property name, never <code>null</code>
     * @param fallback default value, maybe <code>null</code>
     * @return never <code>null</code>
     */
    public HystrixDynamicProperty<Long> getLong(String name, Long fallback);
    /**
     * Requests a property that may or may not actually exist.
     * @param name property name
     * @param fallback default value
     * @return never <code>null</code>
     */
    public HystrixDynamicProperty<Boolean> getBoolean(String name, Boolean fallback);
}


而HystrixDynamicProperty類具體表示一個參數屬性,且有動態變更的能力,接口定義如下:

public interface HystrixDynamicProperty<T> extends HystrixProperty<T>{
    
    public String getName();
    
    /**
     * Register a callback to be run if the property is updated.
     * @param callback callback.
     */
    public void addCallback(Runnable callback);
    
}

其中addCallback方法是實現屬性動態變更的核心所在,如其註釋說明的那樣,它會在屬性變更時註冊callback回調方法進行屬性動態刷新。而這塊動態刷新邏輯是Hystrix內部已實現的,對於我們只需要自定義擴展時將callback保存,然後在配置中心變更時觸發對應屬性對象的callback方法即可。

實現步驟如下:

1、定義HystrixDynamicProperty實現類

完成動態屬性類的自定義實現,包括String/Integer/Long/Boolean四種類型動態屬性態實現。

如上面HystrixDynamicProperty類描述中說的那樣,需要對callback進行保存,並在在收到配置中心屬性變更時觸發這些屬性的callback方法,來實現屬性的動態變更。這塊邏輯可以參照觀察者模式進行設計實現。

代碼如下:

private abstract static class CustomDynamicProperty<T> implements HystrixDynamicProperty<T>, PropertyObserver {
        protected final String name;
        protected final T defaultValue;
        protected List<Runnable> callbacks;

        protected CustomDynamicProperty(String propName, T defaultValue) {
            this.name = propName;
            this.defaultValue = defaultValue;

            PropertyObserverManager.add(this);
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public void addCallback(Runnable callback) {
            if (callbacks == null)
                callbacks = new ArrayList<>(1);
            this.callbacks.add(callback);
        }

        @Override
        public String keyName() {
            return name;
        }

        @Override
        public void update(PropertyItem item) {
            if(getName().equals(item.getName())) {
                for(Runnable r : callbacks) {
                    r.run();
                }
            }
        }
    }

    private static class StringDynamicProperty extends CustomDynamicProperty<String> {
        protected StringDynamicProperty(String propName, String defaultValue) {
            super(propName, defaultValue);
        }

        @Override
        public String get() {
            return ConfigManager.getString(name, defaultValue);
        }
    }

    private static class IntegerDynamicProperty extends CustomDynamicProperty<Integer> {
        protected IntegerDynamicProperty(String propName, Integer defaultValue) {
            super(propName, defaultValue);
        }

        @Override
        public Integer get() {
            String configValue =  ConfigManager.get(name);
            if(StringUtils.isNotEmpty(configValue)) {
                return Integer.valueOf(configValue);
            }
            return defaultValue;
        }
    }

    private static class LongDynamicProperty extends CustomDynamicProperty<Long> {
        protected LongDynamicProperty(String propName, Long defaultValue) {
            super(propName, defaultValue);
        }

        @Override
        public Long get() {
            String configValue =  ConfigManager.get(name);
            if(StringUtils.isNotEmpty(configValue)) {
                return Long.valueOf(configValue);
            }
            return defaultValue;
        }
    }

    private static class BooleanDynamicProperty extends CustomDynamicProperty<Boolean> {
        protected BooleanDynamicProperty(String propName, Boolean defaultValue) {
            super(propName, defaultValue);
        }

        @Override
        public Boolean get() {
            String configValue =  ConfigManager.get(name);
            if(StringUtils.isNotEmpty(configValue)) {
                return Boolean.valueOf(configValue);
            }
            return defaultValue;
        }
    }

其中ConfigManager類暫時默認爲配置中心配置管理類,提供參數獲取與參數監聽器等功能。而PropertyObserver類(keyName/update方法屬於其定義)、PropertyObserverManager類就是參照觀察者模式定義實現的,負責觀察者的註冊與通知管理,來完成動態屬性與配置中心變更通知間的聯動。這兩個類實現比較簡單就不展示描述。

2、定義HystrixDynamicProperties實現類

基於第1步定義的HystrixDynamicProperty擴展類完成HystrixDynamicProperties的自定義。代碼如下:


public class DemoHystrixDynamicProperties implements HystrixDynamicProperties {
    @Override
    public HystrixDynamicProperty<String> getString(String name, String fallback) {
        return new StringDynamicProperty(name, fallback);
    }

    @Override
    public HystrixDynamicProperty<Integer> getInteger(String name, Integer fallback) {
        return new IntegerDynamicProperty(name, fallback);
    }

    @Override
    public HystrixDynamicProperty<Long> getLong(String name, Long fallback) {
        return new LongDynamicProperty(name, fallback);
    }

    @Override
    public HystrixDynamicProperty<Boolean> getBoolean(String name, Boolean fallback) {
        return new BooleanDynamicProperty(name, fallback);
    }
}

3、註冊SPI實現類

在META-INF/services/添加名爲com.netflix.hystrix.strategy.properties.HystrixDynamicProperties的文本文件,內容爲第2步HystrixDynamicProperties自定義實現類全路徑名。

2.8.2 基於默認Archaius進行擴展

Hystrix默認通過Archaius實現參數動態獲取,而Archaius自身也提供自定義的參數獲取方式,分別是 PolledConfigurationSource接口 和AbstractPollingScheduler類,其中PolledConfigurationSource接口表示配置獲取源,AbstractPollingScheduler類表示配置定時刷新機制。

實現步驟如下:

1、創建配置獲取源:

public class CustomCfgConfigurationSource implements PolledConfigurationSource {
    private final static String CONFIG_KEY_PREFIX = "hystrix";
 
    @Override
    public PollResult poll(boolean initial, Object checkPoint) throws Exception {
        Map<String, Object> map = load();
        return PollResult.createFull(map);
    }
 
    private Map<String, Object> load() throws Exception{
        Map<String, Object> map = new HashMap<>();
 
        Set<String> keys = ConfigManager.keys();
        for(String key : keys) {
            if(key.startsWith(CONFIG_KEY_PREFIX)) {
                map.put(key, ConfigManager.get(key));
            }
        }
 
        return map;
    }
}

其實現非常簡單,核心實現就是poll方法,遍歷配置中心中所有hystrix開頭的配置參數並返回保存。

2、定義配置刷新方式:


public class CustomCfgPollingScheduler extends AbstractPollingScheduler {
    private final static Logger logger = LoggerFactory.getLogger("CustomCfgPollingScheduler");
 
    private final static String CONFIG_KEY_PREFIX = "hystrix";
 
    @Override
    public void startPolling(PolledConfigurationSource source, final Configuration config) {
        super.startPolling(source, config);
        //
        ConfigManager.addListener(new ConfigListener() {
            @Override
            public void eventReceived(PropertyItem item, ChangeEventType type) {
                String name = item.getName();
                if(name.startsWith(CONFIG_KEY_PREFIX)) {
                    String newValue = item.getValue();
                    //新增&修改
                    if(ChangeEventType.ITEM_ADDED.equals(type) || ChangeEventType.ITEM_UPDATED.equals(type)) {
                        addOrChangeProperty(name, newValue, config);
                    }
                    //刪除
                    else if(ChangeEventType.ITEM_REMOVED.equals(type)) {
                        deleteProperty(name, config);
                    }
                    else {
                        logger.error("error config change event type {}.", type);
                    }
                }
            }
        });
    }
 
    private void addOrChangeProperty(String name, Object newValue, final Configuration config) {
        if (!config.containsKey(name)) {
            config.addProperty(name, newValue);
        } else {
            Object oldValue = config.getProperty(name);
            if (newValue != null) {
                if (!newValue.equals(oldValue)) {
                    config.setProperty(name, newValue);
                }
            } else if (oldValue != null) {
                config.setProperty(name, null);
            }
        }
    }
 
    private void deleteProperty(String key, final Configuration config) {
        if (config.containsKey(key)) {
            config.clearProperty(key);
        }
    }
 
    @Override
    protected void schedule(Runnable pollingRunnable) {
        //IGNORE OPERATION
    }
 
    @Override
    public void stop() {
        //IGNORE OPERATION
    }
}

但對應實際項目,通過定時刷新的方式一是不太實時,二是每次都得全量檢查配置中心是否有修改,邏輯複雜,所以此處改用 ConfigManager.addListener 增加配置中心監聽來實現。

3、定義並初始化自動配置:

DynamicConfiguration dynamicConfiguration = new DynamicConfiguration(new CustomCfgConfigurationSource(), new CustomCfgPollingScheduler());
ConfigurationManager.install(dynamicConfiguration);

最後只需要在容器啓動時執行以上初始化腳本即可。

細心的同學可能發現上面步驟中第3步,最終“安裝”install到Hystrix配置管理類中的是 DynamicConfiguration類實現,且第2步的定時刷新類也比較雞肋,就想着能否繼續簡化上面方案,只需要實現一個自定義的"DynamicConfiguration"就包含配置源獲取與監聽配置修改功能,實現如下:

public class CustomCfgDynamicConfiguration extends ConcurrentMapConfiguration {
    private final static Logger logger = LoggerFactory.getLogger("CustomCfgDynamicConfiguration");
 
    private final static String CONFIG_KEY_PREFIX = "hystrix";
 
    public CustomCfgDynamicConfiguration() {
        super();
        load();
        initEvent();
    }
 
    /**
     * 從配置中心全量加載Hystrix配置參數信息
     */
    private void load() {
        Set<String> keys = ConfigManager.keys();
        for(String key : keys) {
            if(key.startsWith(CONFIG_KEY_PREFIX)) {
                map.put(key, ConfigManager.get(key));
            }
        }
    }
 
    /**
     * 通過配置中心監聽事件回調處理,針對Hystrix配置參數變更進行同步
     */
    private void initEvent() {
        ConfigManager.addListener(new ConfigListener() {
            @Override
            public void eventReceived(PropertyItem item, ChangeEventType type) {
                String name = item.getName();
                if(name.startsWith(CONFIG_KEY_PREFIX)) {
                    String newValue = item.getValue();
                    //新增&修改
                    if(ChangeEventType.ITEM_ADDED.equals(type) || ChangeEventType.ITEM_UPDATED.equals(type)) {
                        addOrChangeProperty(name, newValue);
                    }
                    //刪除
                    else if(ChangeEventType.ITEM_REMOVED.equals(type)) {
                        deleteProperty(name);
                    }
                    else {
                        logger.error("error config change event type {}.", type);
                    }
                }
            }
        });
    }
 
    /**
     * 新增或修改參數值
     * @param name
     * @param newValue
     */
    private void addOrChangeProperty(String name, Object newValue) {
        if (!this.containsKey(name)) {
            this.addProperty(name, newValue);
        } else {
            Object oldValue = this.getProperty(name);
            if (newValue != null) {
                if (!newValue.equals(oldValue)) {
                    this.setProperty(name, newValue);
                }
            } else if (oldValue != null) {
                this.setProperty(name, null);
            }
        }
    }
 
    /**
     * 刪除參數值
     * @param key
     */
    private void deleteProperty(String key) {
        if (this.containsKey(key)) {
            this.clearProperty(key);
        }
    }
}

最後通過 ConfigurationManager.install(new CustomCfgDynamicConfiguration());“安裝”該實現即可。

三、寫在最後

筆者結合項目實戰對Hystrix使用進行總結分享,有關於隔離策略、線程池設置、參數優先級等知識點講解,也有關於註解疊加、異常處理、參數動態配置等具體問題解決方案,希望對大家有所幫助。

作者:vivo 官網商城開發團隊

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