什麼是Hystrix
前面已經講完了 Feign 和 Ribbon,今天我們來研究 Netflix 團隊開發的另一個類庫--Hystrix。
從抽象層面看,Hystrix 是一個保護器。它可以保護我們的應用不會因為某個依賴的故障而 down 掉。
目前,官方已不再迭代 Hystrix,一方面是認為 Hystrix 已經足夠穩定了,另一方面是轉向了更具彈性的保護器(而不是根據預先配置來啟用保護),例如 resilience4j。當然,停止迭代並不是說 Hystrix 已經沒有價值,它的很多思想仍值得學習和借鑑。
和之前一樣,本文研究的 Hystrix 是原生的,而不是被 Spring 層層封裝的。
Hystrix解決了什麼問題
關於這個問題,官方已經給了詳細的答案(見文末連結的官方 wiki)。這裡我結合著給出自己的一些理解(下面的圖也是借用官方的)。
我們的應用經常需要去呼叫某些依賴。這裡說的依賴,一般是遠端服務,那為什麼不直接說遠端服務呢?因為 Hystrix 適用的場景要更寬泛一些,當我們學完 Hystrix 就會發現,即使是應用裡呼叫的普通方法也可以算是依賴。
呼叫這些依賴,有可能會遇到異常:呼叫失敗或呼叫超時。
先說說呼叫失敗。當某個依賴 down 掉時,我們的應用呼叫它都會失敗。針對這種情況,我們會考慮快速失敗,從而減少大量呼叫失敗的開銷。
再說說呼叫超時。不同於呼叫失敗,這個時候依賴還是可用的,只是需要花費更多的時間來獲取我們想要的東西。當流量比較大時,執行緒池將很快被耗盡。在大型的專案中,一個依賴的超時帶來的影響會被放大,甚至會導致整個系統癱瘓。所以,呼叫失敗也需要快速失敗。
針對上面說的的異常,Hystrix 可以及時將故障的依賴隔離開,後續的呼叫都會快速失敗,直到依賴恢復正常。
如何實現
呼叫失敗或超時到達一定的閾值後,Hystrix 的保護器將被觸發開啟。
呼叫依賴之前,Hystrix 會檢查保護器是否開啟,如果開啟會直接走 fall back,如果沒有開啟,才會執行呼叫操作。
另外,Hystrix 會定時地去檢查依賴是否已經恢復,當依賴恢復時,將關閉保護器,整個呼叫鏈路又恢復正常。
當然,實際流程要更復雜一些,還涉及到了快取、執行緒池等。官方提供了一張圖,並給出了較為詳細的描述。
如何使用
這裡我用具體例子來說明各個節點的邏輯,專案程式碼見文末連結。
包裝為command
首先,要使用 Hystrix,我們需要將對某個依賴的呼叫請求包裝成一個 command,具體通過繼承HystrixCommand
或 HystrixObservableCommand
進行包裝。繼承後我們需要做三件事:
- 在構造中指定 commandKey 和 commandGroupKey。需要注意的是,相同 commandGroupKey 的 command 會共用一個執行緒池,相同 commandKey 的會共用一個保護器和快取。例如,我們需要根據使用者 id 從 UC 服務獲取使用者物件,可以讓所有 UC 介面共用一個 commandGroupKey,而不同的介面採用不同的 commandKey。
- 重寫 run 或 construct 方法。這個方法裡放的是我們呼叫某個依賴的程式碼。我可以放呼叫遠端服務的程式碼,也可以隨便列印一句話,因此,我前面說過,依賴的定義可以更寬泛一些,而不僅限於遠端服務。
- 重寫 getFallback 方法。當快速失敗時,就會走這個方法。
public class CommandGetUserByIdFromUserService extends HystrixCommand<DataResponse<User>> {
private final String userId;
public CommandGetUserByIdFromUserService(String userId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserService")) // 相同command group共用一個ThreadPool
.andCommandKey(HystrixCommandKey.Factory.asKey("UserService_GetUserById"))// 相同command key共用一個CircuitBreaker、requestCache
);
this.userId = userId;
}
/**
* 執行最終任務,如果繼承的是HystrixObservableCommand則重寫construct()
*/
@Override
protected DataResponse<User> run() {
return userService.getUserById(userId);
}
/**
* 該方法在以下場景被呼叫
* 1. 最終任務執行時丟擲異常;
* 2. 最終任務執行超時;
* 3. 斷路器開啟時,請求短路;
* 4. 連線池、佇列或訊號量耗盡
*/
@Override
protected DataResponse<User> getFallback() {
return DataResponse.buildFailure("fail or timeout");
}
}
執行command
然後,只有執行 command,上面的圖就“動起來”了。有四種方法執行 command,呼叫 execute() 或 observe() 會馬上執行,而呼叫 queue() 或 toObservable() 不會馬上執行,要等 future.get() 或 observable.subscribe() 時才會被執行。
@Test
public void testExecuteWays() throws Exception {
DataResponse<User> response = new CommandGetUserByIdFromUserService("1").execute();// execute()=queue().get() 同步
LOG.info("command.execute():{}", response);
Future<DataResponse<User>> future = new CommandGetUserByIdFromUserService("1").queue();//queue()=toObservable().toBlocking().toFuture() 同步
LOG.info("command.queue().get():{}", future.get());
Observable<DataResponse<User>> observable = new CommandGetUserByIdFromUserService("1").observe();//hot observable 非同步
observable.subscribe(x -> LOG.info("command.observe():{}", x));
Observable<DataResponse<User>> observable2 = new CommandGetUserByIdFromUserService("1").toObservable();//cold observable 非同步
observable2.subscribe(x -> LOG.info("command.toObservable():{}", x));
}
是否使用快取
接著,進入 command 的邏輯後,Hystrix 會先判斷是否使用快取。
預設情況下,快取是禁用的,我們可以通過重寫 command 的 getCacheKey() 來開啟(只要返回非空,都會開啟)。
@Override
protected String getCacheKey() {
return userId;
}
需要注意一點,用到快取(HystrixRequestCache)、請求日誌(HystrixRequestLog)、批處理(HystrixCollapser)時需要初始化HystrixRequestContext,並按以下 try...finally 格式呼叫:
@Test
public void testCache() {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
CommandGetUserByIdFromUserService command1 = new CommandGetUserByIdFromUserService("1");
command1.execute();
// 第一次呼叫時快取裡沒有
assertFalse(command1.isResponseFromCache());
CommandGetUserByIdFromUserService command2 = new CommandGetUserByIdFromUserService("1");
command2.execute();
// 第二次呼叫直接從快取拿結果
assertTrue(command2.isResponseFromCache());
} finally {
context.shutdown();
}
// zzs001
}
保護器是否開啟
接著,Hystrix 會判斷保護器是否開啟。
這裡我在 command 的 run 方法中手動製造 fail 或 time out。另外,我們可以通過 HystrixCommandProperties 調整保護器開啟的閾值。
public CommandGetUserByIdFromUserService(String userId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserService")) // 相同command group共用一個ThreadPool
.andCommandKey(HystrixCommandKey.Factory.asKey("UserService_GetUserById"))// 相同command key共用一個CircuitBreaker、requestCache
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withCircuitBreakerRequestVolumeThreshold(10)
.withCircuitBreakerErrorThresholdPercentage(50)
.withMetricsHealthSnapshotIntervalInMilliseconds(1000)
.withExecutionTimeoutInMilliseconds(1000)
));
this.userId = userId;
}
@Override
protected DataResponse<User> run() {
LOG.info("執行最終任務,執行緒為:{}", Thread.currentThread());
// 手動製造超時
/*try {
Thread.sleep(1200);
} catch(InterruptedException e) {
e.printStackTrace();
}*/
// 手動製造異常
throw new RuntimeException("");
//return UserService.instance().getUserById(userId);
}
這個時候,當呼叫失敗達到一定閾值後,保護器被觸發開啟,後續的請求都會直接走 fall back。
@Test
public void testCircuitBreaker() {
CommandGetUserByIdFromUserService command;
int count = 1;
do {
command = new CommandGetUserByIdFromUserService("1");
command.execute();
count++;
} while(!command.isCircuitBreakerOpen());
LOG.info("呼叫{}次之後,斷路器開啟", count);
// 這個時候再去呼叫,會直接走fall back
command = new CommandGetUserByIdFromUserService("1");
command.execute();
assertTrue(command.isCircuitBreakerOpen());
}
連線池、佇列或訊號量是否耗盡
即使保護器是關閉狀態,我們也不能馬上呼叫依賴,需要先檢查連線池或訊號量是否耗盡(通過 HystrixCommandProperties 可以配置使用執行緒池還是訊號量)。
因為預設的執行緒池比較大,所以,這裡我通過 HystrixThreadPoolProperties 調小了執行緒池。
public CommandGetUserByIdFromUserService(String userId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserService")) // 相同command group共用一個ThreadPool
.andCommandKey(HystrixCommandKey.Factory.asKey("UserService_GetUserById"))// 相同command key共用一個CircuitBreaker、requestCache
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(2)
.withMaxQueueSize(5)
.withQueueSizeRejectionThreshold(5)
));
this.userId = userId;
}
這個時候,當執行緒池耗盡後,後續的請求都會直接走 fall back,而保護器並沒有開啟。
@Test
public void testThreadPoolFull() throws InterruptedException {
int maxRequest = 100;
int i = 0;
do {
CommandGetUserByIdFromUserService command = new CommandGetUserByIdFromUserService("1");
command.toObservable().subscribe(v -> LOG.info("non-blocking command.toObservable():{}", v));
LOG.info("是否執行緒池、佇列或訊號量耗盡:{}", command.isResponseRejected());
} while(i++ < maxRequest - 1);
// 這個時候再去呼叫,會直接走fall back
CommandGetUserByIdFromUserService command = new CommandGetUserByIdFromUserService("1");
command.execute();
// 執行緒池、佇列或訊號量耗盡
assertTrue(command.isResponseRejected());
assertFalse(command.isCircuitBreakerOpen());
Thread.sleep(10000);
// zzs001
}
結語
以上簡單地講完了 Hystrix。閱讀官方的 wiki,再結合上面的幾個例子,相信大家可以對 Hystrix 有較深的瞭解。
最後,感謝閱讀,歡迎私信交流。
參考資料
Home · Netflix/Hystrix Wiki · GitHub
本文為原創文章,轉載請附上原文出處連結:https://www.cnblogs.com/ZhangZiSheng001/p/15567420.html