前言
微服務架構以其輕量級痊剖、易擴展韩玩、穩(wěn)定性高等特點,近幾年受到了熱烈的追捧陆馁,從互聯(lián)網(wǎng)項目到企業(yè)級系統(tǒng)找颓,都紛紛開始采用微服務架構。一般情況下叮贩,我們會按照業(yè)務對服務進行拆分击狮,再通過接口實現(xiàn)服務之間的相互調(diào)用佛析,從而實現(xiàn)服務的獨立部署和維護。
但隨著服務數(shù)量的增多彪蓬,也會遇到一些單體服務中不會出現(xiàn)的問題寸莫,服務雪崩就是其中的典型。今天我們就來聊一下服務雪崩產(chǎn)生的原因及其防治方法档冬。
服務雪崩
定義
服務雪崩產(chǎn)生于服務堆積在同一個線程池中膘茎,因為所有的請求都是同一個線程池進行處理,這時候如果在高并發(fā)情況下酷誓,所有的請求全部訪問同一個接口披坏,這時候可能會導致其他服務沒有線程進行接受請求,這就是服務雪崩盐数。
產(chǎn)生過程
下面我們以五個服務為例(調(diào)用關系如下圖所示)棒拂,演示一下服務雪崩發(fā)生的過程:
第一階段:服務E由于故障或負載過高,無法及時向上游服務C和D返回響應玫氢,服務C和D的請求線程處于等待狀態(tài)帚屉;
第二階段:服務C和D收不到E的響應,開始加大重試漾峡,同時又收到上游服務請求涮阔,導致更多的線程處于等待狀態(tài),最終線程池被耗盡灰殴,此時服務C和D也處于無法服務狀態(tài)敬特。
第三階段:隨著時間的推移,這種服務不可用的影響被逐漸放大牺陶,由于相同的原因伟阔,服務A和B也因為線程耗盡而無法對上游提供服務,于是整個調(diào)用鏈路崩潰掰伸,服務出現(xiàn)大面積癱瘓皱炉,服務雪崩就形成了。
產(chǎn)生原因
服務雪崩的每個階段都可能由不同的原因造成, 比如造成服務不可用的原因有:
- 硬件故障
硬件故障可能為硬件損壞造成的服務器主機宕機狮鸭,網(wǎng)絡硬件故障造成的服務提供者的不可訪問合搅。 - 緩存擊穿
緩存擊穿一般發(fā)生在緩存應用重啟, 所有緩存被清空時歧蕉,以及短時間內(nèi)大量緩存失效時大量的緩存不命中灾部,使請求直擊后端,造成服務提供者超負荷運行惯退,引起服務不可用赌髓。 - 用戶大量請求
在秒殺和大促開始前,如果準備不充分,用戶發(fā)起大量請求也會造成服務提供者的不可用锁蠕。在服務提供者不可用后夷野,用戶由于忍受不了界面上長時間的等待,而不斷刷新頁面甚至提交表單荣倾,導致后端服務壓力雪上加霜悯搔。服務調(diào)用端的會存在大量服務異常后的重試邏輯。 - 程序BUG
如程序邏輯導致內(nèi)存泄漏舌仍,JVM長時間Full GC等妒貌。 - 同步等待
服務間采用同步調(diào)用模式,同步等待造成的資源耗盡抡笼。
解決方案
雪崩效應之所以這么被重視是因為它極容易在被人們忽視的情況下發(fā)生苏揣,對微服務而言黄鳍,服務實例成百上千推姻,我們很難一個個服務地檢查以保證每個服務的質(zhì)量,并且很多情況下只有在達到一定壓力問題才會暴露框沟,常規(guī)的代碼Reivew或是針對單個服務的壓力測試未必可以發(fā)現(xiàn)問題藏古,再則這些依賴服務未必都是我們自己的服務,如果說我們自己服務尚有一定的排查優(yōu)化方法的話那么對三方服務依賴而言那幾乎只能是憑經(jīng)驗了忍燥,只要我的依賴服務中存在一處不起眼的Bug拧晕,或是過少的連接池配置,抑或是網(wǎng)絡波動都有可能引發(fā)雪崩梅垄。
怎么有效地避免呢厂捞?
服務隔離
從服務雪崩的過程我們可以知道,服務雪崩的根本原因是長期等待下游服務接口響應队丝,而導致線程池被耗盡靡馁,從而無法處理上游請求。如果為每個下游服務接口創(chuàng)建一個獨立的線程池机久,每個線程池互不影響臭墨,當某個下游服務接口無法響應時,只有其對應的線程池被耗盡膘盖,其他服務接口并不受到影響胧弛。
流量控制
當發(fā)現(xiàn)服務失敗數(shù)量達到某個閾值,拒絕訪問侠畔,限制更多流量的到來结缚,防止過多失敗的請求將資源耗盡。
緩存
對下游服務正常響應的數(shù)據(jù)進行緩存软棺,之后一段時間內(nèi)直接向上游返回緩存中的數(shù)據(jù)掺冠。這樣可以有效降低對下游服務質(zhì)量的敏感度,在一定程度上提升服務的穩(wěn)定性。
服務熔斷
當下游的服務因為某種原因突然變得不可用或響應過慢德崭,上游服務為了保證自己整體服務的可用性斥黑,當目標服務超過設定最長等待時間未響應時,直接終止等待眉厨,快速釋放資源锌奴。如果目標服務情況好轉則恢復調(diào)用。
服務降級
在高并發(fā)情況下憾股,為防止用戶一直等待鹿蜀,可以使用服務降級的方式:對于簡單的展示功能,如果有失敗的請求服球,返回默認值茴恰;對于整個站點或客戶端,如果服務器負載過高斩熊,將其他非核心業(yè)務停止往枣,以讓出更多資源給其他服務使用。
Hystrix
前面介紹了服務隔離粉渠、流量控制分冈、緩存、熔斷降級等解決方案霸株,很多小伙伴可能感覺到頭大:理想很豐滿雕沉,現(xiàn)實很骨感,手頭的需求都做不完去件,更別說再花時間去實現(xiàn)這些高大上的技術方案了坡椒。
很幸運的是,得益于偉大的開源精神尤溜,Netflix為我們帶來了一款開源的基于Java語言開發(fā)的服務雪崩預防神器:Hystrix倔叼。
介紹
Hystrix的中文含義是豪豬, 因其背上長滿了刺靴跛,而擁有自我保護能力缀雳。 Hystrix 是一個通過增加延遲容錯和容錯邏輯來控制分布式服務之間交互的一個庫。Hystrix通過線程隔離梢睛,防止錯誤級聯(lián)傳遞肥印,導致服務雪崩,從而提高服務穩(wěn)定性绝葡。
Hystrix的主要目標
- 通過隔離第三方客戶端庫訪問依賴關系深碱,防止和控制延遲和故障;
- 防止復雜分布式系統(tǒng)的級聯(lián)失敳爻敷硅;
- 快速響應失敗并迅速恢復功咒;
- 提供回滾以及友好降級;
- 實現(xiàn)近實時監(jiān)控绞蹦,告警和操作控制
Hystrix設計原則
- 防止單個依賴耗盡了服務容器的用戶線程
- 降低負載以及快速失敗力奋,而不是排隊
- 當可以阻止服務的失敗時提供回退策略
- 使用隔離技術減少任意依賴的影響
- 通過近實時指標、監(jiān)控和告警優(yōu)化發(fā)現(xiàn)時間
- 在Hystrix的大多數(shù)方面幽七,通過配置更改的低延遲和對動態(tài)屬性更改的支持景殷,使得可以在低延遲的情況下進行實時修改操作,從而優(yōu)化恢復時間
- 防止整個依賴關系客戶端執(zhí)行中的故障澡屡,而不僅僅是網(wǎng)絡流量
Hystrix如何做到上面的目標
- 所有外部的調(diào)用都封裝到HystrixCommand或HystrixObservableCommand對象猿挚,這些對象通常在單獨的線程下執(zhí)行。
- 超時調(diào)用的時間驶鹉,超過定義的閾值绩蜻。有一個默認值,但是對于大多數(shù)的依賴室埋,你可以自定義該屬性使得略高于每個依賴測量的99.5%的性能办绝。
- 為每一個依賴項維護一個線程池(或者信號),如果依賴項的線程池滿了词顾,新的依賴請求不會繼續(xù)排隊等待八秃,而是馬上被拒絕訪問碱妆。
- 計算成功肉盹、失敗、超時和線程拒絕的數(shù)量疹尾。
- 如果依賴服務的失敗百分比超過閾值上忍,則手動或自動啟動斷路器,在一段時間內(nèi)停止對指定服務的所有請求纳本。
- 為請求失敗窍蓝、被拒絕、超時或短路情況提供回退邏輯繁成。
- 近乎實時地監(jiān)控指標和配置更改吓笙。
Hystrix處理流程
Hystrix整個工作流如下:
- 構造一個 HystrixCommand或HystrixObservableCommand對象,用于封裝請求巾腕,并在構造方法配置請求被執(zhí)行需要的參數(shù)面睛;
- 執(zhí)行命令,Hystrix提供了4種執(zhí)行命令的方法尊搬,后面詳述叁鉴;
- 判斷是否使用緩存響應請求,若啟用了緩存佛寿,且緩存可用幌墓,直接使用緩存響應請求。Hystrix支持請求緩存,但需要用戶自定義啟動常侣;
- 判斷熔斷器是否打開蜡饵,如果打開,跳到第8步胳施;
- 判斷線程池/隊列/信號量是否已滿验残,已滿則跳到第8步;
- 執(zhí)行HystrixObservableCommand.construct()或HystrixCommand.run()巾乳,如果執(zhí)行失敗或者超時您没,跳到第8步;否則胆绊,跳到第9步氨鹏;
- 統(tǒng)計熔斷器監(jiān)控指標;
- 走Fallback備用邏輯
- 返回請求響應
從流程圖上可知道压状,第5步線程池/隊列/信號量已滿時仆抵,還會執(zhí)行第7步邏輯,更新熔斷器統(tǒng)計信息种冬,而第6步無論成功與否镣丑,都會更新熔斷器統(tǒng)計信息。
Hystrix簡單使用
第一步娱两,繼承HystrixCommand實現(xiàn)自己的command莺匠,在command的構造方法中需要配置請求被執(zhí)行需要的參數(shù),并組合實際發(fā)送請求的對象十兢,代碼如下:
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個請求趣竣,熔斷器才進行錯誤率的計算
.withCircuitBreakerSleepWindowInMilliseconds(5000)//熔斷器中斷請求5秒后會進入半打開狀態(tài),放部分流量過去重試
.withCircuitBreakerErrorThresholdPercentage(50)//錯誤率達到50開啟熔斷保護
.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ā)起實際請求旱物。
@Test
public void testQueryByOrderIdCommand() {
Integer r = new QueryOrderIdCommand(orderServiceProvider).execute();
logger.info("result:{}", r);
}
執(zhí)行命令的幾種方法
Hystrix提供了4種執(zhí)行命令的方法遥缕,execute()和queue() 適用于HystrixCommand對象,而observe()和toObservable()適用于HystrixObservableCommand對象宵呛。
execute()
以同步堵塞方式執(zhí)行run()单匣,只支持接收一個值對象。hystrix會從線程池中取一個線程來執(zhí)行run()宝穗,并等待返回值户秤。
queue()
以異步非阻塞方式執(zhí)行run(),只支持接收一個值對象讽营。調(diào)用queue()就直接返回一個Future對象虎忌。可通過 Future.get()拿到run()的返回結果橱鹏,但Future.get()是阻塞執(zhí)行的膜蠢。若執(zhí)行成功堪藐,F(xiàn)uture.get()返回單個返回值。當執(zhí)行失敗時挑围,如果沒有重寫fallback礁竞,F(xiàn)uture.get()拋出異常。
observe()
事件注冊前執(zhí)行run()/construct()杉辙,支持接收多個值對象模捂,取決于發(fā)射源。調(diào)用observe()會返回一個hot Observable蜘矢,也就是說狂男,調(diào)用observe()自動觸發(fā)執(zhí)行run()/construct(),無論是否存在訂閱者品腹。
如果繼承的是HystrixCommand岖食,hystrix會從線程池中取一個線程以非阻塞方式執(zhí)行run();如果繼承的是HystrixObservableCommand舞吭,將以調(diào)用線程阻塞執(zhí)行construct()泡垃。
observe()使用方法:
- 調(diào)用observe()會返回一個Observable對象
- 調(diào)用這個Observable對象的subscribe()方法完成事件注冊,從而獲取結果
toObservable()
事件注冊后執(zhí)行run()/construct()羡鸥,支持接收多個值對象蔑穴,取決于發(fā)射源。調(diào)用toObservable()會返回一個cold Observable惧浴,也就是說存和,調(diào)用toObservable()不會立即觸發(fā)執(zhí)行run()/construct(),必須有訂閱者訂閱Observable時才會執(zhí)行赶舆。
如果繼承的是HystrixCommand哑姚,hystrix會從線程池中取一個線程以非阻塞方式執(zhí)行run()祭饭,調(diào)用線程不必等待run()芜茵;如果繼承的是HystrixObservableCommand,將以調(diào)用線程堵塞執(zhí)行construct()倡蝙,調(diào)用線程需等待construct()執(zhí)行完才能繼續(xù)往下走九串。
toObservable()使用方法:
- 調(diào)用observe()會返回一個Observable對象
- 調(diào)用這個Observable對象的subscribe()方法完成事件注冊,從而獲取結果
需注意的是寺鸥,HystrixCommand也支持toObservable()和observe()猪钮,但是即使將HystrixCommand轉換成Observable,它也只能發(fā)射一個值對象胆建。只有HystrixObservableCommand才支持發(fā)射多個值對象烤低。
幾種方法的關系
- execute()實際是調(diào)用了queue().get()
- queue()實際調(diào)用了toObservable().toBlocking().toFuture()
- observe()實際調(diào)用toObservable()獲得一個cold Observable,再創(chuàng)建一個ReplaySubject對象訂閱Observable笆载,將源Observable轉化為hot Observable扑馁。因此調(diào)用observe()會自動觸發(fā)執(zhí)行run()/construct()涯呻。
Hystrix總是以Observable的形式作為響應返回,不同執(zhí)行命令的方法只是進行了相應的轉換腻要。