王新棟 | Hystrix技術解析
一、認識Hystrix
Hystrix是Netflix開源的一款容錯框架,包含常用的容錯方法:線程池隔離、信號量隔離、熔斷、降級回退。在高並發訪問下,係統所依賴的服務的穩定性對係統的影響非常大,依賴有很多不可控的因素,比如網絡連接變慢,資源突然繁忙,暫時不可用,服務脫機等。我們要構建穩定、可靠的分布式係統,就必須要有這樣一套容錯方法。
本文將逐一分析線程池隔離、信號量隔離、熔斷、降級回退這四種技術的原理與實踐。
二、線程隔離
2.1為什麼要做線程隔離
比如我們現在有3個業務調用分別是查詢訂單、查詢商品、查詢用戶,且這三個業務請求都是依賴第三方服務-訂單服務、商品服務、用戶服務。三個服務均是通過RPC調用。當查詢訂單服務,假如線程阻塞了,這個時候後續有大量的查詢訂單請求過來,那麼容器中的線程數量則會持續增加直致CPU資源耗盡到100%,整個服務對外不可用,集群環境下就是雪崩。如下圖
2.2、線程隔離-線程池
2.2.1、Hystrix是如何通過線程池實現線程隔離的
Hystrix通過命令模式,將每個類型的業務請求封裝成對應的命令請求,比如查詢訂單->訂單Command,查詢商品->商品Command,查詢用戶->用戶Command。每個類型的Command對應一個線程池。創建好的線程池是被放入到ConcurrentHashMap中,比如查詢訂單:
final static ConcurrentHashMap<String, HystrixThreadPool> threadPools = new ConcurrentHashMap<String, HystrixThreadPool>();
threadPools.put(“hystrix-order”, new HystrixThreadPoolDefault(threadPoolKey, propertiesBuilder));
當第二次查詢訂單請求過來的時候,則可以直接從Map中獲取該線程池。具體流程如下圖:
hystrix線程執行過程和異步化
創建線程池中的線程的方法,查看源代碼如下:
創建線程池中的線程
執行Command的方式一共四種,直接看官方文檔(https://github.com/Netflix/Hystrix/wiki/How-it-Works ),具體區別如下:
• execute():以同步堵塞方式執行run()。調用execute()後,hystrix先創建一個新線程運行run(),接著調用程序要在execute()調用處一直堵塞著,直到run()運行完成。
• queue():以異步非堵塞方式執行run()。調用queue()就直接返回一個Future對象,同時hystrix創建一個新線程運行run(),調用程序通過Future.get()拿到run()的返回結果,而Future.get()是堵塞執行的。
• observe():事件注冊前執行run()/construct()。第一步是事件注冊前,先調用observe()自動觸發執行run()/construct()(如果繼承的是HystrixCommand,hystrix將創建新線程非堵塞執行run();如果繼承的是HystrixObservableCommand,將以調用程序線程堵塞執行construct()),第二步是從observe()返回後調用程序調用subscribe()完成事件注冊,如果run()/construct()執行成功則觸發onNext()和onCompleted(),如果執行異常則觸發onError()。
• toObservable():事件注冊後執行run()/construct()。第一步是事件注冊前,調用toObservable()就直接返回一個Observable對象,第二步調用subscribe()完成事件注冊後自動觸發執行run()/construct()(如果繼承的是HystrixCommand,hystrix將創建新線程非堵塞執行run(),調用程序不必等待run();如果繼承的是HystrixObservableCommand,將以調用程序線程堵塞執行construct(),調用程序等待construct()執行完才能繼續往下走),如果run()/construct()執行成功則觸發onNext()和onCompleted(),如果執行異常則觸發onError()
注:
execute()和queue()是HystrixCommand中的方法,observe()和toObservable()是HystrixObservableCommand 中的方法。從底層實現來講,HystrixCommand其實也是利用Observable實現的(如果我們看Hystrix的源碼的話,可以發現裏麵大量使用了RxJava),雖然HystrixCommand隻返回單個的結果,但HystrixCommand的queue方法實際上是調用了toObservable().toBlocking().toFuture(),而execute方法實際上是調用了queue().get()。
2.2.3、線程隔離-線程池小結
執行依賴代碼的線程與請求線程(比如Tomcat線程)分離,請求線程可以自由控製離開的時間,這也是我們通常說的異步編程,Hystrix是結合RxJava來實現的異步編程。通過設置線程池大小來控製並發訪問量,當線程飽和的時候可以拒絕服務,防止依賴問題擴散。
線程隔離
線程池隔離的優點:
盡管線程池提供了線程隔離,我們的客戶端底層代碼也必須要有超時設置,不能無限製的阻塞以致線程池一直飽和。
線程池隔離的缺點:
The Netflix API processes 10+ billion Hystrix Command executions per day using thread isolation. Each API instance has 40+ thread-pools with 5–20 threads in each (most are set to 10).
Netflix API每天使用線程隔離處理10億次Hystrix Command執行。 每個API實例都有40多個線程池,每個線程池中有5-20個線程(大多數設置為10個)。
對於不依賴網絡訪問的服務,比如隻依賴內存緩存這種情況下,就不適合用線程池隔離技術,而是采用信號量隔離。
2.3、線程隔離-信號量
2.3.1、線程池和信號量的區別
上麵談到了線程池的缺點,當我們依賴的服務是極低延遲的,比如訪問內存緩存,就沒有必要使用線程池的方式,那樣的話開銷得不償失,而是推薦使用信號量這種方式。下麵這張圖說明了線程池隔離和信號量隔離的主要區別:線程池方式下業務請求線程和執行依賴的服務的線程不是同一個線程;信號量方式下業務請求線程和執行依賴服務的線程是同一個線程
將屬性execution.isolation.strategy設置為SEMAPHORE ,象這樣 ExecutionIsolationStrategy.SEMAPHORE,則Hystrix使用信號量而不是默認的線程池來做隔離。
信號量隔離的方式是限製了總的並發數,每一次請求過來,請求線程和調用依賴服務的線程是同一個線程,那麼如果不涉及遠程RPC調用(沒有網絡開銷)則使用信號量來隔離,更為輕量,開銷更小。
三、熔斷
3.1、熔斷器(Circuit Breaker)介紹
熔斷器,現實生活中有一個很好的類比,就是家庭電路中都會安裝一個保險盒,當電流過大的時候保險盒裏麵的保險絲會自動斷掉,來保護家裏的各種電器及電路。Hystrix中的熔斷器(Circuit Breaker)也是起到這樣的作用,Hystrix在運行過程中會向每個commandKey對應的熔斷器報告成功、失敗、超時和拒絕的狀態,熔斷器維護計算統計的數據,根據這些統計的信息來確定熔斷器是否打開。如果打開,後續的請求都會被截斷。然後會隔一段時間默認是5s,嚐試半開,放入一部分流量請求進來,相當於對依賴服務進行一次健康檢查,如果恢複,熔斷器關閉,隨後完全恢複調用。如下圖:
說明,上麵說的commandKey,就是在初始化的時候設置的andCommandKey(HystrixCommandKey.Factory.asKey("testCommandKey"))
再來看下熔斷器在整個Hystrix流程圖中的位置,從步驟4開始,如下圖:
Hystrix流程圖
Hystrix會檢查Circuit Breaker的狀態。如果Circuit Breaker的狀態為開啟狀態,Hystrix將不會執行對應指令,而是直接進入失敗處理狀態(圖中8 Fallback)。如果Circuit Breaker的狀態為關閉狀態,Hystrix會繼續進行線程池、任務隊列、信號量的檢查(圖中5)
3.2、如何使用熔斷器(Circuit Breaker)
由於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,會嚐試放過去一部分流量進行試探,確定依賴服務是否恢複。
測試代碼(模擬10次調用,錯誤百分比為5%的情況下,打開熔斷器開關。):
測試結果:
call times:1 result:fallback: isCircuitBreakerOpen: false
call times:2 result:running: isCircuitBreakerOpen: false
call times:3 result:running: isCircuitBreakerOpen: false
call times:4 result:fallback: isCircuitBreakerOpen: false
call times:5 result:running: isCircuitBreakerOpen: false
call times:6 result:fallback: isCircuitBreakerOpen: false
call times:7 result:fallback: isCircuitBreakerOpen: false
call times:8 result:fallback: isCircuitBreakerOpen: false
call times:9 result:fallback: isCircuitBreakerOpen: false
call times:10 result:fallback: isCircuitBreakerOpen: false
熔斷器打開
call times:11 result:fallback: isCircuitBreakerOpen: true
call times:12 result:fallback: isCircuitBreakerOpen: true
call times:13 result:fallback: isCircuitBreakerOpen: true
call times:14 result:fallback: isCircuitBreakerOpen: true
call times:15 result:fallback: isCircuitBreakerOpen: true
call times:16 result:fallback: isCircuitBreakerOpen: true
call times:17 result:fallback: isCircuitBreakerOpen: true
call times:18 result:fallback: isCircuitBreakerOpen: true
call times:19 result:fallback: isCircuitBreakerOpen: true
call times:20 result:fallback: isCircuitBreakerOpen: true
5s後熔斷器關閉
call times:21 result:running: isCircuitBreakerOpen: false
call times:22 result:running: isCircuitBreakerOpen: false
call times:23 result:fallback: isCircuitBreakerOpen: false
call times:24 result:running: isCircuitBreakerOpen: false
call times:25 result:running: isCircuitBreakerOpen: false
3.3、熔斷器(Circuit Breaker)源代碼HystrixCircuitBreaker.java分析
HystrixCircuitBreaker.java
Factory 是一個工廠類,提供HystrixCircuitBreaker實例
Factory源碼解析
HystrixCircuitBreakerImpl是HystrixCircuitBreaker的實現,allowRequest()、isOpen()、markSuccess()都會在HystrixCircuitBreakerImpl有默認的實現。
HystrixCircuitBreakerImpl-allowSingleTest()
HystrixCircuitBreakerImpl-isOpen()
3.4、熔斷器小結
每個熔斷器默認維護10個bucket,每秒一個bucket,每個blucket記錄成功,失敗,超時,拒絕的狀態,默認錯誤超過50%且10秒內超過20個請求進行中斷攔截。下圖顯示HystrixCommand或HystrixObservableCommand如何與HystrixCircuitBreaker及其邏輯和決策流程進行交互,包括計數器在斷路器中的行為。
四、回退降級
4.1、降級
所謂降級,就是指在在Hystrix執行非核心鏈路功能失敗的情況下,我們如何處理,比如我們返回默認值等。如果我們要回退或者降級處理,代碼上需要實現HystrixCommand.getFallback()方法或者是HystrixObservableCommand. HystrixObservableCommand()。
4.2、Hystrix的降級回退方式
Hystrix一共有如下幾種降級回退模式:
4.2.1、Fail Fast 快速失敗
@Override
protected String run() {
if (throwException) {
throw new RuntimeException("failure from CommandThatFailsFast");
} else {
return "success";
}
}
如果我們實現的是HystrixObservableCommand.java則 重寫 resumeWithFallback方法
@Override
protected Observable<String> resumeWithFallback() {
if (throwException) {
return Observable.error(new Throwable("failure from CommandThatFailsFast"));
} else {
return Observable.just("success");
}
}
4.2.2、Fail Silent 無聲失敗
返回null,空Map,空List
@Override
protected String getFallback() {
return null;
}
@Override
protected List<String> getFallback() {
return Collections.emptyList();
}
@Override
protected Observable<String> resumeWithFallback() {
return Observable.empty();
}
4.2.3、Fallback: Static 返回默認值
回退的時候返回靜態嵌入代碼中的默認值,這樣就不會導致功能以Fail Silent的方式被清楚,也就是用戶看不到任何功能了。而是按照一個默認的方式顯示。
@Override
protected Boolean getFallback() {
return true;
}
@Override
protected Observable<Boolean> resumeWithFallback() {
return Observable.just( true );
}
4.2.4、Fallback: Stubbed 自己組裝一個值返回
當我們執行返回的結果是一個包含多個字段的對象時,則會以Stubbed 的方式回退。Stubbed 值我們建議在實例化Command的時候就設置好一個值。以countryCodeFromGeoLookup為例,countryCodeFromGeoLookup的值,是在我們調用的時候就注冊進來初始化好的。CommandWithStubbedFallback command = new CommandWithStubbedFallback(1234, "china");主要代碼如下:
CommandWithStubbedFallback
4.2.5、Fallback: Cache via Network 利用遠程緩存
通過遠程緩存的方式。在失敗的情況下再發起一次remote請求,不過這次請求的是一個緩存比如redis。由於是又發起一起遠程調用,所以會重新封裝一次Command,這個時候要注意,執行fallback的線程一定要跟主線程區分開,也就是重新命名一個ThreadPoolKey。
Cache via Network
4.2.6、Primary + Secondary with Fallback 主次方式回退(主要和次要)
這個有點類似我們日常開發中需要上線一個新功能,但為了防止新功能上線失敗可以回退到老的代碼,我們會做一個開關比如使用zookeeper做一個配置開關,可以動態切換到老代碼功能。那麼Hystrix它是使用通過一個配置來在兩個command中進行切換。
Primary + Secondary with Fallback
CommandFacadeWithPrimarySecondary-1
CommandFacadeWithPrimarySecondary-2
CommandFacadeWithPrimarySecondary-3
4.3、回退降級小結
降級的處理方式,返回默認值,返回緩存裏麵的值(包括遠程緩存比如redis和本地緩存比如jvmcache)。
但回退的處理方式也有不適合的場景:
1、寫操作
2、批處理
3、計算
以上幾種情況如果失敗,則程序就要將錯誤返回給調用者。
總結
Hystrix為我們提供了一套線上係統容錯的技術實踐方法,我們通過在係統中引入Hystrix的jar包可以很方便的使用線程隔離、熔斷、回退等技術。同時它還提供了監控頁麵配置,方便我們管理查看每個接口的調用情況。像spring cloud這種微服務構建模式中也引入了Hystrix,我們可以放心使用Hystrix的線程隔離技術,來防止雪崩這種可怕的致命性線上故障。
作者:王新棟,目前就職於京東商城,負責京麥工作台的架構與開發。京麥工作台是京東商家唯一使用的店鋪運營管理平台。
來源:中生代技術
原文鏈接
最後更新:2017-08-30 15:03:12