Hystrix 分散式系統限流、降級、熔斷框架
為什麼需要Hystrix
在大中型分散式系統中,通常系統很多依賴,如下圖:
在高併發訪問下,這些依賴的穩定性與否對系統的影響非常大,但是依賴有很多不可控問題:如網路連線緩慢,資源繁忙,暫時不可用,服務離線等,如下圖:
當依賴阻塞時,大多數伺服器的執行緒池就出現阻塞,影響整個線上服務的穩定性,如下圖:
在複雜的分散式架構的應用程式有很多的依賴,都會不可避免地在某些時候失敗。高併發的依賴失敗時如果沒有隔離措施,當前應用服務就有被拖垮的風險。
Hystrix如何解決依賴隔離
- Hystrix使用命令模式HystrixCommand(Command)包裝依賴呼叫邏輯,每個命令在單獨執行緒中/訊號授權下執行。
- 可配置依賴呼叫超時時間,超時時間一般設為比99.5%平均時間略高即可。當呼叫超時時,直接返回或執行fallback邏輯。
- 為每個依賴提供一個小的執行緒池或訊號,如果執行緒池已滿呼叫將被立即拒絕,預設不採用排隊。加速失敗判定時間。
- 依賴呼叫結果分:成功、失敗/丟擲異常、超時、執行緒拒絕、短路。 請求失敗(異常,拒絕,超時,短路)時執行fallback(降級)邏輯。
- 提供熔斷器元件,可以自動執行或手動呼叫,停止當前依賴一段時間(10秒),熔斷器預設錯誤率閾值為50%,超過將自動執行。
- 提供近實時依賴的統計和監控。
Hystrix依賴的隔離架構,如下圖:
如何使用Hystrix
使用maven引入Hystrix依賴
<hystrix.version>1.3.16</hystrix.version> <hystrix-metrics-event-stream.version>1.1.2</hystrix-metrics-event-stream.version> <dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-core</artifactId> <version>${hystrix.version}</version> </dependency> <dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-metrics-event-stream</artifactId> <version>${hystrix-metrics-event-stream.version}</version> </dependency>
使用命令模式封裝依賴邏輯
public class HelloWorldCommand extends HystrixCommand<String> { private final String name; public HelloWorldCommand(String name) { //最少配置:指定命令組名(CommandGroup) super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")); this.name = name; } @Override protected String run() { // 依賴邏輯封裝在run()方法中 return "Hello " + name +" thread:" + Thread.currentThread().getName(); } //呼叫例項 public static void main(String[] args) throws Exception{ //每個Command物件只能呼叫一次,不可以重複呼叫, //重複呼叫對應異常資訊 HelloWorldCommand helloWorldCommand = new HelloWorldCommand("sync-hystrix"); //使用execute()同步呼叫程式碼,效果等同於:helloWorldCommand.queue().get(); String result = helloWorldCommand.execute(); System.out.println("result=" + result); helloWorldCommand = new HelloWorldCommand("async-hystrix"); //非同步呼叫,可自由控制獲取結果時機, Future<String> future = helloWorldCommand.queue(); //get操作不能超過command定義的超時時間,預設:1秒 result = future.get(100, TimeUnit.MILLISECONDS); System.out.println("result=" + result); System.out.println("mainThread=" + Thread.currentThread().getName()); } }
使用Fallback() 提供降級策略
//過載HystrixCommand的getFallback方法實現邏輯
public class HelloWorldCommand extends HystrixCommand<String> {
private final String name;
public HelloWorldCommand(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionIsolationThreadTimeoutInMilliseconds(500)));
this.name = name;
}
protected String getFallback() {
return "exeucute Falled";
}
protected String run() throws Exception {
//sleep 1 秒,呼叫會超時
TimeUnit.MILLISECONDS.sleep(1000);
return "Hello " + name +" thread:" + Thread.currentThread().getName();
}
public static void main(String[] args) throws Exception{
HelloWorldCommand command = new HelloWorldCommand("test-Fallback");
String result = command.execute();
}
}
NOTE: 除了HystrixBadRequestException異常之外,所有從run()方法丟擲的異常都算作失敗,並觸發降級getFallback()和斷路器邏輯。
HystrixBadRequestException用在非法引數或非系統故障異常等不應觸發回退邏輯的場景。
依賴命名:CommandKey
public HelloWorldCommand(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
/* HystrixCommandKey工廠定義依賴名稱 */
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld")));
this.name = name;
}
NOTE: 每個CommandKey代表一個依賴抽象,相同的依賴要使用相同的CommandKey名稱。依賴隔離的根本就是對相同CommandKey的依賴做隔離。
依賴分組:CommandGroup
命令分組用於對依賴操作分組,便於統計,彙總等。
//使用HystrixCommandGroupKey工廠定義 public HelloWorldCommand(String name) { Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup")) }
NOTE: CommandGroup是每個命令最少配置的必選引數,在不指定ThreadPoolKey的情況下,字面值用於對不同依賴的執行緒池/訊號區分。
執行緒池/訊號:ThreadPoolKey
public HelloWorldCommand(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"))
/* 使用HystrixThreadPoolKey工廠定義執行緒池名稱*/
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("HelloWorldPool")));
this.name = name;
}
NOTE: 當對同一業務依賴做隔離時使用CommandGroup做區分,但是對同一依賴的不同遠端呼叫如(一個是redis 一個是http),可以使用HystrixThreadPoolKey做隔離區分。
最然在業務上都是相同的組,但是需要在資源上做隔離時,可以使用HystrixThreadPoolKey區分。
訊號量隔離:SEMAPHORE
隔離原生代碼或可快速返回遠端呼叫(如memcached,redis)可以直接使用訊號量隔離,降低執行緒隔離開銷。
public class HelloWorldCommand extends HystrixCommand<String> { private final String name; public HelloWorldCommand(String name) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup")) /* 配置訊號量隔離方式,預設採用執行緒池隔離 */ .andCommandPropertiesDefaults(HystrixCommandProperties.Setter() .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE))); this.name = name; } @Override protected String run() throws Exception { return "HystrixThread:" + Thread.currentThread().getName(); } public static void main(String[] args) throws Exception{ HelloWorldCommand command = new HelloWorldCommand("semaphore"); String result = command.execute(); System.out.println(result); System.out.println("MainThread:" + Thread.currentThread().getName()); } }
Hystrix關鍵元件分析
Hystrix流程結構解析
流程說明:
1,每次呼叫建立一個新的HystrixCommand,把依賴呼叫封裝在run()方法中
2,執行execute()/queue做同步或非同步呼叫
3,判斷熔斷器(circuit-breaker)是否開啟,如果開啟跳到步驟8,進行降級策略,否則繼續後續步驟
4,判斷執行緒池/佇列/訊號量是否跑滿,如果跑滿進入降級步驟8,否則繼續後續步驟
5,呼叫HystrixCommand的run方法,執行依賴邏輯
a 依賴邏輯呼叫超時,進入步驟8
6,判斷邏輯是否呼叫成功
a 返回成功呼叫結果
b 調用出錯,進入步驟8
7,計算熔斷器狀態,所有的執行狀態上報給熔斷器,用於統計從而判斷熔斷器狀態
8,getFallback()降級邏輯
以下四種情況將觸發getFallback呼叫:
- run()方法丟擲非HystrixBadRequestException異常
- run()方法呼叫超時
- 熔斷器開啟攔截呼叫
- 執行緒池/佇列/訊號量是否跑滿
沒有實現getFallback的Command將直接丟擲異常
fallback降級邏輯呼叫成功直接返回
降級邏輯呼叫失敗丟擲異常
9,返回執行成功結果
熔斷器:Circuit Breaker
Circuit Breaker 流程架構和統計
每個熔斷器預設維護10個bucket,每秒一個bucket,每個blucket記錄成功、失敗、超時、拒絕的狀態,預設錯誤超過50%且10秒內超過20個請求進行中斷攔截.。
隔離(Isolation)分析
Hystrix隔離方式採用執行緒/訊號的方式,通過隔離限制依賴的併發量和阻塞擴散。
(1) 執行緒隔離
把執行依賴程式碼的執行緒與請求執行緒分離,請求執行緒可以自由控制離開的時間(非同步過程)。
通過執行緒池大小可以控制併發量,當執行緒池飽和時可以提前拒絕服務,防止依賴問題擴散。
線上建議執行緒池不要設定過大,否則大量堵塞執行緒有可能會拖慢伺服器。
執行緒池的使用示意圖如下圖所示,當n個請求執行緒併發對某個介面請求呼叫時,會先從hystrix管理的執行緒池裡面獲得一個執行緒,然後將引數傳遞給這個執行緒去執行真正呼叫。執行緒池的大小有限,預設是10個執行緒,可以使用maxConcurrentRequests引數配置,如果併發請求數多於執行緒池執行緒個數,就有執行緒需要進入佇列排隊,但排隊佇列也有上限,預設是 5,如果排隊佇列也滿,則必定有請求執行緒會走fallback流程。
執行緒池模式可以支援非同步呼叫,支援超時呼叫,支援直接熔斷,存線上程切換,開銷大。
(2) 執行緒隔離的優缺點
執行緒隔離的優點:
- 使用執行緒可以完全隔離第三方程式碼,請求執行緒可以快速放回。
- 當一個失敗的依賴再次變成可用時,執行緒池將清理,並立即恢復可用,而不是一個長時間的恢復。
- 可以完全模擬非同步呼叫,方便非同步程式設計。
執行緒隔離的缺點:
- 執行緒池的主要缺點是它增加了cpu,因為每個命令的執行涉及到排隊(預設使用SynchronousQueue避免排隊),排程和上下文切換。
- 對使用ThreadLocal等依賴執行緒狀態的程式碼增加複雜性,需要手動傳遞和清理執行緒狀態。
NOTE: Netflix公司內部認為執行緒隔離開銷足夠小,不會造成重大的成本或效能的影響。
Netflix內部API每天100億的HystrixCommand依賴請求使用執行緒隔,每個應用大約40多個執行緒池,每個執行緒池大約5-20個執行緒。
(3) 訊號隔離
訊號隔離也可以用於限制併發訪問,防止阻塞擴散, 與執行緒隔離最大不同在於執行依賴程式碼的執行緒依然是請求執行緒(該執行緒需要通過訊號申請)。
如果客戶端是可信的且可以快速返回,可以使用訊號隔離替換執行緒隔離,降低開銷。
執行緒隔離與訊號隔離區別如下圖:
訊號量的使用示意圖如下圖所示,當n個併發請求去呼叫一個目標服務介面時,都要獲取一個訊號量才能真正去呼叫目標服務介面,但訊號量有限,預設是10個,可以使用maxConcurrentRequests引數配置,如果併發請求數多於訊號量個數,就有執行緒需要進入佇列排隊,但排隊佇列也有上限,預設是 5,如果排隊佇列也滿,則必定有請求執行緒會走fallback流程,從而達到限流和防止雪崩的目的。
訊號量模式從始至終都只有請求執行緒自身,是同步呼叫模式,不支援超時呼叫,不支援直接熔斷,由於沒有執行緒的切換,開銷非常小。
(4) 總結
當請求的服務網路開銷比較大的時候,或者是請求比較耗時的時候,我們最好是使用執行緒隔離策略,這樣的話,可以保證大量的容器(tomcat)執行緒可用,不會由於服務原因,一直處於阻塞或等待狀態,快速失敗返回。而當我們請求快取這些服務的時候,我們可以使用訊號量隔離策略,因為這類服務的返回通常會非常的快,不會佔用容器執行緒太長時間,而且也減少了執行緒切換的一些開銷,提高了快取服務的效率。
- 執行緒池:適合絕大多數的場景,99%的。對依賴服務的網路請求的呼叫和訪問,timeout這種問題
- 訊號量:適合你的訪問不是對外部依賴的訪問,而是對內部的一些比較複雜的業務邏輯的訪問,但是像這種訪問,系統內部的程式碼,其實不涉及任何的網路請求,那麼只要做訊號量的普通限流就可以了,因為不需要去捕獲timeout類似的問題,演算法+資料結構的效率不是太高,併發量突然太高,因為這裡稍微耗時一些,導致很多執行緒卡在這裡的話,不太好,所以進行一個基本的資源隔離和訪問,避免內部複雜的低效率的程式碼,導致大量的執行