hystrix是什么?
在微服務場景中,通常會有很多層的服務調用。如果一個底層服務出現問題,故障會被向上傳播給用戶。我們需要一種機制,當底層服務不可用時,可以阻斷故障的傳播。這就是斷路器的作用。他是系統服務穩定性的最后一重保障。
Hystrix是一個用于處理分布式系統的延遲和容錯的開源庫,在分布式系統里,許多依賴不可避免的會調用失敗,比如超時、異常等,Hystrix能夠保證在一個依賴出問題的情況下,不會導致整體服務失敗,避免級聯故障,以提高分布式系統的彈性。
“斷路器”本身是一種開關裝置,當某個服務單元發生故障之后,通過斷路器的故障監控(類似熔斷保險絲),向調用方返回一個符合預期的、可處理的備選響應(FallBack),而不是長時間的等待或者拋出調用方無法處理的異常,這樣就保證了服務調用方的線程不會被長時間、不必要地占用,從而避免了故障在分布式系統中的蔓延,乃至雪崩。
在springcloud中斷路器組件就是Hystrix。Hystrix也是Netflix套件的一部分。他的功能是,當對某個服務的調用在一定的時間內(默認10s,由metrics.rollingStats.timeInMilliseconds配置),有超過一定次數(默認20次,由circuitBreaker.requestVolumeThreshold參數配置)并且失敗率超過一定值(默認50%,由circuitBreaker.errorThresholdPercentage配置),該服務的斷路器會打開。返回一個由開發者設定的fallback,fallback可以是另一個由Hystrix保護的服務調用,也可以是固定的值。fallback也可以設計成鏈式調用,先執行某些邏輯,再返回fallback。
Hystrix設計目標:
對來自依賴的延遲和故障進行防護和控制——這些依賴通常都是通過網絡訪問的
阻止故障的連鎖反應
快速失敗并迅速恢復
回退并優雅降級
提供近實時的監控與告警
Hystrix遵循的設計原則:
防止任何單獨的依賴耗盡資源(線程)
過載立即切斷并快速失敗,防止排隊
盡可能提供回退以保護用戶免受故障
使用隔離技術(例如隔板,泳道和斷路器模式)來限制任何一個依賴的影響
通過近實時的指標,監控和告警,確保故障被及時發現
通過動態修改配置屬性,確保故障及時恢復
防止整個依賴客戶端執行失敗,而不僅僅是網絡通信
Hystrix如何實現這些設計目標?
使用命令模式將所有對外部服務(或依賴關系)的調用包裝在HystrixCommand或HystrixObservableCommand對象中,并將該對象放在單獨的線程中執行;
每個依賴都維護著一個線程池(或信號量),線程池被耗盡則拒絕請求(而不是讓請求排隊)。
記錄請求成功,失敗,超時和線程拒絕。
服務錯誤百分比超過了閾值,熔斷器開關自動打開,一段時間內停止對該服務的所有請求。
請求失敗,被拒絕,超時或熔斷時執行降級邏輯。
近實時地監控指標和配置的修改。
如何使用
Netflix斷路器是安裝在服務消費者上。我們需要做的是在服務消費者上開啟斷路器并配置。
1),引入依賴
2),打開開關
只需要在啟動類上加上@EnableHystrix注解即可
3),在服務上加上fallback
Hystrix處理流程
Hystrix流程圖如下:
Hystrix整個工作流如下:
1.構造一個?HystrixCommand或HystrixObservableCommand對象,用于封裝請求,并在構造方法配置請求被執行需要的參數;
2.執行命令,Hystrix提供了4種執行命令的方法,后面詳述;
3.判斷是否使用緩存響應請求,若啟用了緩存,且緩存可用,直接使用緩存響應請求。Hystrix支持請求緩存,但需要用戶自定義啟動;
4.判斷熔斷器是否打開,如果打開,跳到第8步;
5.判斷線程池/隊列/信號量是否已滿,已滿則跳到第8步;
6.執行HystrixObservableCommand.construct()或HystrixCommand.run(),如果執行失敗或者超時,跳到第8步;否則,跳到第9步;
7.統計熔斷器監控指標;
8.走Fallback備用邏輯
9.返回請求響應
從流程圖上可知道,第5步線程池/隊列/信號量已滿時,還會執行第7步邏輯,更新熔斷器統計信息,而第6步無論成功與否,都會更新熔斷器統計信息。
執行命令的幾種方法
Hystrix提供了4種執行命令的方法,execute()和queue()?適用于HystrixCommand對象,而observe()和toObservable()適用于HystrixObservableCommand對象。
execute():以同步堵塞方式執行run(),只支持接收一個值對象。hystrix會從線程池中取一個線程來執行run(),并等待返回值。
queue():以異步非阻塞方式執行run(),只支持接收一個值對象。調用queue()就直接返回一個Future對象。可通過?Future.get()拿到run()的返回結果,但Future.get()是阻塞執行的。若執行成功,Future.get()返回單個返回值。當執行失敗時,如果沒有重寫fallback,Future.get()拋出異常。
observe():事件注冊前執行run()/construct(),支持接收多個值對象,取決于發射源。調用observe()會返回一個hot Observable,也就是說,調用observe()自動觸發執行run()/construct(),無論是否存在訂閱者。
如果繼承的是HystrixCommand,hystrix會從線程池中取一個線程以非阻塞方式執行run();如果繼承的是HystrixObservableCommand,將以調用線程阻塞執行construct()。
observe()使用方法:調用observe()會返回一個Observable對象;調用這個Observable對象的subscribe()方法完成事件注冊,從而獲取結果
toObservable():事件注冊后執行run()/construct(),支持接收多個值對象,取決于發射源。調用toObservable()會返回一個cold?Observable,也就是說,調用toObservable()不會立即觸發執行run()/construct(),必須有訂閱者訂閱Observable時才會執行。
如果繼承的是HystrixCommand,hystrix會從線程池中取一個線程以非阻塞方式執行run(),調用線程不必等待run();如果繼承的是HystrixObservableCommand,將以調用線程堵塞執行construct(),調用線程需等待construct()執行完才能繼續往下走。
toObservable()使用方法:調用toObservable()會返回一個Observable對象;調用這個Observable對象的subscribe()方法完成事件注冊,從而獲取結果
需注意的是,HystrixCommand也支持toObservable()和observe(),但是即使將HystrixCommand轉換成Observable,它也只能發射一個值對象。只有HystrixObservableCommand才支持發射多個值對象。
幾種方法的關系
execute()實際是調用了queue().get()
queue()實際調用了toObservable().toBlocking().toFuture()
observe()實際調用toObservable()獲得一個cold?Observable,再創建一個ReplaySubject對象訂閱Observable,將源Observable轉化為hot Observable。因此調用observe()會自動觸發執行run()/construct()。
Hystrix總是以Observable的形式作為響應返回,不同執行命令的方法只是進行了相應的轉換。
Hystrix源碼
我們在前面的章節提到過,在maven中引入Hystrix的依賴,然后在啟動類上加上@EnableHystrix注解,這個注解它做了什么呢?在EnableHystrix注解中又有一個@EnableCircuitBreaker的注解,在EnableCircuitBreaker中通過Import注解注入了一個叫EnableCircuitBreakerImportSelector的類,這個類實現了SpringFactoryImportSelector,SpringFactoryImportSelector間接繼承了ImportSelector,ImportSelector是spring提供的一種動態注入bean的方式。SpringFactoryImportSelector是Spring Cloud提供的對ImportSelector的一種實現,通過這個類實現了Hystrix的動態插拔。
何謂Hystrix動態插拔?
我們先看一下spring cloud的hystrix報的spring.factory配置類:
如上圖所示,HystrixCircuitBreakerConfiguration的配置類并沒有包含在spring boot自動掃描的EnableAutoConfiguration配置中,而是自定義了一個配置EnableCircuitBreaker,如果交給spring boot管理,則會在項目啟動的時候就會被spring boot自動加載。EnableCircuitBreaker這個配置是在SpringFactoryImportSelector的importSelectors中通過調用SpringFactoriesLoader.loadFactoryNames將HystrixCircuitBreakerConfiguration加載到spring容器中。
總結:在啟動類中加上@EnableHystrix注解,其實就是注入了HystrixCircuitBreakerConfiguration這個類到spring容器中。
在HystrixCircuitBreakerConfiguration注入了一個切面bean:HystrixCommandAspect.
在HystrixCommandAspect中,定義了兩個切點HystrixCommand和HystrixCollapser以及一個環繞通知,HystrixCollapser是用來進行請求合并的。如果同時加了HystrixCommand和HystrixCollapser注解,直接拋出異常;在META_HOLDER_FACTORY_MAP中包含了兩個值,就是HystrixCommand和HystrixCollapser,通過META_HOLDER_FACTORY_MAP.get來獲取加的到底是哪個注解。
熔斷
現實生活中,可能大家都有注意到家庭電路中通常會安裝一個保險盒,當負載過載時,保險盒中的保險絲會自動熔斷,以保護電路及家里的各種電器,這就是熔斷器的一個常見例子。Hystrix中的熔斷器(Circuit Breaker)也是起類似作用,Hystrix在運行過程中會向每個commandKey對應的熔斷器報告成功、失敗、超時和拒絕的狀態,熔斷器維護并統計這些數據,并根據這些統計信息來決策熔斷開關是否打開。如果打開,熔斷后續請求,快速返回。隔一段時間(默認是5s)之后熔斷器嘗試半開,放入一部分流量請求進來,相當于對依賴服務進行一次健康檢查,如果請求成功,熔斷器關閉。
熔斷器配置
Circuit Breaker主要包括如下6個參數:
1、circuitBreaker.enabled
是否啟用熔斷器,默認是TRUE。可以直接關閉,關閉之后返回一個NoOpCircuitBreaker。
2 、circuitBreaker.forceOpen
熔斷器強制打開,始終保持打開狀態,不關注熔斷開關的實際狀態。默認值FLASE。
3、circuitBreaker.forceClosed
熔斷器強制關閉,始終保持關閉狀態,不關注熔斷開關的實際狀態。默認值FLASE。
4、circuitBreaker.errorThresholdPercentage
錯誤率,默認值50%,例如一段時間(10s)內有100個請求,其中有54個超時或者異常,那么這段時間內的錯誤率是54%,大于了默認值50%,這種情況下會觸發熔斷器打開。
5、circuitBreaker.requestVolumeThreshold
默認值20。含義是一段時間內至少有20個請求才進行errorThresholdPercentage計算。比如一段時間了有19個請求,且這些請求全部失敗了,錯誤率是100%,但熔斷器不會打開,總請求數不滿足20。
6、circuitBreaker.sleepWindowInMilliseconds
半開狀態試探睡眠時間,默認值5000ms。如:當熔斷器開啟5000ms之后,會嘗試放過去一部分流量進行試探,確定依賴服務是否恢復。
熔斷器工作原理
下圖展示了HystrixCircuitBreaker的工作原理:
熔斷器工作的詳細過程如下:
第一步,調用allowRequest()判斷是否允許將請求提交到線程池
如果熔斷器強制打開,circuitBreaker.forceOpen為true,不允許放行,返回。
如果熔斷器強制關閉,circuitBreaker.forceClosed為true,允許放行。此外不必關注熔斷器實際狀態,也就是說熔斷器仍然會維護統計數據和開關狀態,只是不生效而已。
第二步,調用isOpen()判斷熔斷器開關是否打開
如果熔斷器開關打開,進入第三步,否則繼續;
如果一個周期內總的請求數小于circuitBreaker.requestVolumeThreshold的值,允許請求放行,否則繼續;
如果一個周期內錯誤率小于circuitBreaker.errorThresholdPercentage的值,允許請求放行。否則,打開熔斷器開關,進入第三步。
第三步,調用allowSingleTest()判斷是否允許單個請求通行,檢查依賴服務是否恢復
如果熔斷器打開,且距離熔斷器打開的時間或上一次試探請求放行的時間超過circuitBreaker.sleepWindowInMilliseconds的值時,熔斷器器進入半開狀態,允許放行一個試探請求;否則,不允許放行。
此外,為了提供決策依據,每個熔斷器默認維護了10個bucket,每秒一個bucket,當新的bucket被創建時,最舊的bucket會被拋棄。其中每個blucket維護了請求成功、失敗、超時、拒絕的計數器,Hystrix負責收集并統計這些計數器。
在獲取熔斷器時,判斷熔斷器是否為空,如果不為空,則說明有緩存;如果為空,則構建一個新的熔斷器。
總結:在程序入口的配置類里面,定義了AOP的切面,通過環繞通知around代理Hystrix的注解,所有加了注解的方法都會進入代理方法中,在代理方法中,會讀取系統的配置、注解的配置,構建一個Command對象,在Command對象構建的中途,會構建熔斷器組件和線程池等等一系列的配置的初始化,配置來源都是yml+注解上面的配置;可以根據Command的key把多個注解綁定起來,這樣就不用重復構建多個Command對象。
資源隔離
資源隔離主要指對線程的隔離。Hystrix提供了兩種線程隔離方式:線程池和信號量。
線程隔離-線程池
Hystrix通過命令模式對發送請求的對象和執行請求的對象進行解耦,將不同類型的業務請求封裝為對應的命令請求。如訂單服務查詢商品,查詢商品請求->商品Command;商品服務查詢庫存,查詢庫存請求->庫存Command。并且為每個類型的Command配置一個線程池,當第一次創建Command時,根據配置創建一個線程池,并放入ConcurrentHashMap,如商品Command:
后續查詢商品的請求創建Command時,將會重用已創建的線程池。線程池隔離之后的服務依賴關系:
通過將發送請求線程與執行請求的線程分離,可有效防止發生級聯故障。當線程池或請求隊列飽和時,Hystrix將拒絕服務,使得請求線程可以快速失敗,從而避免依賴問題擴散。
線程池隔離優缺點
優點:
1.保護應用程序以免受來自依賴故障的影響,指定依賴線程池飽和不會影響應用程序的其余部分。
2.當引入新客戶端lib時,即使發生問題,也是在本lib中,并不會影響到其他內容。
3。當依賴從故障恢復正常時,應用程序會立即恢復正常的性能。
4.當應用程序一些配置參數錯誤時,線程池的運行狀況會很快檢測到這一點(通過增加錯誤,延遲,超時,拒絕等),同時可以通過動態屬性進行實時糾正錯誤的參數配置。
5.如果服務的性能有變化,需要實時調整,比如增加或者減少超時時間,更改重試次數,可以通過線程池指標動態屬性修改,而且不會影響到其他調用請求。
6.除了隔離優勢外,hystrix擁有專門的線程池可提供內置的并發功能,使得可以在同步調用之上構建異步門面(外觀模式),為異步編程提供了支持(Hystrix引入了Rxjava異步框架)。
注意:盡管線程池提供了線程隔離,我們的客戶端底層代碼也必須要有超時設置或響應線程中斷,不能無限制的阻塞以致線程池一直飽和。
缺點:
線程池的主要缺點是增加了計算開銷。每個命令的執行都在單獨的線程完成,增加了排隊、調度和上下文切換的開銷。因此,要使用Hystrix,就必須接受它帶來的開銷,以換取它所提供的好處。
通常情況下,線程池引入的開銷足夠小,不會有重大的成本或性能影響。但對于一些訪問延遲極低的服務,如只依賴內存緩存,線程池引入的開銷就比較明顯了,這時候使用線程池隔離技術就不適合了,我們需要考慮更輕量級的方式,如信號量隔離。