Hystrix微服務容錯處理及回撥方法原始碼分析
阿新 • • 發佈:2020-06-02
## 前言
在 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 微服務架構實戰》 --- 周立
碼字不易,如果你覺得讀完以後有收穫,不妨點個推薦讓更多的人看