原文參考:http://hot66hot.iteye.com/blog/2155036
一:為什么需要Hystrix?
在大中型分布式系統中,通常系統很多依賴(HTTP,hession,Netty,Dubbo等),如下圖:

在高並發訪問下,這些依賴的穩定性與否對系統的影響非常大,但是依賴有很多不可控問題:如網絡連接緩慢,資源繁忙,暫時不可用,服務脫機等.
如下圖:QPS為50的依賴 I 出現不可用,但是其他依賴仍然可用.

當依賴I 阻塞時,大多數服務器的線程池就出現阻塞(BLOCK),影響整個線上服務的穩定性.如下圖:

在復雜的分布式架構的應用程序有很多的依賴,都會不可避免地在某些時候失敗。高並發的依賴失敗時如果沒有隔離措施,當前應用服務就有被拖垮的風險。
- 例如:一個依賴30個SOA服務的系統,每個服務99.99%可用。
- 99.99%的30次方 ≈ 99.7%
- 0.3% 意味着一億次請求 會有 3,000,00次失敗
- 換算成時間大約每月有2個小時服務不穩定.
- 隨着服務依賴數量的變多,服務不穩定的概率會成指數性提高.
解決問題方案:對依賴做隔離,Hystrix就是處理依賴隔離的框架,同時也是可以幫我們做依賴服務的治理和監控.
Netflix 公司開發並成功使用Hystrix,使用規模如下:
- The Netflix API processes 10+ billion HystrixCommand executions per day using thread isolation.
- Each API instance has 40+ thread-pools with 5-20 threads in each (most are set to 10).
二:Hystrix如何解決依賴隔離
1:Hystrix使用命令模式HystrixCommand(Command)包裝依賴調用邏輯,每個命令在單獨線程中/信號授權下執行。
2:可配置依賴調用超時時間,超時時間一般設為比99.5%平均時間略高即可.當調用超時時,直接返回或執行fallback邏輯。
3:為每個依賴提供一個小的線程池(或信號),如果線程池已滿調用將被立即拒絕,默認不采用排隊.加速失敗判定時間。
4:依賴調用結果分:成功,失敗(拋出異常),超時,線程拒絕,短路。 請求失敗(異常,拒絕,超時,短路)時執行fallback(降級)邏輯。
5:提供熔斷器組件,可以自動運行或手動調用,停止當前依賴一段時間(10秒),熔斷器默認錯誤率閾值為50%,超過將自動運行。
6:提供近實時依賴的統計和監控
Hystrix依賴的隔離架構,如下圖:

三:如何使用Hystrix
1:使用maven引入Hystrix依賴
1 <!-- 依賴版本 --> 2 <hystrix.version>1.3.16</hystrix.version> 3 <hystrix-metrics-event-stream.version>1.1.2</hystrix-metrics-event-stream.version> 4 5 <dependency> 6 <groupId>com.netflix.hystrix</groupId> 7 <artifactId>hystrix-core</artifactId> 8 <version>${hystrix.version}</version> 9 </dependency> 10 <dependency> 11 <groupId>com.netflix.hystrix</groupId> 12 <artifactId>hystrix-metrics-event-stream</artifactId> 13 <version>${hystrix-metrics-event-stream.version}</version> 14 </dependency> 15 <!-- 倉庫地址 --> 16 <repository> 17 <id>nexus</id> 18 <name>local private nexus</name> 19 <url>http://maven.oschina.net/content/groups/public/</url> 20 <releases> 21 <enabled>true</enabled> 22 </releases> 23 <snapshots> 24 <enabled>false</enabled> 25 </snapshots> 26 </repository>
2:使用命令模式封裝依賴邏輯
1 public class HelloWorldCommand extends HystrixCommand<String> { 2 private final String name; 3 public HelloWorldCommand(String name) { 4 //最少配置:指定命令組名(CommandGroup) 5 super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")); 6 this.name = name; 7 } 8 @Override 9 protected String run() { 10 // 依賴邏輯封裝在run()方法中 11 return "Hello " + name +" thread:" + Thread.currentThread().getName(); 12 } 13 //調用實例 14 public static void main(String[] args) throws Exception{ 15 //每個Command對象只能調用一次,不可以重復調用, 16 //重復調用對應異常信息:This instance can only be executed once. Please instantiate a new instance. 17 HelloWorldCommand helloWorldCommand = new HelloWorldCommand("Synchronous-hystrix"); 18 //使用execute()同步調用代碼,效果等同於:helloWorldCommand.queue().get(); 19 String result = helloWorldCommand.execute(); 20 System.out.println("result=" + result); 21 22 helloWorldCommand = new HelloWorldCommand("Asynchronous-hystrix"); 23 //異步調用,可自由控制獲取結果時機, 24 Future<String> future = helloWorldCommand.queue(); 25 //get操作不能超過command定義的超時時間,默認:1秒 26 result = future.get(100, TimeUnit.MILLISECONDS); 27 System.out.println("result=" + result); 28 System.out.println("mainThread=" + Thread.currentThread().getName()); 29 } 30 31 } 32 //運行結果: run()方法在不同的線程下執行 33 // result=Hello Synchronous-hystrix thread:hystrix-HelloWorldGroup-1 34 // result=Hello Asynchronous-hystrix thread:hystrix-HelloWorldGroup-2 35 // mainThread=main
note:異步調用使用 command.queue()get(timeout, TimeUnit.MILLISECONDS);同步調用使用command.execute() 等同於 command.queue().get();
3:注冊異步事件回調執行
1 //注冊觀察者事件攔截 2 Observable<String> fs = new HelloWorldCommand("World").observe(); 3 //注冊結果回調事件 4 fs.subscribe(new Action1<String>() { 5 @Override 6 public void call(String result) { 7 //執行結果處理,result 為HelloWorldCommand返回的結果 8 //用戶對結果做二次處理. 9 } 10 }); 11 //注冊完整執行生命周期事件 12 fs.subscribe(new Observer<String>() { 13 @Override 14 public void onCompleted() { 15 // onNext/onError完成之后最后回調 16 System.out.println("execute onCompleted"); 17 } 18 @Override 19 public void onError(Throwable e) { 20 // 當產生異常時回調 21 System.out.println("onError " + e.getMessage()); 22 e.printStackTrace(); 23 } 24 @Override 25 public void onNext(String v) { 26 // 獲取結果后回調 27 System.out.println("onNext: " + v); 28 } 29 }); 30 /* 運行結果 31 call execute result=Hello observe-hystrix thread:hystrix-HelloWorldGroup-3 32 onNext: Hello observe-hystrix thread:hystrix-HelloWorldGroup-3 33 execute onCompleted 34 */
4:使用Fallback() 提供降級策略

NOTE: 除了HystrixBadRequestException異常之外,所有從run()方法拋出的異常都算作失敗,並觸發降級getFallback()和斷路器邏輯。
HystrixBadRequestException用在非法參數或非系統故障異常等不應觸發回退邏輯的場景。
5:依賴命名:CommandKey
1 public HelloWorldCommand(String name) { 2 super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")) 3 /* HystrixCommandKey工廠定義依賴名稱 */ 4 .andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"))); 5 this.name = name; 6 }
NOTE: 每個CommandKey代表一個依賴抽象,相同的依賴要使用相同的CommandKey名稱。依賴隔離的根本就是對相同CommandKey的依賴做隔離.
6:依賴分組:CommandGroup
命令分組用於對依賴操作分組,便於統計,匯總等.
NOTE: CommandGroup是每個命令最少配置的必選參數,在不指定ThreadPoolKey的情況下,字面值用於對不同依賴的線程池/信號區分.
7:線程池/信號:ThreadPoolKey
最然在業務上都是相同的組,但是需要在資源上做隔離時,可以使用HystrixThreadPoolKey區分.
8:請求緩存 Request-Cache
NOTE:請求緩存可以讓(CommandKey/CommandGroup)相同的情況下,直接共享結果,降低依賴調用次數,在高並發和CacheKey碰撞率高場景下可以提升性能.
Servlet容器中,可以直接實用Filter機制Hystrix請求上下文
9:信號量隔離:SEMAPHORE
隔離本地代碼或可快速返回遠程調用(如memcached,redis)可以直接使用信號量隔離,降低線程隔離開銷.
10:fallback降級邏輯命令嵌套

適用場景:用於fallback邏輯涉及網絡訪問的情況,如緩存訪問。
NOTE:依賴調用和降級調用使用不同的線程池做隔離,防止上層線程池跑滿,影響二級降級邏輯調用.
11:顯示調用fallback邏輯,用於特殊業務處理

NOTE:顯示調用降級適用於特殊需求的場景,fallback用於業務處理,fallback不再承擔降級職責,建議慎重使用,會造成監控統計換亂等問題.
12:命令調用合並:HystrixCollapser
命令調用合並允許多個請求合並到一個線程/信號下批量執行。
執行流程圖如下:

NOTE:使用場景:HystrixCollapser用於對多個相同業務的請求合並到一個線程甚至可以合並到一個連接中執行,降低線程交互次和IO數,但必須保證他們屬於同一依賴.
四:監控平台搭建Hystrix-dashboard
1:監控dashboard介紹
dashboard面板可以對依賴關鍵指標提供實時監控,如下圖:

2:實例暴露command統計數據
Hystrix使用Servlet對當前JVM下所有command調用情況作數據流輸出
配置如下:
3:集群模式監控統計搭建
1)使用Turbine組件做集群數據匯總
結構圖如下;

2)內嵌jetty提供Servlet容器,暴露HystrixMetrics
3)Turbine搭建和配置
a:配置Turbine Servlet收集器
b:編寫config.properties配置集群實例
c:使用Dashboard配置連接Turbine
如下圖 :

五:Hystrix配置與分析
1:Hystrix 配置
1):Command 配置
Command配置源碼在HystrixCommandProperties,構造Command時通過Setter進行配置
具體配置解釋和默認值如下
1 //使用命令調用隔離方式,默認:采用線程隔離,ExecutionIsolationStrategy.THREAD 2 private final HystrixProperty<ExecutionIsolationStrategy> executionIsolationStrategy; 3 //使用線程隔離時,調用超時時間,默認:1秒 4 private final HystrixProperty<Integer> executionIsolationThreadTimeoutInMilliseconds; 5 //線程池的key,用於決定命令在哪個線程池執行 6 private final HystrixProperty<String> executionIsolationThreadPoolKeyOverride; 7 //使用信號量隔離時,命令調用最大的並發數,默認:10 8 private final HystrixProperty<Integer> executionIsolationSemaphoreMaxConcurrentRequests; 9 //使用信號量隔離時,命令fallback(降級)調用最大的並發數,默認:10 10 private final HystrixProperty<Integer> fallbackIsolationSemaphoreMaxConcurrentRequests; 11 //是否開啟fallback降級策略 默認:true 12 private final HystrixProperty<Boolean> fallbackEnabled; 13 // 使用線程隔離時,是否對命令執行超時的線程調用中斷(Thread.interrupt())操作.默認:true 14 private final HystrixProperty<Boolean> executionIsolationThreadInterruptOnTimeout; 15 // 統計滾動的時間窗口,默認:5000毫秒circuitBreakerSleepWindowInMilliseconds 16 private final HystrixProperty<Integer> metricsRollingStatisticalWindowInMilliseconds; 17 // 統計窗口的Buckets的數量,默認:10個,每秒一個Buckets統計 18 private final HystrixProperty<Integer> metricsRollingStatisticalWindowBuckets; // number of buckets in the statisticalWindow 19 //是否開啟監控統計功能,默認:true 20 private final HystrixProperty<Boolean> metricsRollingPercentileEnabled; 21 // 是否開啟請求日志,默認:true 22 private final HystrixProperty<Boolean> requestLogEnabled; 23 //是否開啟請求緩存,默認:true 24 private final HystrixProperty<Boolean> requestCacheEnabled; // Whether request caching is enabled.
Circuit Breaker配置源碼在HystrixCommandProperties,構造Command時通過Setter進行配置,每種依賴使用一個Circuit Breaker
1 // 熔斷器在整個統計時間內是否開啟的閥值,默認20秒。也就是10秒鍾內至少請求20次,熔斷器才發揮起作用 2 private final HystrixProperty<Integer> circuitBreakerRequestVolumeThreshold; 3 //熔斷器默認工作時間,默認:5秒.熔斷器中斷請求5秒后會進入半打開狀態,放部分流量過去重試 4 private final HystrixProperty<Integer> circuitBreakerSleepWindowInMilliseconds; 5 //是否啟用熔斷器,默認true. 啟動 6 private final HystrixProperty<Boolean> circuitBreakerEnabled; 7 //默認:50%。當出錯率超過50%后熔斷器啟動. 8 private final HystrixProperty<Integer> circuitBreakerErrorThresholdPercentage; 9 //是否強制開啟熔斷器阻斷所有請求,默認:false,不開啟 10 private final HystrixProperty<Boolean> circuitBreakerForceOpen; 11 //是否允許熔斷器忽略錯誤,默認false, 不開啟 12 private final HystrixProperty<Boolean> circuitBreakerForceClosed;
3):命令合並(Collapser)配置
Command配置源碼在HystrixCollapserProperties,構造Collapser時通過Setter進行配置
1 //請求合並是允許的最大請求數,默認: Integer.MAX_VALUE 2 private final HystrixProperty<Integer> maxRequestsInBatch; 3 //批處理過程中每個命令延遲的時間,默認:10毫秒 4 private final HystrixProperty<Integer> timerDelayInMilliseconds; 5 //批處理過程中是否開啟請求緩存,默認:開啟 6 private final HystrixProperty<Boolean> requestCacheEnabled;
4):線程池(ThreadPool)配置
1 /** 2 配置線程池大小,默認值10個. 3 建議值:請求高峰時99.5%的平均響應時間 + 向上預留一些即可 4 */ 5 HystrixThreadPoolProperties.Setter().withCoreSize(int value) 6 /** 7 配置線程值等待隊列長度,默認值:-1 8 建議值:-1表示不等待直接拒絕,測試表明線程池使用直接決絕策略+ 合適大小的非回縮線程池效率最高.所以不建議修改此值。 9 當使用非回縮線程池時,queueSizeRejectionThreshold,keepAliveTimeMinutes 參數無效 10 */ 11 HystrixThreadPoolProperties.Setter().withMaxQueueSize(int value)
2:Hystrix關鍵組件分析
1):Hystrix流程結構解析

流程說明:
1:每次調用創建一個新的HystrixCommand,把依賴調用封裝在run()方法中.
2:執行execute()/queue做同步或異步調用.
3:判斷熔斷器(circuit-breaker)是否打開,如果打開跳到步驟8,進行降級策略,如果關閉進入步驟.
4:判斷線程池/隊列/信號量是否跑滿,如果跑滿進入降級步驟8,否則繼續后續步驟.
5:調用HystrixCommand的run方法.運行依賴邏輯
5a:依賴邏輯調用超時,進入步驟8.
6:判斷邏輯是否調用成功
6a:返回成功調用結果
6b:調用出錯,進入步驟8.
7:計算熔斷器狀態,所有的運行狀態(成功, 失敗, 拒絕,超時)上報給熔斷器,用於統計從而判斷熔斷器狀態.
8:getFallback()降級邏輯.
以下四種情況將觸發getFallback調用:
(1):run()方法拋出非HystrixBadRequestException異常。
(2):run()方法調用超時
(3):熔斷器開啟攔截調用
(4):線程池/隊列/信號量是否跑滿
8a:沒有實現getFallback的Command將直接拋出異常
8b:fallback降級邏輯調用成功直接返回
8c:降級邏輯調用失敗拋出異常
9:返回執行成功結果
2):熔斷器:Circuit Breaker
Circuit Breaker 流程架構和統計

每個熔斷器默認維護10個bucket,每秒一個bucket,每個blucket記錄成功,失敗,超時,拒絕的狀態,
默認錯誤超過50%且10秒內超過20個請求進行中斷攔截.
3)隔離(Isolation)分析
Hystrix隔離方式采用線程/信號的方式,通過隔離限制依賴的並發量和阻塞擴散.
(1):線程隔離
把執行依賴代碼的線程與請求線程(如:jetty線程)分離,請求線程可以自由控制離開的時間(異步過程)。
通過線程池大小可以控制並發量,當線程池飽和時可以提前拒絕服務,防止依賴問題擴散。
線上建議線程池不要設置過大,否則大量堵塞線程有可能會拖慢服務器。

(2):線程隔離的優缺點
線程隔離的優點:
[1]:使用線程可以完全隔離第三方代碼,請求線程可以快速放回。
[2]:當一個失敗的依賴再次變成可用時,線程池將清理,並立即恢復可用,而不是一個長時間的恢復。
[3]:可以完全模擬異步調用,方便異步編程。
線程隔離的缺點:
[1]:線程池的主要缺點是它增加了cpu,因為每個命令的執行涉及到排隊(默認使用SynchronousQueue避免排隊),調度和上下文切換。
[2]:對使用ThreadLocal等依賴線程狀態的代碼增加復雜性,需要手動傳遞和清理線程狀態。
NOTE: Netflix公司內部認為線程隔離開銷足夠小,不會造成重大的成本或性能的影響。
Netflix 內部API 每天100億的HystrixCommand依賴請求使用線程隔,每個應用大約40多個線程池,每個線程池大約5-20個線程。
(3):信號隔離
信號隔離也可以用於限制並發訪問,防止阻塞擴散, 與線程隔離最大不同在於執行依賴代碼的線程依然是請求線程(該線程需要通過信號申請),
如果客戶端是可信的且可以快速返回,可以使用信號隔離替換線程隔離,降低開銷.
線程隔離與信號隔離區別如下圖:

解析圖片出自官網wiki , 更多內容請見官網: https://github.com/Netflix/Hystrix

