一、認識Hystrix
Hystrix是Netflix開源的一款容錯框架贺纲,包含常用的容錯方法:線程池隔離、信號量隔離褪测、熔斷猴誊、降級回退。在高并發(fā)訪問下侮措,系統(tǒng)所依賴的服務(wù)的穩(wěn)定性對系統(tǒng)的影響非常大懈叹,依賴有很多不可控的因素,比如網(wǎng)絡(luò)連接變慢分扎,資源突然繁忙澄成,暫時不可用,服務(wù)脫機等笆包。我們要構(gòu)建穩(wěn)定环揽、可靠的分布式系統(tǒng),就必須要有這樣一套容錯方法庵佣。
本文將逐一分析線程池隔離歉胶、信號量隔離、熔斷巴粪、降級回退這四種技術(shù)的原理與實踐通今。
二、線程隔離
2.1為什么要做線程隔離
比如我們現(xiàn)在有3個業(yè)務(wù)調(diào)用分別是查詢訂單肛根、查詢商品辫塌、查詢用戶,且這三個業(yè)務(wù)請求都是依賴第三方服務(wù)-訂單服務(wù)派哲、商品服務(wù)臼氨、用戶服務(wù)。三個服務(wù)均是通過RPC調(diào)用芭届。當查詢訂單服務(wù)储矩,假如線程阻塞了感耙,這個時候后續(xù)有大量的查詢訂單請求過來,那么容器中的線程數(shù)量則會持續(xù)增加直致CPU資源耗盡到100%持隧,整個服務(wù)對外不可用即硼,集群環(huán)境下就是雪崩。如下圖
:
2.2屡拨、線程隔離-線程池
2.2.1只酥、Hystrix是如何通過線程池實現(xiàn)線程隔離的
Hystrix通過命令模式,將每個類型的業(yè)務(wù)請求封裝成對應(yīng)的命令請求呀狼,比如查詢訂單->訂單Command裂允,查詢商品->商品Command,查詢用戶->用戶Command哥艇。每個類型的Command對應(yīng)一個線程池叫胖。創(chuàng)建好的線程池是被放入到ConcurrentHashMap中,比如查詢訂單:
final static ConcurrentHashMap<String, HystrixThreadPool> threadPools = new ConcurrentHashMap<String, HystrixThreadPool>();
threadPools.put(“hystrix-order”, new HystrixThreadPoolDefault(threadPoolKey, propertiesBuilder));
當?shù)诙尾樵冇唵握埱筮^來的時候她奥,則可以直接從Map中獲取該線程池。具體流程如下圖:
創(chuàng)建線程池中的線程的方法怎棱,查看源代碼如下:
執(zhí)行Command的方式一共四種哩俭,直接看官方文檔(https://github.com/Netflix/Hystrix/wiki/How-it-Works),具體區(qū)別如下:
execute():以同步堵塞方式執(zhí)行run()拳恋。調(diào)用execute()后凡资,hystrix先創(chuàng)建一個新線程運行run(),接著調(diào)用程序要在execute()調(diào)用處一直堵塞著谬运,直到run()運行完成隙赁。
queue():以異步非堵塞方式執(zhí)行run()。調(diào)用queue()就直接返回一個Future對象梆暖,同時hystrix創(chuàng)建一個新線程運行run()伞访,調(diào)用程序通過Future.get()拿到run()的返回結(jié)果,而Future.get()是堵塞執(zhí)行的轰驳。
observe():事件注冊前執(zhí)行run()/construct()厚掷。第一步是事件注冊前,先調(diào)用observe()自動觸發(fā)執(zhí)行run()/construct()(如果繼承的是HystrixCommand级解,hystrix將創(chuàng)建新線程非堵塞執(zhí)行run()冒黑;如果繼承的是HystrixObservableCommand,將以調(diào)用程序線程堵塞執(zhí)行construct())勤哗,第二步是從observe()返回后調(diào)用程序調(diào)用subscribe()完成事件注冊抡爹,如果run()/construct()執(zhí)行成功則觸發(fā)onNext()和onCompleted(),如果執(zhí)行異常則觸發(fā)onError()芒划。
toObservable():事件注冊后執(zhí)行run()/construct()冬竟。第一步是事件注冊前欧穴,調(diào)用toObservable()就直接返回一個Observable<String>對象,第二步調(diào)用subscribe()完成事件注冊后自動觸發(fā)執(zhí)行run()/construct()(如果繼承的是HystrixCommand诱咏,hystrix將創(chuàng)建新線程非堵塞執(zhí)行run()苔可,調(diào)用程序不必等待run();如果繼承的是HystrixObservableCommand袋狞,將以調(diào)用程序線程堵塞執(zhí)行construct()焚辅,調(diào)用程序等待construct()執(zhí)行完才能繼續(xù)往下走),如果run()/construct()執(zhí)行成功則觸發(fā)onNext()和onCompleted()苟鸯,如果執(zhí)行異常則觸發(fā)onError()
注:
execute()和queue()是HystrixCommand中的方法同蜻,observe()和toObservable()是HystrixObservableCommand 中的方法。從底層實現(xiàn)來講早处,HystrixCommand其實也是利用Observable實現(xiàn)的(如果我們看Hystrix的源碼的話湾蔓,可以發(fā)現(xiàn)里面大量使用了RxJava),雖然HystrixCommand只返回單個的結(jié)果砌梆,但HystrixCommand的queue方法實際上是調(diào)用了toObservable().toBlocking().toFuture()默责,而execute方法實際上是調(diào)用了queue().get()。
2.2.2咸包、如何應(yīng)用到實際代碼中
2.2.3桃序、線程隔離-線程池小結(jié)
執(zhí)行依賴代碼的線程與請求線程(比如Tomcat線程)分離,請求線程可以自由控制離開的時間烂瘫,這也是我們通常說的異步編程媒熊,Hystrix是結(jié)合RxJava來實現(xiàn)的異步編程。通過設(shè)置線程池大小來控制并發(fā)訪問量坟比,當線程飽和的時候可以拒絕服務(wù)芦鳍,防止依賴問題擴散。
線程池隔離的優(yōu)點:
[1]:應(yīng)用程序會被完全保護起來葛账,即使依賴的一個服務(wù)的線程池滿了柠衅,也不會影響到應(yīng)用程序的其他部分。
[2]:我們給應(yīng)用程序引入一個新的風(fēng)險較低的客戶端lib的時候籍琳,如果發(fā)生問題茄茁,也是在本lib中,并不會影響到其他內(nèi)容巩割,因此我們可以大膽的引入新lib庫裙顽。
[3]:當依賴的一個失敗的服務(wù)恢復(fù)正常時,應(yīng)用程序會立即恢復(fù)正常的性能宣谈。
[4]:如果我們的應(yīng)用程序一些參數(shù)配置錯誤了愈犹,線程池的運行狀況將會很快顯示出來,比如延遲、超時漩怎、拒絕等勋颖。同時可以通過動態(tài)屬性實時執(zhí)行來處理糾正錯誤的參數(shù)配置。
[5]:如果服務(wù)的性能有變化勋锤,從而需要調(diào)整饭玲,比如增加或者減少超時時間,更改重試次數(shù)叁执,就可以通過線程池指標動態(tài)屬性修改茄厘,而且不會影響到其他調(diào)用請求。
[6]:除了隔離優(yōu)勢外谈宛,hystrix擁有專門的線程池可提供內(nèi)置的并發(fā)功能次哈,使得可以在同步調(diào)用之上構(gòu)建異步的外觀模式,這樣就可以很方便的做異步編程(Hystrix引入了Rxjava異步框架)吆录。
盡管線程池提供了線程隔離窑滞,我們的客戶端底層代碼也必須要有超時設(shè)置,不能無限制的阻塞以致線程池一直飽和恢筝。
線程池隔離的缺點:
[1]:線程池的主要缺點就是它增加了計算的開銷哀卫,每個業(yè)務(wù)請求(被包裝成命令)在執(zhí)行的時候,會涉及到請求排隊撬槽,調(diào)度和上下文切換聊训。不過Netflix公司內(nèi)部認為線程隔離開銷足夠小,不會產(chǎn)生重大的成本或性能的影響恢氯。
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執(zhí)行。 每個API實例都有40多個線程池鼓寺,每個線程池中有5-20個線程(大多數(shù)設(shè)置為10個)勋拟。
對于不依賴網(wǎng)絡(luò)訪問的服務(wù),比如只依賴內(nèi)存緩存這種情況下妈候,就不適合用線程池隔離技術(shù)敢靡,而是采用信號量隔離。
2.3苦银、線程隔離-信號量啸胧。
2.3.1、線程池和信號量的區(qū)別
上面談到了線程池的缺點幔虏,當我們依賴的服務(wù)是極低延遲的纺念,比如訪問內(nèi)存緩存,就沒有必要使用線程池的方式想括,那樣的話開銷得不償失陷谱,而是推薦使用信號量這種方式。下面這張圖說明了線程池隔離和信號量隔離的主要區(qū)別:線程池方式下業(yè)務(wù)請求線程和執(zhí)行依賴的服務(wù)的線程不是同一個線程;信號量方式下業(yè)務(wù)請求線程和執(zhí)行依賴服務(wù)的線程是同一個線程
2.3.2烟逊、如何使用信號量來隔離線程
將屬性execution.isolation.strategy設(shè)置為SEMAPHORE 渣窜,象這樣 ExecutionIsolationStrategy.SEMAPHORE,則Hystrix使用信號量而不是默認的線程池來做隔離宪躯。
2.3.4乔宿、線程隔離-信號量小結(jié)
信號量隔離的方式是限制了總的并發(fā)數(shù),每一次請求過來访雪,請求線程和調(diào)用依賴服務(wù)的線程是同一個線程详瑞,那么如果不涉及遠程RPC調(diào)用(沒有網(wǎng)絡(luò)開銷)則使用信號量來隔離,更為輕量冬阳,開銷更小蛤虐。
三、熔斷
3.1肝陪、熔斷器(Circuit Breaker)介紹
熔斷器驳庭,現(xiàn)實生活中有一個很好的類比,就是家庭電路中都會安裝一個保險盒氯窍,當電流過大的時候保險盒里面的保險絲會自動斷掉饲常,來保護家里的各種電器及電路。Hystrix中的熔斷器(Circuit Breaker)也是起到這樣的作用狼讨,Hystrix在運行過程中會向每個commandKey對應(yīng)的熔斷器報告成功贝淤、失敗、超時和拒絕的狀態(tài)政供,熔斷器維護計算統(tǒng)計的數(shù)據(jù)播聪,根據(jù)這些統(tǒng)計的信息來確定熔斷器是否打開。如果打開布隔,后續(xù)的請求都會被截斷离陶。然后會隔一段時間默認是5s,嘗試半開衅檀,放入一部分流量請求進來招刨,相當于對依賴服務(wù)進行一次健康檢查,如果恢復(fù)哀军,熔斷器關(guān)閉沉眶,隨后完全恢復(fù)調(diào)用。如下圖:
說明杉适,上面說的commandKey谎倔,就是在初始化的時候設(shè)置的andCommandKey(HystrixCommandKey.Factory.asKey("testCommandKey"))
再來看下熔斷器在整個Hystrix流程圖中的位置,從步驟4開始猿推,如下圖:
Hystrix會檢查Circuit Breaker的狀態(tài)传藏。如果Circuit Breaker的狀態(tài)為開啟狀態(tài),Hystrix將不會執(zhí)行對應(yīng)指令,而是直接進入失敗處理狀態(tài)(圖中8 Fallback)毯侦。如果Circuit Breaker的狀態(tài)為關(guān)閉狀態(tài)哭靖,Hystrix會繼續(xù)進行線程池、任務(wù)隊列侈离、信號量的檢查(圖中5)
3.2试幽、如何使用熔斷器(Circuit Breaker)
由于Hystrix是一個容錯框架,因此我們在使用的時候卦碾,要達到熔斷的目的只需配置一些參數(shù)就可以了铺坞。但我們要達到真正的效果,就必須要了解這些參數(shù)洲胖。Circuit Breaker一共包括如下6個參數(shù)济榨。
1、circuitBreaker.enabled
是否啟用熔斷器绿映,默認是TURE擒滑。
2、circuitBreaker.forceOpen
熔斷器強制打開叉弦,始終保持打開狀態(tài)丐一。默認值FLASE。
3淹冰、circuitBreaker.forceClosed
熔斷器強制關(guān)閉库车,始終保持關(guān)閉狀態(tài)。默認值FLASE樱拴。
4柠衍、circuitBreaker.errorThresholdPercentage
設(shè)定錯誤百分比,默認值50%晶乔,例如一段時間(10s)內(nèi)有100個請求珍坊,其中有55個超時或者異常返回了,那么這段時間內(nèi)的錯誤百分比是55%瘪弓,大于了默認值50%,這種情況下觸發(fā)熔斷器-打開禽最。
5腺怯、circuitBreaker.requestVolumeThreshold
默認值20.意思是至少有20個請求才進行errorThresholdPercentage錯誤百分比計算。比如一段時間(10s)內(nèi)有19個請求全部失敗了川无。錯誤百分比是100%呛占,但熔斷器不會打開,因為requestVolumeThreshold的值是20. 這個參數(shù)非常重要懦趋,熔斷器是否打開首先要滿足這個條件晾虑,源代碼如下
6、circuitBreaker.sleepWindowInMilliseconds
半開試探休眠時間,默認值5000ms帜篇。當熔斷器開啟一段時間之后比如5000ms糙捺,會嘗試放過去一部分流量進行試探,確定依賴服務(wù)是否恢復(fù)笙隙。
測試代碼(模擬10次調(diào)用洪灯,錯誤百分比為5%的情況下,打開熔斷器開關(guān)竟痰。):
測試結(jié)果:
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后熔斷器關(guān)閉
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分析
Factory 是一個工廠類,提供HystrixCircuitBreaker實例
HystrixCircuitBreakerImpl是HystrixCircuitBreaker的實現(xiàn)坏快,allowRequest()铅檩、isOpen()、markSuccess()都會在HystrixCircuitBreakerImpl有默認的實現(xiàn)莽鸿。
3.4昧旨、熔斷器小結(jié)
每個熔斷器默認維護10個bucket,每秒一個bucket,每個blucket記錄成功,失敗,超時,拒絕的狀態(tài),默認錯誤超過50%且10秒內(nèi)超過20個請求進行中斷攔截富拗。下圖顯示HystrixCommand或HystrixObservableCommand如何與HystrixCircuitBreaker及其邏輯和決策流程進行交互臼予,包括計數(shù)器在斷路器中的行為。
四啃沪、回退降級
4.1粘拾、降級
所謂降級,就是指在在Hystrix執(zhí)行非核心鏈路功能失敗的情況下创千,我們?nèi)绾翁幚礴止停热缥覀兎祷啬J值等。如果我們要回退或者降級處理追驴,代碼上需要實現(xiàn)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";
}
}
如果我們實現(xiàn)的是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 返回默認值
回退的時候返回靜態(tài)嵌入代碼中的默認值,這樣就不會導(dǎo)致功能以Fail Silent的方式被清楚亏镰,也就是用戶看不到任何功能了扯旷。而是按照一個默認的方式顯示。
@Override
protected Boolean getFallback() {
return true;
}
@Override
protected Observable<Boolean> resumeWithFallback() {
return Observable.just( true );
}
4.2.4索抓、Fallback: Stubbed 自己組裝一個值返回
當我們執(zhí)行返回的結(jié)果是一個包含多個字段的對象時钧忽,則會以Stubbed 的方式回退毯炮。Stubbed 值我們建議在實例化Command的時候就設(shè)置好一個值。以countryCodeFromGeoLookup為例耸黑,countryCodeFromGeoLookup的值桃煎,是在我們調(diào)用的時候就注冊進來初始化好的。CommandWithStubbedFallback command = new CommandWithStubbedFallback(1234, "china");主要代碼如下:
4.2.5崎坊、Fallback: Cache via Network 利用遠程緩存
通過遠程緩存的方式备禀。在失敗的情況下再發(fā)起一次remote請求,不過這次請求的是一個緩存比如redis奈揍。由于是又發(fā)起一起遠程調(diào)用曲尸,所以會重新封裝一次Command,這個時候要注意,執(zhí)行fallback的線程一定要跟主線程區(qū)分開,也就是重新命名一個ThreadPoolKey衅谷。
4.2.6缤弦、Primary + Secondary with Fallback 主次方式回退(主要和次要)
這個有點類似我們?nèi)粘i_發(fā)中需要上線一個新功能,但為了防止新功能上線失敗可以回退到老的代碼,我們會做一個開關(guān)比如使用zookeeper做一個配置開關(guān),可以動態(tài)切換到老代碼功能。那么Hystrix它是使用通過一個配置來在兩個command中進行切換鹏倘。
4.3、回退降級小結(jié)
降級的處理方式顽爹,返回默認值纤泵,返回緩存里面的值(包括遠程緩存比如redis和本地緩存比如jvmcache)。
但回退的處理方式也有不適合的場景:
1镜粤、寫操作
2捏题、批處理
3、計算
以上幾種情況如果失敗肉渴,則程序就要將錯誤返回給調(diào)用者公荧。
總結(jié)
Hystrix為我們提供了一套線上系統(tǒng)容錯的技術(shù)實踐方法,我們通過在系統(tǒng)中引入Hystrix的jar包可以很方便的使用線程隔離同规、熔斷循狰、回退等技術(shù)。同時它還提供了監(jiān)控頁面配置券勺,方便我們管理查看每個接口的調(diào)用情況绪钥。像spring cloud這種微服務(wù)構(gòu)建模式中也引入了Hystrix,我們可以放心使用Hystrix的線程隔離技術(shù)朱灿,來防止雪崩這種可怕的致命性線上故障昧识。
轉(zhuǎn)載請注明出處钠四,并附上鏈接 http://www.reibang.com/p/3e11ac385c73
參考資料:
https://github.com/Netflix/Hystrix/wiki
《億級流量網(wǎng)站架構(gòu)核心技術(shù)》一書