一、Hystrix
Hystrix是Netflix開源的一款容錯系統,能幫助使用者碼出具備強大的容錯能力和魯棒性的程序。提供降級,熔斷等功能,並且熔斷開關打開之後,會在服務可用之後,自動關閉。spring cloud中有用到。
如果你的服務依賴於多個服務,並且不想因爲某個服務掛掉,而影響你服務。比如hbase掛掉了,你可以通過降級策略返回默認值,或者直接熔斷。
Hystrix提供了服務隔離,每個服務使用獨立的線程池實現。
二、 工作流程
下面將更詳細的解析每一個步驟都發生哪些動作:
構建一個HystrixCommand或者HystrixObservableCommand對象。
第一步就是構建一個HystrixCommand或者HystrixObservableCommand對象,該對象將代表你的一個依賴請求,向構造函數中傳入請求依賴所需要的參數。
如果構建HystrixCommand中的依賴返回單個響應,例如:
public class HelloWorldHystrixCommand extends HystrixCommand {
private final String name;
public HelloWorldHystrixCommand(String name) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.name = name;
}
// 如果繼承的是HystrixObservableCommand,要重寫Observable construct()
@Override
protected String run() throws InterruptedException {
return "Hello " + name + "! thread:" + Thread.currentThread().getName();
}
@Override
protected String getFallback() {
System.out.println("觸發了降級!");
return "exeucute fallback";
}
public static void main(String[] args) {
/* 調用程序對HelloWorldHystrixCommand實例化,執行execute()即觸發HelloWorldHystrixCommand.run()的執行 */
Object result = new HelloWorldHystrixCommand("HLX").execute();
System.out.println(result); // 打印出Hello HLX
}
}
有4種方式可以執行一個Hystrix命令。
execute()—該方法是阻塞的,從依賴請求中接收到單個響應(或者出錯時拋出異常)。
queue()—從依賴請求中返回一個包含單個響應的Future對象。
observe()—訂閱一個從依賴請求中返回的代表響應的Observable對象。
toObservable()—返回一個Observable對象,只有當你訂閱它時,它纔會執行Hystrix命令併發射響應。
K value = command.execute();
Future<K> fValue = command.queue();
Observable<K> ohValue = command.observe(); //hot observable
Observable<K> ocValue = command.toObservable(); //cold observable
三、降級(fallback)
使用fallback機制很簡單,繼承HystrixCommand只需重寫getFallback(),繼承HystrixObservableCommand只需重寫resumeWithFallback()。
根據上面的工作流程圖所示,當出現以下幾種情況時會觸發降級:
名字 | 描述 |
---|---|
FAILURE | 執行拋出異常 |
TIMEOUT | 執行開始,但沒有在允許的時間內完成 |
SHORT_CIRCUITED | 斷路器打開,不嘗試執行 |
THREAD_POOL_REJECTED | 線程池拒絕,不嘗試執行 |
SEMAPHORE_REJECTED | 信號量拒絕,不嘗試執行 |
如果沒有實現降級方法,默認會拋出異常。降級方法也可能會超時,或者拋出異常。
四、隔離
hystrix提供了兩種隔離策略:線程池隔離和信號量隔離。hystrix默認採用線程池隔離。
我認爲採用信號量隔離並不是真正的隔離,因爲還是在調用者的線程中執行,如果服務掛了,還是會拖垮調用線程。使用信號量的超時機制,是在結果返回之後才計算是否超時的,服務掛了之後,肯定很長時間才返回,這時候再採取降級已經沒有意義了。
線程池隔離:不同服務通過使用不同線程池,彼此間將不受影響,達到隔離效果。**代碼是在hystrix的線程中執行的,如果超時,可以中斷。**以demo爲例,我們通過andThreadPoolKey配置使用命名爲ThreadPoolTest的線程池,實現與其他命名的線程池天然隔離,如果不配置andThreadPoolKey則使用withGroupKey配置來命名線程池。
public class HystrixCommand4ThreadPoolTest extends HystrixCommand<String> {
private final String name;
public HystrixCommand4ThreadPoolTest(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ThreadPoolTestGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("testCommandKey"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ThreadPoolTest"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(1500)
.withExecutionIsolationThreadInterruptOnTimeout(true)
.withCircuitBreakerErrorThresholdPercentage(50)
.withCircuitBreakerRequestVolumeThreshold(5)
)
.andThreadPoolPropertiesDefaults(
HystrixThreadPoolProperties.Setter()
.withCoreSize(3)// 配置線程池裏的線程數
.withMaxQueueSize(20)
.withQueueSizeRejectionThreshold(20)
)
);
this.name = name;
}
@Override
protected String run() throws Exception {
System.out.println("====command run : " + Thread.currentThread().getName() + " at: " + System.currentTimeMillis());
TimeUnit.MILLISECONDS.sleep(3000);
// throw new RuntimeException("=== timeout ...");
return name;
}
@Override
protected String getFallback() {
return "fallback: " + name + Thread.currentThread().getName() + " at : " + System.currentTimeMillis();
}
public static class UnitTest {
@Test
public void testSynchronous() throws IOException {
for (int i = 0; i < 10; i++) {
try {
new Thread(() -> System.out.println("===========" + new HystrixCommand4ThreadPoolTest("Hlx").execute())).start();
Thread.sleep(500);
} catch (Exception e) {
System.out.println("run()拋出HystrixBadRequestException時,被捕獲到這裏" + e.getCause());
}
}
try {
TimeUnit.MILLISECONDS.sleep(5000);
} catch (Exception e) {
}
for (int i = 0; i < 10; i++) {
try {
new Thread(() -> System.out.println("===========" + new HystrixCommand4ThreadPoolTest("Hlx").execute())).start();
// futures.add(new HystrixCommand4ThreadPoolTest("Hlx"+i).queue());
} catch (Exception e) {
System.out.println("run()拋出HystrixBadRequestException時,被捕獲到這裏" + e.getCause());
}
}
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
信號量隔離:線程隔離會帶來線程開銷,有些場景(比如無網絡請求場景)可能會因爲用開銷換隔離得不償失,爲此hystrix提供了信號量隔離,當服務的併發數大於信號量閾值時將進入fallback。
package com.step.jliang.hystrix;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.hystrix.HystrixThreadPoolKey;
import com.netflix.hystrix.HystrixThreadPoolProperties;
import java.util.concurrent.TimeUnit;
/**
* @author jliang
*/
public class HystrixCommand4Semphore extends HystrixCommand<String> {
private final String name;
public HystrixCommand4Semphore(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("SemphoreTestGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("testCommandKey"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(1500)
.withCircuitBreakerErrorThresholdPercentage(50)
.withCircuitBreakerRequestVolumeThreshold(5)
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
.withExecutionIsolationSemaphoreMaxConcurrentRequests(5)
)
);
this.name = name;
}
@Override
protected String run() throws Exception {
System.out.println("====command run : " + Thread.currentThread().getName() + " at: " + System.currentTimeMillis());
TimeUnit.MILLISECONDS.sleep(3000);
return name;
}
// @Override
// protected String getFallback() {
// return "semphore fallback: " + name + Thread.currentThread().getName() + " at : " + System.currentTimeMillis();
// }
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
try {
new Thread(() -> System.out.println("===========" + new HystrixCommand4Semphore("Hlx").execute())).start();
Thread.sleep(500);
} catch (Exception e) {
System.out.println("run()拋出HystrixBadRequestException時,被捕獲到這裏" + e.getCause());
}
}
}
}
五、如何使用熔斷器(Circuit Breaker)
熔斷器,現實生活中有一個很好的類比,就是家庭電路中都會安裝一個保險盒,當電流過大的時候保險盒裏面的保險絲會自動斷掉,來保護家裏的各種電器及電路。Hystrix中的熔斷器(Circuit Breaker)也是起到這樣的作用,Hystrix在運行過程中會向每個commandKey對應的熔斷器報告成功、失敗、超時和拒絕的狀態,熔斷器維護計算統計的數據,根據這些統計的信息來確定熔斷器是否打開。如果打開,後續的請求都會被截斷。然後會隔一段時間默認是5s,嘗試半開,放入一部分流量請求進來,相當於對依賴服務進行一次健康檢查,如果恢復,熔斷器關閉,隨後完全恢復調用。
熔斷器的數據通路如下圖所示:
每個熔斷器默認維護10個bucket,每秒一個bucket,每個blucket記錄成功,失敗,超時,拒絕的狀態,默認錯誤超過50%且10秒內超過20個請求進行中斷攔截。
由於Hystrix是一個容錯框架,因此我們在使用的時候,要達到熔斷的目的只需配置一些參數就可以了。但我們要達到真正的效果,就必須要了解這些參數。Circuit Breaker一共包括如下6個參數。
1、circuitBreaker.enabled
是否啓用熔斷器,默認是TURE。
2、circuitBreaker.forceOpen
熔斷器強制打開,始終保持打開狀態。默認值FLASE。
3、circuitBreaker.forceClosed
熔斷器強制關閉,始終保持關閉狀態。默認值FLASE。
4、circuitBreaker.errorThresholdPercentage
設定錯誤百分比,默認值50%,例如一段時間(10s)內有100個請求,其中有55個超時或者異常返回了,那麼這段時間內的錯誤百分比是55%,大於了默認值50%,這種情況下觸發熔斷器-打開。
5、circuitBreaker.requestVolumeThreshold
默認值20.意思是至少有20個請求才進行errorThresholdPercentage錯誤百分比計算。比如一段時間(10s)內有19個請求全部失敗了。錯誤百分比是100%,但熔斷器不會打開,因爲requestVolumeThreshold的值是20. 這個參數非常重要,熔斷器是否打開首先要滿足這個條件。在自己測試的時候,如果熔斷器不起作用,要看下是否滿足這個條件。
6、circuitBreaker.sleepWindowInMilliseconds
半開試探休眠時間,默認值5000ms。當熔斷器開啓一段時間之後比如5000ms,會嘗試放過去一部分流量進行試探,確定依賴服務是否恢復。
具體的例子可以參考上面使用線程池隔離的代碼。
六、常見的參數及默認值
可以參考這篇博客
如果還有不明白的,可以參考官方文檔
參考鏈接:
https://www.jianshu.com/p/14958039fd15
https://github.com/Netflix/Hystrix/wiki/How-it-Works
https://segmentfault.com/a/1190000012439580
https://blog.csdn.net/tongtong_use/article/details/78611225
https://juejin.im/entry/59dbe2cc51882578d152bdf8
https://www.jianshu.com/p/b9af028efebb