原文:https://my.oschina.net/7001/blog/1619842
摘要: Hystrix是Netflix開源的一款容錯(cuò)框架,提供線程池隔離、信號(hào)量隔離异剥、熔斷、降級(jí)回退等容錯(cuò)方法。Hystrix為幫助我們構(gòu)建穩(wěn)定蚕泽、可靠的分布式系統(tǒng)提供了一種解決方案。
背景
分布式系統(tǒng)環(huán)境下桥嗤,服務(wù)間類似依賴非常常見须妻,一個(gè)業(yè)務(wù)調(diào)用通常依賴多個(gè)基礎(chǔ)服務(wù)。如下圖泛领,對(duì)于同步調(diào)用荒吏,當(dāng)庫(kù)存服務(wù)不可用時(shí),商品服務(wù)請(qǐng)求線程被阻塞渊鞋,當(dāng)有大批量請(qǐng)求調(diào)用庫(kù)存服務(wù)時(shí)绰更,最終可能導(dǎo)致整個(gè)商品服務(wù)對(duì)外不可用, 并且這種可不用可能沿請(qǐng)求調(diào)用鏈向上傳遞锡宋,這種現(xiàn)象被稱為雪崩效應(yīng)儡湾。
雪崩效應(yīng)常見場(chǎng)景
- 硬件故障:如服務(wù)器宕機(jī),機(jī)房斷電执俩,光纖被挖斷等徐钠。
- 流量激增:如異常流量,重試加大流量等役首。
- 緩存穿透:一般發(fā)生在應(yīng)用重啟尝丐,所有緩存失效時(shí)显拜,以及短時(shí)間內(nèi)大量緩存失效時(shí)。大量的緩存不命中爹袁,使請(qǐng)求直擊后端服務(wù)远荠,造成服務(wù)提供者超負(fù)荷運(yùn)行,引起服務(wù)不可用失息。
- 程序BUG:如程序邏輯導(dǎo)致內(nèi)存泄漏譬淳,JVM長(zhǎng)時(shí)間FullGC等。
- 同步等待:服務(wù)間采用同步調(diào)用模式根时,同步等待造成的資源耗盡瘦赫。
雪崩效應(yīng)應(yīng)對(duì)策略
針對(duì)造成雪崩效應(yīng)的不同場(chǎng)景,可以使用不同的應(yīng)對(duì)策略蛤迎,沒有一種通用所有場(chǎng)景的策略确虱,參考如下:
- 硬件故障:多機(jī)房容災(zāi)、異地多活等替裆。
- 流量激增:服務(wù)自動(dòng)擴(kuò)容校辩、流量控制(限流、關(guān)閉重試)等辆童。
- 緩存穿透:緩存預(yù)加載宜咒、緩存異步加載等。
- 程序BUG:修改程序bug把鉴、及時(shí)釋放資源等故黑。
- 同步等待:資源隔離、MQ解耦庭砍、不可用服務(wù)調(diào)用快速失敗等场晶。資源隔離通常指不同服務(wù)調(diào)用采用不同的線程池;不可用服務(wù)調(diào)用快速失敗一般通過(guò)超時(shí)機(jī)制怠缸,熔斷器以及熔斷后降級(jí)方法等方案實(shí)現(xiàn)诗轻。
綜上所述,如果一個(gè)應(yīng)用不能對(duì)來(lái)自依賴的故障進(jìn)行隔離揭北,那該應(yīng)用本身就處在被拖垮的風(fēng)險(xiǎn)中扳炬。 因此,為了構(gòu)建穩(wěn)定搔体、可靠的分布式系統(tǒng)恨樟,我們的服務(wù)應(yīng)當(dāng)具有自我保護(hù)能力,當(dāng)依賴服務(wù)不可用時(shí)疚俱,當(dāng)前服務(wù)啟動(dòng)自我保護(hù)功能厌杜,從而避免發(fā)生雪崩效應(yīng)。本文將重點(diǎn)介紹使用Hystrix解決同步等待的雪崩問題计螺。
初探Hystrix
Hystrix [h?st'r?ks]夯尽,中文含義是豪豬,因其背上長(zhǎng)滿棘刺登馒,從而擁有了自我保護(hù)的能力匙握。而Hystrix是Netflix開源的一款容錯(cuò)框架,同樣具有自我保護(hù)能力陈轿。為了實(shí)現(xiàn)容錯(cuò)和自我保護(hù)圈纺,下面我們看看Hystrix如何設(shè)計(jì)和實(shí)現(xiàn)的。
Hystrix設(shè)計(jì)目標(biāo):
- 對(duì)來(lái)自依賴的延遲和故障進(jìn)行防護(hù)和控制——這些依賴通常都是通過(guò)網(wǎng)絡(luò)訪問的
- 阻止故障的連鎖反應(yīng)
- 快速失敗并迅速恢復(fù)
- 回退并優(yōu)雅降級(jí)
- 提供近實(shí)時(shí)的監(jiān)控與告警
Hystrix遵循的設(shè)計(jì)原則:
- 防止任何單獨(dú)的依賴耗盡資源(線程)
- 過(guò)載立即切斷并快速失敗麦射,防止排隊(duì)
- 盡可能提供回退以保護(hù)用戶免受故障
- 使用隔離技術(shù)(例如隔板蛾娶,泳道和斷路器模式)來(lái)限制任何一個(gè)依賴的影響
- 通過(guò)近實(shí)時(shí)的指標(biāo),監(jiān)控和告警潜秋,確保故障被及時(shí)發(fā)現(xiàn)
- 通過(guò)動(dòng)態(tài)修改配置屬性蛔琅,確保故障及時(shí)恢復(fù)
- 防止整個(gè)依賴客戶端執(zhí)行失敗,而不僅僅是網(wǎng)絡(luò)通信
Hystrix如何實(shí)現(xiàn)這些設(shè)計(jì)目標(biāo)峻呛?
- 使用命令模式將所有對(duì)外部服務(wù)(或依賴關(guān)系)的調(diào)用包裝在HystrixCommand或HystrixObservableCommand對(duì)象中罗售,并將該對(duì)象放在單獨(dú)的線程中執(zhí)行;
- 每個(gè)依賴都維護(hù)著一個(gè)線程池(或信號(hào)量)钩述,線程池被耗盡則拒絕請(qǐng)求(而不是讓請(qǐng)求排隊(duì))寨躁。
- 記錄請(qǐng)求成功,失敗牙勘,超時(shí)和線程拒絕职恳。
- 服務(wù)錯(cuò)誤百分比超過(guò)了閾值,熔斷器開關(guān)自動(dòng)打開方面,一段時(shí)間內(nèi)停止對(duì)該服務(wù)的所有請(qǐng)求放钦。
- 請(qǐng)求失敗,被拒絕葡幸,超時(shí)或熔斷時(shí)執(zhí)行降級(jí)邏輯最筒。
- 近實(shí)時(shí)地監(jiān)控指標(biāo)和配置的修改。
Hystrix入門
Hystrix簡(jiǎn)單示例
開始深入Hystrix原理之前蔚叨,我們先簡(jiǎn)單看一個(gè)示例床蜘。
第一步,繼承HystrixCommand實(shí)現(xiàn)自己的command蔑水,在command的構(gòu)造方法中需要配置請(qǐng)求被執(zhí)行需要的參數(shù)邢锯,并組合實(shí)際發(fā)送請(qǐng)求的對(duì)象,代碼如下:
public class QueryOrderIdCommand extends HystrixCommand<Integer> {
private final static Logger logger = LoggerFactory.getLogger(QueryOrderIdCommand.class);
private OrderServiceProvider orderServiceProvider;
public QueryOrderIdCommand(OrderServiceProvider orderServiceProvider) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("orderService"))
.andCommandKey(HystrixCommandKey.Factory.asKey("queryByOrderId"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withCircuitBreakerRequestVolumeThreshold(10)//至少有10個(gè)請(qǐng)求搀别,熔斷器才進(jìn)行錯(cuò)誤率的計(jì)算
.withCircuitBreakerSleepWindowInMilliseconds(5000)//熔斷器中斷請(qǐng)求5秒后會(huì)進(jìn)入半打開狀態(tài),放部分流量過(guò)去重試
.withCircuitBreakerErrorThresholdPercentage(50)//錯(cuò)誤率達(dá)到50開啟熔斷保護(hù)
.withExecutionTimeoutEnabled(true))
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties
.Setter().withCoreSize(10)));
this.orderServiceProvider = orderServiceProvider;
}
@Override
protected Integer run() {
return orderServiceProvider.queryByOrderId();
}
@Override
protected Integer getFallback() {
return -1;
}
}
第二步丹擎,調(diào)用HystrixCommand的執(zhí)行方法發(fā)起實(shí)際請(qǐng)求。
@Test
public void testQueryByOrderIdCommand() {
Integer r = new QueryOrderIdCommand(orderServiceProvider).execute();
logger.info("result:{}", r);
}
Hystrix處理流程
Hystrix流程圖如下:
圖片來(lái)源Hystrix官網(wǎng)[https://github.com/Netflix/Hystrix/wiki](https://github.com/Netflix/Hystrix/wiki)
Hystrix整個(gè)工作流如下:
- 構(gòu)造一個(gè) HystrixCommand或HystrixObservableCommand對(duì)象,用于封裝請(qǐng)求蒂培,并在構(gòu)造方法配置請(qǐng)求被執(zhí)行需要的參數(shù)再愈;
- 執(zhí)行命令,Hystrix提供了4種執(zhí)行命令的方法护戳,后面詳述翎冲;
- 判斷是否使用緩存響應(yīng)請(qǐng)求,若啟用了緩存媳荒,且緩存可用抗悍,直接使用緩存響應(yīng)請(qǐng)求。Hystrix支持請(qǐng)求緩存钳枕,但需要用戶自定義啟動(dòng)缴渊;
- 判斷熔斷器是否打開,如果打開鱼炒,跳到第8步衔沼;
- 判斷線程池/隊(duì)列/信號(hào)量是否已滿,已滿則跳到第8步田柔;
- 執(zhí)行HystrixObservableCommand.construct()或HystrixCommand.run()俐巴,如果執(zhí)行失敗或者超時(shí),跳到第8步硬爆;否則欣舵,跳到第9步;
- 統(tǒng)計(jì)熔斷器監(jiān)控指標(biāo)缀磕;
- 走Fallback備用邏輯
- 返回請(qǐng)求響應(yīng)
從流程圖上可知道缘圈,第5步線程池/隊(duì)列/信號(hào)量已滿時(shí),還會(huì)執(zhí)行第7步邏輯袜蚕,更新熔斷器統(tǒng)計(jì)信息糟把,而第6步無(wú)論成功與否,都會(huì)更新熔斷器統(tǒng)計(jì)信息牲剃。
執(zhí)行命令的幾種方法
Hystrix提供了4種執(zhí)行命令的方法遣疯,execute()和queue() 適用于HystrixCommand對(duì)象,而observe()和toObservable()適用于HystrixObservableCommand對(duì)象凿傅。
execute()
以同步堵塞方式執(zhí)行run()缠犀,只支持接收一個(gè)值對(duì)象。hystrix會(huì)從線程池中取一個(gè)線程來(lái)執(zhí)行run()聪舒,并等待返回值辨液。
queue()
以異步非阻塞方式執(zhí)行run(),只支持接收一個(gè)值對(duì)象箱残。調(diào)用queue()就直接返回一個(gè)Future對(duì)象滔迈≈褂酰可通過(guò) Future.get()拿到run()的返回結(jié)果,但Future.get()是阻塞執(zhí)行的燎悍。若執(zhí)行成功敬惦,F(xiàn)uture.get()返回單個(gè)返回值。當(dāng)執(zhí)行失敗時(shí)间涵,如果沒有重寫fallback仁热,F(xiàn)uture.get()拋出異常。
observe()
事件注冊(cè)前執(zhí)行run()/construct()勾哩,支持接收多個(gè)值對(duì)象,取決于發(fā)射源举哟。調(diào)用observe()會(huì)返回一個(gè)hot Observable思劳,也就是說(shuō),調(diào)用observe()自動(dòng)觸發(fā)執(zhí)行run()/construct()妨猩,無(wú)論是否存在訂閱者潜叛。
如果繼承的是HystrixCommand,hystrix會(huì)從線程池中取一個(gè)線程以非阻塞方式執(zhí)行run()壶硅;如果繼承的是HystrixObservableCommand威兜,將以調(diào)用線程阻塞執(zhí)行construct()。
observe()使用方法:
- 調(diào)用observe()會(huì)返回一個(gè)Observable對(duì)象
- 調(diào)用這個(gè)Observable對(duì)象的subscribe()方法完成事件注冊(cè)庐椒,從而獲取結(jié)果
toObservable()
事件注冊(cè)后執(zhí)行run()/construct()椒舵,支持接收多個(gè)值對(duì)象,取決于發(fā)射源约谈。調(diào)用toObservable()會(huì)返回一個(gè)cold Observable笔宿,也就是說(shuō),調(diào)用toObservable()不會(huì)立即觸發(fā)執(zhí)行run()/construct()棱诱,必須有訂閱者訂閱Observable時(shí)才會(huì)執(zhí)行泼橘。
如果繼承的是HystrixCommand,hystrix會(huì)從線程池中取一個(gè)線程以非阻塞方式執(zhí)行run()迈勋,調(diào)用線程不必等待run()炬灭;如果繼承的是HystrixObservableCommand,將以調(diào)用線程堵塞執(zhí)行construct()重归,調(diào)用線程需等待construct()執(zhí)行完才能繼續(xù)往下走。
toObservable()使用方法:
- 調(diào)用observe()會(huì)返回一個(gè)Observable對(duì)象
- 調(diào)用這個(gè)Observable對(duì)象的subscribe()方法完成事件注冊(cè),從而獲取結(jié)果
需注意的是烤礁,HystrixCommand也支持toObservable()和observe()鲤脏,但是即使將HystrixCommand轉(zhuǎn)換成Observable阻问,它也只能發(fā)射一個(gè)值對(duì)象彻桃。只有HystrixObservableCommand才支持發(fā)射多個(gè)值對(duì)象改衩。
幾種方法的關(guān)系
- execute()實(shí)際是調(diào)用了queue().get()
- queue()實(shí)際調(diào)用了toObservable().toBlocking().toFuture()
- observe()實(shí)際調(diào)用toObservable()獲得一個(gè)cold Observable洽胶,再創(chuàng)建一個(gè)ReplaySubject對(duì)象訂閱Observable响逢,將源Observable轉(zhuǎn)化為hot Observable。因此調(diào)用observe()會(huì)自動(dòng)觸發(fā)執(zhí)行run()/construct()棕孙。
Hystrix總是以O(shè)bservable的形式作為響應(yīng)返回,不同執(zhí)行命令的方法只是進(jìn)行了相應(yīng)的轉(zhuǎn)換些膨。
Hystrix容錯(cuò)
Hystrix的容錯(cuò)主要是通過(guò)添加容許延遲和容錯(cuò)方法蟀俊,幫助控制這些分布式服務(wù)之間的交互。 還通過(guò)隔離服務(wù)之間的訪問點(diǎn)订雾,阻止它們之間的級(jí)聯(lián)故障以及提供回退選項(xiàng)來(lái)實(shí)現(xiàn)這一點(diǎn)肢预,從而提高系統(tǒng)的整體彈性。Hystrix主要提供了以下幾種容錯(cuò)方法:
- 資源隔離
- 熔斷
- 降級(jí)
下面我們?cè)敿?xì)談?wù)勥@幾種容錯(cuò)機(jī)制洼哎。
資源隔離
資源隔離主要指對(duì)線程的隔離烫映。Hystrix提供了兩種線程隔離方式:線程池和信號(hào)量。
線程隔離-線程池
Hystrix通過(guò)命令模式對(duì)發(fā)送請(qǐng)求的對(duì)象和執(zhí)行請(qǐng)求的對(duì)象進(jìn)行解耦噩峦,將不同類型的業(yè)務(wù)請(qǐng)求封裝為對(duì)應(yīng)的命令請(qǐng)求锭沟。如訂單服務(wù)查詢商品,查詢商品請(qǐng)求->商品Command识补;商品服務(wù)查詢庫(kù)存族淮,查詢庫(kù)存請(qǐng)求->庫(kù)存Command。并且為每個(gè)類型的Command配置一個(gè)線程池凭涂,當(dāng)?shù)谝淮蝿?chuàng)建Command時(shí)祝辣,根據(jù)配置創(chuàng)建一個(gè)線程池,并放入ConcurrentHashMap切油,如商品Command:
final static ConcurrentHashMap<String, HystrixThreadPool> threadPools = new ConcurrentHashMap<String, HystrixThreadPool>();
...
if (!threadPools.containsKey(key)) {
threadPools.put(key, new HystrixThreadPoolDefault(threadPoolKey, propertiesBuilder));
}
后續(xù)查詢商品的請(qǐng)求創(chuàng)建Command時(shí)蝙斜,將會(huì)重用已創(chuàng)建的線程池。線程池隔離之后的服務(wù)依賴關(guān)系:
通過(guò)將發(fā)送請(qǐng)求線程與執(zhí)行請(qǐng)求的線程分離澎胡,可有效防止發(fā)生級(jí)聯(lián)故障孕荠。當(dāng)線程池或請(qǐng)求隊(duì)列飽和時(shí)娩鹉,Hystrix將拒絕服務(wù),使得請(qǐng)求線程可以快速失敗岛琼,從而避免依賴問題擴(kuò)散底循。
線程池隔離優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 保護(hù)應(yīng)用程序以免受來(lái)自依賴故障的影響,指定依賴線程池飽和不會(huì)影響應(yīng)用程序的其余部分槐瑞。
- 當(dāng)引入新客戶端lib時(shí)熙涤,即使發(fā)生問題,也是在本lib中困檩,并不會(huì)影響到其他內(nèi)容祠挫。
- 當(dāng)依賴從故障恢復(fù)正常時(shí),應(yīng)用程序會(huì)立即恢復(fù)正常的性能悼沿。
- 當(dāng)應(yīng)用程序一些配置參數(shù)錯(cuò)誤時(shí)等舔,線程池的運(yùn)行狀況會(huì)很快檢測(cè)到這一點(diǎn)(通過(guò)增加錯(cuò)誤,延遲糟趾,超時(shí)归榕,拒絕等)锭碳,同時(shí)可以通過(guò)動(dòng)態(tài)屬性進(jìn)行實(shí)時(shí)糾正錯(cuò)誤的參數(shù)配置。
- 如果服務(wù)的性能有變化,需要實(shí)時(shí)調(diào)整墩莫,比如增加或者減少超時(shí)時(shí)間陈惰,更改重試次數(shù)壕鹉,可以通過(guò)線程池指標(biāo)動(dòng)態(tài)屬性修改担扑,而且不會(huì)影響到其他調(diào)用請(qǐng)求。
- 除了隔離優(yōu)勢(shì)外劫笙,hystrix擁有專門的線程池可提供內(nèi)置的并發(fā)功能芙扎,使得可以在同步調(diào)用之上構(gòu)建異步門面(外觀模式),為異步編程提供了支持(Hystrix引入了Rxjava異步框架)填大。
注意:盡管線程池提供了線程隔離戒洼,我們的客戶端底層代碼也必須要有超時(shí)設(shè)置或響應(yīng)線程中斷,不能無(wú)限制的阻塞以致線程池一直飽和栋盹。
缺點(diǎn):
線程池的主要缺點(diǎn)是增加了計(jì)算開銷施逾。每個(gè)命令的執(zhí)行都在單獨(dú)的線程完成,增加了排隊(duì)例获、調(diào)度和上下文切換的開銷汉额。因此,要使用Hystrix榨汤,就必須接受它帶來(lái)的開銷蠕搜,以換取它所提供的好處。
通常情況下收壕,線程池引入的開銷足夠小妓灌,不會(huì)有重大的成本或性能影響轨蛤。但對(duì)于一些訪問延遲極低的服務(wù),如只依賴內(nèi)存緩存虫埂,線程池引入的開銷就比較明顯了祥山,這時(shí)候使用線程池隔離技術(shù)就不適合了,我們需要考慮更輕量級(jí)的方式掉伏,如信號(hào)量隔離缝呕。
線程隔離-信號(hào)量
上面提到了線程池隔離的缺點(diǎn),當(dāng)依賴延遲極低的服務(wù)時(shí)斧散,線程池隔離技術(shù)引入的開銷超過(guò)了它所帶來(lái)的好處供常。這時(shí)候可以使用信號(hào)量隔離技術(shù)來(lái)代替,通過(guò)設(shè)置信號(hào)量來(lái)限制對(duì)任何給定依賴的并發(fā)調(diào)用量鸡捐。下圖說(shuō)明了線程池隔離和信號(hào)量隔離的主要區(qū)別:
圖片來(lái)源Hystrix官網(wǎng)[https://github.com/Netflix/Hystrix/wiki](https://github.com/Netflix/Hystrix/wiki)
使用線程池時(shí)栈暇,發(fā)送請(qǐng)求的線程和執(zhí)行依賴服務(wù)的線程不是同一個(gè),而使用信號(hào)量時(shí)箍镜,發(fā)送請(qǐng)求的線程和執(zhí)行依賴服務(wù)的線程是同一個(gè)源祈,都是發(fā)起請(qǐng)求的線程。先看一個(gè)使用信號(hào)量隔離線程的示例:
public class QueryByOrderIdCommandSemaphore extends HystrixCommand<Integer> {
private final static Logger logger = LoggerFactory.getLogger(QueryByOrderIdCommandSemaphore.class);
private OrderServiceProvider orderServiceProvider;
public QueryByOrderIdCommandSemaphore(OrderServiceProvider orderServiceProvider) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("orderService"))
.andCommandKey(HystrixCommandKey.Factory.asKey("queryByOrderId"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withCircuitBreakerRequestVolumeThreshold(10)////至少有10個(gè)請(qǐng)求色迂,熔斷器才進(jìn)行錯(cuò)誤率的計(jì)算
.withCircuitBreakerSleepWindowInMilliseconds(5000)//熔斷器中斷請(qǐng)求5秒后會(huì)進(jìn)入半打開狀態(tài),放部分流量過(guò)去重試
.withCircuitBreakerErrorThresholdPercentage(50)//錯(cuò)誤率達(dá)到50開啟熔斷保護(hù)
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
.withExecutionIsolationSemaphoreMaxConcurrentRequests(10)));//最大并發(fā)請(qǐng)求量
this.orderServiceProvider = orderServiceProvider;
}
@Override
protected Integer run() {
return orderServiceProvider.queryByOrderId();
}
@Override
protected Integer getFallback() {
return -1;
}
}
由于Hystrix默認(rèn)使用線程池做線程隔離新博,使用信號(hào)量隔離需要顯示地將屬性execution.isolation.strategy設(shè)置為ExecutionIsolationStrategy.SEMAPHORE,同時(shí)配置信號(hào)量個(gè)數(shù)脚草,默認(rèn)為10≡祝客戶端需向依賴服務(wù)發(fā)起請(qǐng)求時(shí)馏慨,首先要獲取一個(gè)信號(hào)量才能真正發(fā)起調(diào)用,由于信號(hào)量的數(shù)量有限姑隅,當(dāng)并發(fā)請(qǐng)求量超過(guò)信號(hào)量個(gè)數(shù)時(shí)写隶,后續(xù)的請(qǐng)求都會(huì)直接拒絕,進(jìn)入fallback流程讲仰。
信號(hào)量隔離主要是通過(guò)控制并發(fā)請(qǐng)求量慕趴,防止請(qǐng)求線程大面積阻塞,從而達(dá)到限流和防止雪崩的目的鄙陡。
線程隔離總結(jié)
線程池和信號(hào)量都可以做線程隔離冕房,但各有各的優(yōu)缺點(diǎn)和支持的場(chǎng)景,對(duì)比如下:
| | 線程切換 | 支持異步 | 支持超時(shí) | 支持熔斷 | 限流 | 開銷 |
| 信號(hào)量 | 否 | 否 | 否 | 是 | 是 | 小 |
| 線程池 | 是 | 是 | 是 | 是 | 是 | 大 |
線程池和信號(hào)量都支持熔斷和限流趁矾。相比線程池耙册,信號(hào)量不需要線程切換,因此避免了不必要的開銷毫捣。但是信號(hào)量不支持異步详拙,也不支持超時(shí)帝际,也就是說(shuō)當(dāng)所請(qǐng)求的服務(wù)不可用時(shí),信號(hào)量會(huì)控制超過(guò)限制的請(qǐng)求立即返回饶辙,但是已經(jīng)持有信號(hào)量的線程只能等待服務(wù)響應(yīng)或從超時(shí)中返回蹲诀,即可能出現(xiàn)長(zhǎng)時(shí)間等待。線程池模式下弃揽,當(dāng)超過(guò)指定時(shí)間未響應(yīng)的服務(wù)脯爪,Hystrix會(huì)通過(guò)響應(yīng)中斷的方式通知線程立即結(jié)束并返回。
熔斷
熔斷器簡(jiǎn)介
現(xiàn)實(shí)生活中蹋宦,可能大家都有注意到家庭電路中通常會(huì)安裝一個(gè)保險(xiǎn)盒披粟,當(dāng)負(fù)載過(guò)載時(shí),保險(xiǎn)盒中的保險(xiǎn)絲會(huì)自動(dòng)熔斷冷冗,以保護(hù)電路及家里的各種電器守屉,這就是熔斷器的一個(gè)常見例子。Hystrix中的熔斷器(Circuit Breaker)也是起類似作用蒿辙,Hystrix在運(yùn)行過(guò)程中會(huì)向每個(gè)commandKey對(duì)應(yīng)的熔斷器報(bào)告成功拇泛、失敗、超時(shí)和拒絕的狀態(tài)思灌,熔斷器維護(hù)并統(tǒng)計(jì)這些數(shù)據(jù)俺叭,并根據(jù)這些統(tǒng)計(jì)信息來(lái)決策熔斷開關(guān)是否打開。如果打開泰偿,熔斷后續(xù)請(qǐng)求熄守,快速返回。隔一段時(shí)間(默認(rèn)是5s)之后熔斷器嘗試半開耗跛,放入一部分流量請(qǐng)求進(jìn)來(lái)裕照,相當(dāng)于對(duì)依賴服務(wù)進(jìn)行一次健康檢查,如果請(qǐng)求成功调塌,熔斷器關(guān)閉晋南。
熔斷器配置
Circuit Breaker主要包括如下6個(gè)參數(shù):
1、circuitBreaker.enabled
是否啟用熔斷器羔砾,默認(rèn)是TRUE负间。
2 、circuitBreaker.forceOpen
熔斷器強(qiáng)制打開姜凄,始終保持打開狀態(tài)政溃,不關(guān)注熔斷開關(guān)的實(shí)際狀態(tài)。默認(rèn)值FLASE檀葛。
3玩祟、circuitBreaker.forceClosed
熔斷器強(qiáng)制關(guān)閉,始終保持關(guān)閉狀態(tài)屿聋,不關(guān)注熔斷開關(guān)的實(shí)際狀態(tài)空扎。默認(rèn)值FLASE藏鹊。
4、circuitBreaker.errorThresholdPercentage
錯(cuò)誤率转锈,默認(rèn)值50%盘寡,例如一段時(shí)間(10s)內(nèi)有100個(gè)請(qǐng)求,其中有54個(gè)超時(shí)或者異常撮慨,那么這段時(shí)間內(nèi)的錯(cuò)誤率是54%竿痰,大于了默認(rèn)值50%,這種情況下會(huì)觸發(fā)熔斷器打開砌溺。
5影涉、circuitBreaker.requestVolumeThreshold
默認(rèn)值20。含義是一段時(shí)間內(nèi)至少有20個(gè)請(qǐng)求才進(jìn)行errorThresholdPercentage計(jì)算规伐。比如一段時(shí)間了有19個(gè)請(qǐng)求蟹倾,且這些請(qǐng)求全部失敗了,錯(cuò)誤率是100%猖闪,但熔斷器不會(huì)打開鲜棠,總請(qǐng)求數(shù)不滿足20。
6培慌、circuitBreaker.sleepWindowInMilliseconds
半開狀態(tài)試探睡眠時(shí)間豁陆,默認(rèn)值5000ms。如:當(dāng)熔斷器開啟5000ms之后吵护,會(huì)嘗試放過(guò)去一部分流量進(jìn)行試探盒音,確定依賴服務(wù)是否恢復(fù)。
熔斷器工作原理
下圖展示了HystrixCircuitBreaker的工作原理:
圖片來(lái)源Hystrix官網(wǎng)[https://github.com/Netflix/Hystrix/wiki](https://github.com/Netflix/Hystrix/wiki)
熔斷器工作的詳細(xì)過(guò)程如下:
第一步馅而,調(diào)用allowRequest()判斷是否允許將請(qǐng)求提交到線程池
- 如果熔斷器強(qiáng)制打開里逆,circuitBreaker.forceOpen為true,不允許放行用爪,返回。
- 如果熔斷器強(qiáng)制關(guān)閉胁镐,circuitBreaker.forceClosed為true偎血,運(yùn)行放行。此外不必關(guān)注熔斷器實(shí)際狀態(tài)盯漂,也就是說(shuō)熔斷器仍然會(huì)維護(hù)統(tǒng)計(jì)數(shù)據(jù)和開關(guān)狀態(tài)颇玷,只是不生效而已。
第二步就缆,調(diào)用isOpen()判斷熔斷器開關(guān)是否打開
- 如果熔斷器開關(guān)打開帖渠,進(jìn)入第三步,否則繼續(xù)竭宰;
- 如果一個(gè)周期內(nèi)總的請(qǐng)求數(shù)小于circuitBreaker.requestVolumeThreshold的值空郊,允許請(qǐng)求放行份招,否則繼續(xù);
- 如果一個(gè)周期內(nèi)錯(cuò)誤率小于circuitBreaker.errorThresholdPercentage的值狞甚,允許請(qǐng)求放行锁摔。否則,打開熔斷器開關(guān)哼审,進(jìn)入第三步谐腰。
第三步,調(diào)用allowSingleTest()判斷是否允許單個(gè)請(qǐng)求通行涩盾,檢查依賴服務(wù)是否恢復(fù)
- 如果熔斷器打開十气,且距離熔斷器打開的時(shí)間或上一次試探請(qǐng)求放行的時(shí)間超過(guò)circuitBreaker.sleepWindowInMilliseconds的值時(shí),熔斷器器進(jìn)入半開狀態(tài)春霍,允許放行一個(gè)試探請(qǐng)求砸西;否則,不允許放行终畅。
此外籍胯,為了提供決策依據(jù),每個(gè)熔斷器默認(rèn)維護(hù)了10個(gè)bucket离福,每秒一個(gè)bucket杖狼,當(dāng)新的bucket被創(chuàng)建時(shí),最舊的bucket會(huì)被拋棄妖爷。其中每個(gè)blucket維護(hù)了請(qǐng)求成功蝶涩、失敗、超時(shí)絮识、拒絕的計(jì)數(shù)器绿聘,Hystrix負(fù)責(zé)收集并統(tǒng)計(jì)這些計(jì)數(shù)器。
熔斷器測(cè)試
1次舌、以QueryOrderIdCommand為測(cè)試command
2熄攘、配置orderServiceProvider不重試且500ms超時(shí)
<dubbo:reference id="orderServiceProvider" interface="com.huang.provider.OrderServiceProvider"
timeout="500" retries="0"/>
3、OrderServiceProviderImpl實(shí)現(xiàn)很簡(jiǎn)單彼念,前10個(gè)請(qǐng)求挪圾,服務(wù)端休眠600ms,使得客戶端調(diào)用超時(shí)逐沙。
@Service
public class OrderServiceProviderImpl implements OrderServiceProvider {
private final static Logger logger = LoggerFactory.getLogger(OrderServiceProviderImpl.class);
private AtomicInteger OrderIdCounter = new AtomicInteger(0);
@Override
public Integer queryByOrderId() {
int c = OrderIdCounter.getAndIncrement();
if (logger.isDebugEnabled()) {
logger.debug("OrderIdCounter:{}", c);
}
if (c < 10) {
try {
Thread.sleep(600);
} catch (InterruptedException e) {
}
}
return c;
}
@Override
public void reset() {
OrderIdCounter.getAndSet(0);
}
}
4哲思、單測(cè)代碼
@Test
public void testExecuteCommand() throws InterruptedException {
orderServiceProvider.reset();
int i = 1;
for (; i < 15; i++) {
HystrixCommand<Integer> command = new QueryByOrderIdCommand(orderServiceProvider);
Integer r = command.execute();
String method = r == -1 ? "fallback" : "run";
logger.info("call {} times,result:{},method:{},isCircuitBreakerOpen:{}", i, r, method, command.isCircuitBreakerOpen());
}
//等待6s,使得熔斷器進(jìn)入半打開狀態(tài)
Thread.sleep(6000);
for (; i < 20; i++) {
HystrixCommand<Integer> command = new QueryByOrderIdCommand(orderServiceProvider);
Integer r = command.execute();
String method = r == -1 ? "fallback" : "run";
logger.info("call {} times,result:{},method:{},isCircuitBreakerOpen:{}", i, r, method, command.isCircuitBreakerOpen());
}
}
5吩案、輸出結(jié)果
2018-02-07 11:38:36,056 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 1 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:36,564 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 2 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:37,074 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 3 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:37,580 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 4 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:38,089 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 5 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:38,599 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 6 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:39,109 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 7 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:39,622 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 8 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:40,138 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 9 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:40,647 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 10 times,result:-1,method:fallback,isCircuitBreakerOpen:true
2018-02-07 11:38:40,651 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 11 times,result:-1,method:fallback,isCircuitBreakerOpen:true
2018-02-07 11:38:40,653 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 12 times,result:-1,method:fallback,isCircuitBreakerOpen:true
2018-02-07 11:38:40,656 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 13 times,result:-1,method:fallback,isCircuitBreakerOpen:true
2018-02-07 11:38:40,658 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 14 times,result:-1,method:fallback,isCircuitBreakerOpen:true
2018-02-07 11:38:46,671 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:44 call 15 times,result:10,method:run,isCircuitBreakerOpen:false
2018-02-07 11:38:46,675 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:44 call 16 times,result:11,method:run,isCircuitBreakerOpen:false
2018-02-07 11:38:46,680 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:44 call 17 times,result:12,method:run,isCircuitBreakerOpen:false
2018-02-07 11:38:46,685 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:44 call 18 times,result:13,method:run,isCircuitBreakerOpen:false
2018-02-07 11:38:46,691 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:44 call 19 times,result:14,method:run,isCircuitBreakerOpen:false
前9個(gè)請(qǐng)求調(diào)用超時(shí)棚赔,走fallback邏輯;
10-14個(gè)請(qǐng)求,熔斷器開關(guān)打開靠益,直接快速失敗走fallback邏輯丧肴;
15-19個(gè)請(qǐng)求,熔斷器進(jìn)入半開狀態(tài)捆毫,放行一個(gè)試探請(qǐng)求調(diào)用成功闪湾,熔斷器關(guān)閉,后續(xù)請(qǐng)求恢復(fù)绩卤。
回退降級(jí)
降級(jí)途样,通常指務(wù)高峰期,為了保證核心服務(wù)正常運(yùn)行濒憋,需要停掉一些不太重要的業(yè)務(wù)何暇,或者某些服務(wù)不可用時(shí),執(zhí)行備用邏輯從故障服務(wù)中快速失敗或快速返回凛驮,以保障主體業(yè)務(wù)不受影響裆站。Hystrix提供的降級(jí)主要是為了容錯(cuò),保證當(dāng)前服務(wù)不受依賴服務(wù)故障的影響黔夭,從而提高服務(wù)的健壯性宏胯。要支持回退或降級(jí)處理,可以重寫HystrixCommand的getFallBack方法或HystrixObservableCommand的resumeWithFallback方法本姥。
Hystrix在以下幾種情況下會(huì)走降級(jí)邏輯:
- 執(zhí)行construct()或run()拋出異常
- 熔斷器打開導(dǎo)致命令短路
- 命令的線程池和隊(duì)列或信號(hào)量的容量超額肩袍,命令被拒絕
- 命令執(zhí)行超時(shí)
降級(jí)回退方式
Fail Fast 快速失敗
快速失敗是最普通的命令執(zhí)行方法,命令沒有重寫降級(jí)邏輯婚惫。 如果命令執(zhí)行發(fā)生任何類型的故障氛赐,它將直接拋出異常。
Fail Silent 無(wú)聲失敗
指在降級(jí)方法中通過(guò)返回null先舷,空Map艰管,空List或其他類似的響應(yīng)來(lái)完成。
@Override
protected Integer getFallback() {
return null;
}
@Override
protected List<Integer> getFallback() {
return Collections.emptyList();
}
@Override
protected Observable<Integer> resumeWithFallback() {
return Observable.empty();
}
Fallback: Static
指在降級(jí)方法中返回靜態(tài)默認(rèn)值蒋川。 這不會(huì)導(dǎo)致服務(wù)以“無(wú)聲失敗”的方式被刪除牲芋,而是導(dǎo)致默認(rèn)行為發(fā)生。如:應(yīng)用根據(jù)命令執(zhí)行返回true / false執(zhí)行相應(yīng)邏輯捺球,但命令執(zhí)行失敗街图,則默認(rèn)為true
@Override
protected Boolean getFallback() {
return true;
}
@Override
protected Observable<Boolean> resumeWithFallback() {
return Observable.just( true );
}
Fallback: Stubbed
當(dāng)命令返回一個(gè)包含多個(gè)字段的復(fù)合對(duì)象時(shí),適合以Stubbed 的方式回退懒构。
@Override
protected MissionInfo getFallback() {
return new MissionInfo("missionName","error");
}
Fallback: Cache via Network
有時(shí),如果調(diào)用依賴服務(wù)失敗耘擂,可以從緩存服務(wù)(如redis)中查詢舊數(shù)據(jù)版本胆剧。由于又會(huì)發(fā)起遠(yuǎn)程調(diào)用,所以建議重新封裝一個(gè)Command,使用不同的ThreadPoolKey秩霍,與主線程池進(jìn)行隔離篙悯。
@Override
protected Integer getFallback() {
return new RedisServiceCommand(redisService).execute();
}
Primary + Secondary with Fallback
有時(shí)系統(tǒng)具有兩種行為- 主要和次要,或主要和故障轉(zhuǎn)移铃绒。主要和次要邏輯涉及到不同的網(wǎng)絡(luò)調(diào)用和業(yè)務(wù)邏輯鸽照,所以需要將主次邏輯封裝在不同的Command中,使用線程池進(jìn)行隔離颠悬。為了實(shí)現(xiàn)主從邏輯切換矮燎,可以將主次command封裝在外觀HystrixCommand的run方法中,并結(jié)合配置中心設(shè)置的開關(guān)切換主從邏輯赔癌。由于主次邏輯都是經(jīng)過(guò)線程池隔離的HystrixCommand诞外,因此外觀HystrixCommand可以使用信號(hào)量隔離,而沒有必要使用線程池隔離引入不必要的開銷灾票。原理圖如下:
圖片來(lái)源Hystrix官網(wǎng)[https://github.com/Netflix/Hystrix/wiki](https://github.com/Netflix/Hystrix/wiki)
主次模型的使用場(chǎng)景還是很多的峡谊。如當(dāng)系統(tǒng)升級(jí)新功能時(shí),如果新版本的功能出現(xiàn)問題刊苍,通過(guò)開關(guān)控制降級(jí)調(diào)用舊版本的功能既们。示例代碼如下:
public class CommandFacadeWithPrimarySecondary extends HystrixCommand<String> {
private final static DynamicBooleanProperty usePrimary = DynamicPropertyFactory.getInstance().getBooleanProperty("primarySecondary.usePrimary", true);
private final int id;
public CommandFacadeWithPrimarySecondary(int id) {
super(Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("SystemX"))
.andCommandKey(HystrixCommandKey.Factory.asKey("PrimarySecondaryCommand"))
.andCommandPropertiesDefaults(
// 由于主次command已經(jīng)使用線程池隔離,F(xiàn)acade Command使用信號(hào)量隔離即可
HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE)));
this.id = id;
}
@Override
protected String run() {
if (usePrimary.get()) {
return new PrimaryCommand(id).execute();
} else {
return new SecondaryCommand(id).execute();
}
}
@Override
protected String getFallback() {
return "static-fallback-" + id;
}
@Override
protected String getCacheKey() {
return String.valueOf(id);
}
private static class PrimaryCommand extends HystrixCommand<String> {
private final int id;
private PrimaryCommand(int id) {
super(Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("SystemX"))
.andCommandKey(HystrixCommandKey.Factory.asKey("PrimaryCommand"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("PrimaryCommand"))
.andCommandPropertiesDefaults( HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(600)));
this.id = id;
}
@Override
protected String run() {
return "responseFromPrimary-" + id;
}
}
private static class SecondaryCommand extends HystrixCommand<String> {
private final int id;
private SecondaryCommand(int id) {
super(Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("SystemX"))
.andCommandKey(HystrixCommandKey.Factory.asKey("SecondaryCommand"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("SecondaryCommand"))
.andCommandPropertiesDefaults( HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(100)));
this.id = id;
}
@Override
protected String run() {
return "responseFromSecondary-" + id;
}
}
public static class UnitTest {
@Test
public void testPrimary() {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
ConfigurationManager.getConfigInstance().setProperty("primarySecondary.usePrimary", true);
assertEquals("responseFromPrimary-20", new CommandFacadeWithPrimarySecondary(20).execute());
} finally {
context.shutdown();
ConfigurationManager.getConfigInstance().clear();
}
}
@Test
public void testSecondary() {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
ConfigurationManager.getConfigInstance().setProperty("primarySecondary.usePrimary", false);
assertEquals("responseFromSecondary-20", new CommandFacadeWithPrimarySecondary(20).execute());
} finally {
context.shutdown();
ConfigurationManager.getConfigInstance().clear();
}
}
}
}
通常情況下正什,建議重寫getFallBack或resumeWithFallback提供自己的備用邏輯啥纸,但不建議在回退邏輯中執(zhí)行任何可能失敗的操作。
總結(jié)
本文介紹了Hystrix及其工作原理埠忘,還介紹了Hystrix線程池隔離脾拆、信號(hào)量隔離和熔斷器的工作原理,以及如何使用Hystrix的資源隔離莹妒,熔斷和降級(jí)等技術(shù)實(shí)現(xiàn)服務(wù)容錯(cuò)名船,從而提高系統(tǒng)的整體健壯性。