1. 程式人生 > >Hystrix微服務容錯處理及回撥方法原始碼分析

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

## 前言 在 SpringCloud 微服務專案中,我們有了 Eureka 做服務的註冊中心,進行服務的註冊與發現和服務治理。使得我們可以摒棄硬編碼式的 ip:埠 + 對映路徑 來發送請求。我們有了 Feign 作為宣告式服務呼叫元件,可以像呼叫本地服務一樣來呼叫遠端服務。基於 Ribbon 我們又實現了客戶端負載均衡,輕鬆的在叢集環境下選取合適的服務提供者。這樣看來我們的微服務貌似很完善了。是這樣的嗎? 並非如此,想想我們在編碼過程中進行的健壯性檢查。類比一下服務與服務呼叫是否也應該更加健壯一些呢?我們目前的微服務在正常執行的時候是沒有問題的,但若是某個偏下游的服務提供者不可用,造成服務積壓,接連引起上游的服務消費者宕機,引法雪崩效應。是不是就顯得我們的微服務不堪一擊呢?因此我們需要一個元件來解決這樣的問題,前輩們參考生活中保險絲的原理做出了微服務中的保險絲-Hystrix熔斷器。下面讓我們來一起使用一下 宣告:本文首發於部落格園,作者:後青春期的Keats;地址:https://www.cnblogs.com/keatsCoder/ 轉載請註明,謝謝! ## Hystrix簡介 Hystrix主要實現了下面的功能: - 包裹請求:使用 HystrixCommand(或 HystrixObservableCommand) 包裹對依賴的呼叫邏輯。每個命令在獨立的執行緒中執行,使用了設計模式中的‘命令模式’ - 跳閘機制:當某微服務的錯誤率超過一定閾值時,可以自動跳閘,停止請求該服務一段時間 - 資源隔離:Hystrix 為每個微服務都維護了一個小型的執行緒池(或訊號量)如果該執行緒池已滿,發往該依賴的請求就會被立即拒絕 - 監控:Hystrix 可以近乎實時的監控執行指標和配置的變化,例如成功、失敗、超時和被拒絕的請求等 - 回退機制:當請求成功、失敗、超時和被拒絕或者斷路器開啟時,執行回退邏輯。回退邏輯可由開發人員自行提供 - 自我修復:斷路器開啟一段時間後,會進入‘半開’狀態,允許一個請求訪問服務提供方,如果成功。則關閉斷路器 ## 使用 Hystrix ### 引入依賴 ```xml org.springframework.cloud spring-cloud-starter-netflix-hystrix ``` ### 在啟動類上新增 @EnableHystrix ### 兩種情況下的回退方法 #### 非 Feign 呼叫下的回退方法 ##### 編寫回退方法 ```java /** * getUserByAge 方法 Hystrix 回退方法 * @param age * @return */ public User getUserByAgeFallBack(Integer age){ User user = new User(); user.setName("預設使用者"); user.setAge(age); return user; } ``` ##### 在客戶端的方法上宣告 ```java @HystrixCommand(fallbackMethod = "getUserByAgeFallBack") ``` 測試:將服務提供方的程式碼打斷點。呼叫服務消費方,會發現返回了預設使用者 需要注意: 1. 回退方法的返回值型別需要和原來方法返回值型別相同(否則會報 FallbackDefinitionException: Incompatible return types) 2. 回退方法的引數列表也要和原來方法相同(否則會報 FallbackDefinitionException: fallback method wasn't found: getUserByAgeFallBack([class java.lang.Integer])) 3. **當我寫下第二句時,發現書中下一節介紹說可以通過在回退方法中新增第二個引數:ThrowEable 來捕獲異常,分析呼叫失敗的原因,我就知道我錯了。**為了避免繼續得到錯誤的結論,我決定讀一讀 Hystrix 處理回退方法的原始碼 ##### 加點料:Hystrix 對回退方法的封裝的原始碼如下: ```java 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,按順序選取前者匹配到的方法 / 後者 / 空返回 ```java 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 exFallbackMethod = MethodProvider.getMethod(enclosingType, name, extendedFallbackParameterTypes); Optional 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. 回退類實現客戶端介面 ```yml # feign的配置 feign: hystrix: enabled: true # 開啟 feign 的 hystrix 支援 ``` 注意回退類加上 @Component 介面,避免因為 Spring 容器找不到該類而啟動報錯 ```java // 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 getUsers(); } // 回退類實現客戶端介面 @Component public class UserServiceFeignClientFallBack implements UserServiceFeignClient { @Override public User getUser(Integer age) { return null; } @Override public List getUsers() { return null; } } ``` 當採用 Feign 客戶端來實現回退的時候,前面的捕捉異常方法就不起作用了,那我們應該如何來處理異常呢?可以使用 @FeignClient 的 fallbackFactory 屬性 ```java @FeignClient(name = "SERVICE-PROVIDER", fallbackFactory = UserServiceFallbackFactory.class) @Component @Slf4j public class UserServiceFallbackFactory implements FallbackFactory { @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 getUsers() { return null; } }; } } ``` 注意: **fallback 和 fallbackFactory 屬性同時存在時,fallback 的優先順序更高。因此開發中如果需要處理異常,只需配置 fallbackFactory 屬性即可 ** ### 避免業務異常走進回退方法 在某些場景下,當發生業務異常時,我們並不想觸發 fallback。例如業務中判斷年齡 age 不能小於 1,否則丟擲異常 ```java 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 屬性中 ```java @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 微服務架構實戰》 --- 周立 碼字不易,如果你覺得讀完以後有收穫,不妨點個推薦讓更多的人看