Hystrix微服務容錯處理及回撥方法原始碼分析

後青春期的Keats發表於2020-06-02

前言

在 SpringCloud 微服務專案中,我們有了 Eureka 做服務的註冊中心,進行服務的註冊於發現和服務治理。使得我們可以摒棄硬編碼式的 ip:埠 + 對映路徑 來傳送請求。我們有了 Feign 作為宣告式服務呼叫元件,可以像呼叫本地服務一樣來呼叫遠端服務。基於 Ribbon 我們又實現了客戶端負載均衡,輕鬆的在叢集環境下選取合適的服務提供者。這樣看來我們的微服務貌似很完善了。是這樣的嗎?

並非如此,想想我們在編碼過程中進行的健壯性檢查。類比一下服務與服務呼叫是否也應該更加健壯一些呢?我們目前的微服務在正常執行的時候是沒有問題的,但若是某個偏下游的服務提供者不可用,造成服務積壓,接連引起上游的服務消費者當機,引法雪崩效應。是不是就顯得我們的微服務不堪一擊呢?因此我們需要一個元件來解決這樣的問題,前輩們參考生活中保險絲的原理做出了微服務中的保險絲-Hystrix熔斷器。下面讓我們來一起使用一下

宣告:本文首發於部落格園,作者:後青春期的Keats;地址:https://www.cnblogs.com/keatsCoder/ 轉載請註明,謝謝!

Hystrix簡介

Hystrix主要實現了下面的功能:

  • 包裹請求:使用 HystrixCommand(或 HystrixObservableCommand) 包裹對依賴的呼叫邏輯。每個命令在獨立的執行緒中執行,使用了設計模式中的‘命令模式’
  • 跳閘機制:當某微服務的錯誤率超過一定閾值時,可以自動跳閘,停止請求該服務一段時間
  • 資源隔離:Hystrix 為每個微服務都維護了一個小型的執行緒池(或訊號量)如果該執行緒池已滿,發往該依賴的請求就會被立即拒絕
  • 監控:Hystrix 可以近乎實時的監控執行指標和配置的變化,例如成功、失敗、超時和被拒絕的請求等
  • 回退機制:當請求成功、失敗、超時和被拒絕或者斷路器開啟時,執行回退邏輯。回退邏輯可由開發人員自行提供
  • 自我修復:斷路器開啟一段時間後,會進入‘半開’狀態,允許一個請求訪問服務提供方,如果成功。則關閉斷路器

使用 Hystrix

引入依賴

        <!-- 熔斷器 hystrix -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

在啟動類上新增 @EnableHystrix

兩種情況下的回退方法

非 Feign 呼叫下的回退方法

編寫回退方法
/**
 * getUserByAge 方法 Hystrix 回退方法
 * @param age
 * @return
 */
public User getUserByAgeFallBack(Integer age){
    User user = new User();
    user.setName("預設使用者");
    user.setAge(age);
    return user;
}
在客戶端的方法上宣告
@HystrixCommand(fallbackMethod = "getUserByAgeFallBack")

測試:將服務提供方的程式碼打斷點。呼叫服務消費方,會發現返回了預設使用者

需要注意:

  1. 回退方法的返回值型別需要和原來方法返回值型別相同(否則會報 FallbackDefinitionException: Incompatible return types)
  2. 回退方法的引數列表也要和原來方法相同(否則會報 FallbackDefinitionException: fallback method wasn't found: getUserByAgeFallBack([class java.lang.Integer]))
  3. 當我寫下第二句時,發現書中下一節介紹說可以通過在回退方法中新增第二個引數:ThrowEable 來捕獲異常,分析呼叫失敗的原因,我就知道我錯了。為了避免繼續得到錯誤的結論,我決定讀一讀 Hystrix 處理回退方法的原始碼
加點料:Hystrix 對回退方法的封裝的原始碼如下:
com.netflix.hystrix.contrib.javanica.utils.MethodProvider
public FallbackMethod find(Class<?> enclosingType, Method commandMethod, boolean extended) {
	// 首先判斷該方法的 HystrixCommand 註解上有沒有 defaultFallback / fallbackMethod 配置回退方法名稱
    if (this.canHandle(enclosingType, commandMethod)) {
    	// 呼叫 doFind 方法
        return this.doFind(enclosingType, commandMethod, extended);
    } else {
    	// 沒有配置的化就接著下一個判斷
        return this.next != null ? this.next.find(enclosingType, commandMethod, extended) : FallbackMethod.ABSENT;
    }
}

find 方法在使用者所請求的方法的 HystrixCommand 註解上有用 defaultFallback / fallbackMethod 配置回退方法名稱的時候,會呼叫 doFind 方法來尋找回退方法。該方法的引數有兩個,enclosingType 是使用者所請求的方法的類位元組碼檔案,commandMethod 是使用者所請求的方法

首先通過 this.getFallbackName 獲取回退方法名稱,接著通過獲取 commandMethod 的引數型別們

接著分兩種情況:

  1. 回撥方法繼承於 commandMethod 且最後一個引數型別是 Throwable,則去掉回退方法引數列表中的 Throwable 型別進行匹配
  2. 回撥方法不繼承於 commandMethod ,則存在兩個可能的引數型別列表: fallbackParameterTypes 和 extendedFallbackParameterTypes 前者是 commandMethod 是引數列表,後者是前者 + Throwable。然後兩個都進行匹配。接著使用 Java8 Optional API,按順序選取前者匹配到的方法 / 後者 / 空返回
private FallbackMethod doFind(Class<?> enclosingType, Method commandMethod, boolean extended) {
    String name = this.getFallbackName(enclosingType, commandMethod);
    Class<?>[] fallbackParameterTypes = null;
    if (this.isDefault()) {
        fallbackParameterTypes = new Class[0];
    } else {
        fallbackParameterTypes = commandMethod.getParameterTypes();
    }

    if (extended && fallbackParameterTypes[fallbackParameterTypes.length - 1] == Throwable.class) {
        fallbackParameterTypes = (Class[])ArrayUtils.remove(fallbackParameterTypes, fallbackParameterTypes.length - 1);
    }

    Class<?>[] extendedFallbackParameterTypes = (Class[])Arrays.copyOf(fallbackParameterTypes, fallbackParameterTypes.length + 1);
    extendedFallbackParameterTypes[fallbackParameterTypes.length] = Throwable.class;
    Optional<Method> exFallbackMethod = MethodProvider.getMethod(enclosingType, name, extendedFallbackParameterTypes);
    Optional<Method> fMethod = MethodProvider.getMethod(enclosingType, name, fallbackParameterTypes);
    Method method = (Method)exFallbackMethod.or(fMethod).orNull();
    if (method == null) {
        throw new FallbackDefinitionException("fallback method wasn't found: " + name + "(" + Arrays.toString(fallbackParameterTypes) + ")");
    } else {
        return new FallbackMethod(method, exFallbackMethod.isPresent(), this.isDefault());
    }
}

由原始碼可以得到結論:回退方法要麼引數列表和原始方法相同,要麼加且僅加一個型別為 Throwable 的引數。其他的都不行

Feign 客戶端下的回退方法

  1. 設定:feign.hystrix.enabled: true

  2. Feign 客戶端介面上的 @FeignClient 新增 fallback 屬性,指向回退類

  3. 回退類實現客戶端介面

# feign的配置
feign:
  hystrix:
    enabled: true # 開啟 feign 的 hystrix 支援

注意回退類加上 @Component 介面,避免因為 Spring 容器找不到該類而啟動報錯

// Feign 客戶端介面上的 @FeignClient 新增 fallback 屬性,指向回退類
@FeignClient(name = "SERVICE-PROVIDER", fallback = UserServiceFeignClientFallBack.class)
public interface UserServiceFeignClient {

    @GetMapping("/api/v1/user/{age}")
    User getUser(@PathVariable("age") Integer age);

    /**
     * 使用者列表
     * @return
     */
    @GetMapping("/api/v1/users")
    List<User> getUsers();
}
// 回退類實現客戶端介面
@Component 
public class UserServiceFeignClientFallBack implements UserServiceFeignClient {
    @Override
    public User getUser(Integer age) {
        return null;
    }

    @Override
    public List<User> getUsers() {
        return null;
    }
}

當採用 Feign 客戶端來實現回退的時候,前面的捕捉異常方法就不起作用了,那我們應該如何來處理異常呢?可以使用 @FeignClient 的 fallbackFactory 屬性

@FeignClient(name = "SERVICE-PROVIDER", fallbackFactory = UserServiceFallbackFactory.class)

@Component
@Slf4j
public class UserServiceFallbackFactory implements FallbackFactory<UserServiceFeignClient> {
    @Override
    public UserServiceFeignClient create(Throwable t) {
        // 日誌最好寫在各個 fallback 方法中,而不要直接解除安裝 create方法中
        // 否則引用啟動時就會列印該日誌

        return new UserServiceFeignClient() {
            @Override
            public User getUser(Integer age) {
                log.info("呼叫User服務提供者失敗", t);
                User user = new User();
                user.setName("預設使用者");
                user.setAge(age);
                return user;
            }

            @Override
            public List<User> getUsers() {
                return null;
            }
        };
    }
}

注意: **fallback 和 fallbackFactory 屬性同時存在時,fallback 的優先順序更高。因此開發中如果需要處理異常,只需配置 fallbackFactory 屬性即可 **

避免業務異常走進回退方法

在某些場景下,當發生業務異常時,我們並不想觸發 fallback。例如業務中判斷年齡 age 不能小於 1,否則丟擲異常

if(age < 1){
    throw new KeatsException(ExceptionEnum.NUM_LESS_THAN_MIN);
}

這時 Hystrix 會捕捉到異常然後執行 fallback 方法,我們可以通過下面兩個方法來避免:

  1. 繼承 HystrixBadRequestException 該類繼承自 RunntimeException
  2. 在 @HystrixCommand 新增屬性 ignoreExceptions = {KeatsException.class}

為 Feign 禁用 Hystrix

只要開啟 feign 的 hystrix 支援開關,feign 就會使用斷路器包裹 feign 客戶端的所有方法,但很多場景並不需要這樣。該如何禁用呢?

  • 為指定客戶端禁用。需要藉助 Feign 的自定義配置。首先新增一個自定義配置類,然後配置到 @FeignClient 的 configuration 屬性中
@Configuration
public class FeignDisableHystrixConfiguration {
    @Bean
    @Scope("prototype")
    public Feign.Builder feignBuilder(){
        return Feign.builder();
    }
}

@FeignClient(name = "SERVICE-PROVIDER", configuration = {FeignDisableHystrixConfiguration.class})
  • 全域性禁用: feign.hystrix.enabled: false

本部落格中所有示例程式碼都已上傳至 github倉庫: https://github.com/keatsCoder/cloud-cli

參考文獻:《Spring Cloud與Docker 微服務架構實戰》 --- 周立

碼字不易,如果你覺得讀完以後有收穫,不妨點個推薦讓更多的人看到吧!

相關文章