SpringCloud 的限流、降級和熔斷——Hystrix
一、前言
分佈式系統環境中,服務間類似依賴非常常見,一個業餘調用通常依賴多個基礎服務。如下圖,對於同步調用,當庫存服務不可用時,商品服務請求線程被阻塞,當有大批量請求調用庫存服務時,最終可能導致整個商品服務資源耗盡,無法繼續對外提供服務。並且這種不可用可能沿請求調用鏈向上傳遞,這種現象稱爲雪崩效應。
二、雪崩效應
1、常見場景
(1)硬件故障:如服務器宕機,機房斷電,光纖被挖斷等。
(2)流量激增:如異常流量,重試加大流量等。
(3)緩存穿透:一般發生在應用重啓,所有緩存失效時,以及短時間內大量緩存失效時。大量的緩存不命中,使請求直擊後端服務,造成服務提供者超負荷運行,引起服務不可用。
(4)程序 bug:如程序邏輯導致內存泄漏,JVM 長時間 FullGC 等。
(5)同步等待:服務間採用同步調用模式,同步等待造成的資源耗盡。
2、應對策略
針對造成雪崩效應的不同場景,可以使用不同的應對策略,沒有一種通用所有場景的策略。
(1)硬件故障:多機房容災、異地多活等。
(2)流量激增:服務自動擴容、流量控制(限流、關閉重試)等。
(3)緩存穿透:緩存預加載、緩存異步加載等。
(4)程序 bug:修改程序 bug、及時釋放資源等。
(5)同步等待:資源隔離、MQ 解耦。、不可用服務調用快速失敗等。資源隔離通常指不同服務調用採取不同的線程池;不可用服務調用快速失敗一般通過熔斷模式結合超時機制實現。
綜上所述,如果一個應用不能對來自依賴的故障進行隔離,那該應用本身就處在被拖垮的風險中。因此,爲了構建穩定、可靠的分佈式系統,我們的服務應當具有自我保護能力,當依賴服務不可用時,當前服務啓動自我保護功能,從而避免發生雪崩效應。本文將重點介紹使用 Hystrix 解決同步等待的雪崩問題。
三、初探 Hystrix
Hystrix,中文含義是豪豬,因其背上長滿荊棘,從而擁有了自我保護的能力。本文所說的 Hystrix 是 Netflix 公司開源的一款容錯框架,同樣具有自我保護能力。爲了實現容錯和自我保護,下面我們看看 Hystrix 如何設計和實現的。
Hystrix 設計目標:
-
對來自依賴的延遲和故障進行防護和控制,這些依賴通常都是通過網絡訪問的。
-
阻止失敗並迅速恢復
-
回退並優雅降級
-
提供近實時的監控與告警
Hystrix 遵循的設計原則:
-
防止任何單獨的依賴耗盡資源(線程)
-
過載立即切斷並快速失敗,防止排隊
-
儘可能提供回退以保護用戶免受故障
-
使用隔離技術(例如隔板、泳道和斷路器模式)來限制任何一個依賴的影響
-
通過近實時的指標,監控和告警,確保故障被及時發現
-
通過動態修改配置屬性,確保故障及時恢復
-
防止整個依賴客戶端執行失敗,而不僅僅是網絡通信
Hystrix 如何實現這些設計目標?
-
使用命令模式將所有對外部服務(或依賴關係)的調用包裝在 HystrixCommand 或 HystrixObservableCommand 對象中,並將該對象放在單獨的線程中執行。
-
每個依賴都維護着一個線程池(或信號量),線程池被耗盡則拒絕請求(而不是讓請求排隊)。
-
記錄請求成功,失敗,超時和線程拒絕。
-
服務錯誤百分比超過了閾值,熔斷器開關自動打開,一段時間內停止對該服務的所有請求。
-
請求失敗,被拒絕,超時或熔斷時執行降級邏輯。
-
近實時地監控指標和配置的修改。
四、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 對象,而 observer() 和 toObservable() 適用於 HystrixObservableCommand 對象。
1、execute()
以同步阻塞方法執行 run(),只支持接收一個值對象。 Hystrix 會從線程池中取一個線程來執行 run(),並等待返回值。
2、queue()
以異步非阻塞方法執行 run(),只支持接收一個值對象。調用 queue() 就直接返回一個 Future 對象。可通過 Future.get() 拿到 run() 的返回結果,但 Future.get() 是阻塞執行的。若執行成功, Future.get() 返回單個返回值。當執行失敗時,如果沒有重寫 fallback, Future.get() 拋出異常。
3、observe()
事件註冊前執行 run()/construct(),支持接收多個值對象,取決於發射源。調用 observe() 會返回一個 hot Observable,也就是說,調用 observe() 自動觸發執行 run()/construct(),無論是否存在訂閱者。
如果繼承的是 HystrixCommand,hystrix 會從線程池中取一個線程以非阻塞方式執行 run();如果繼承的是 HystrixObservableCommand,將以調用線程阻塞執行 construct()。
observe() 使用方法:
(1)調用 observe() 會返回一個 Observable 對象
4、toObservable()
事件註冊後執行 run()/construct(),支持接收多個值對象,取決於發射源。調用 toObservable() 會返回一個 cold Observable,也就是說,調用 toObservable() 不會立即觸發執行 run()/construct(),必須有訂閱者訂閱 Observable 時纔會執行。
如果繼承的是 HystrixComman,hystrix 會從線程池中取一個線程以非阻塞方式執行 run(),調用線程不必等待 run();如果繼承的是 HystrixObservableCommand ,將以調用線程堵塞執行 construct(),調用線程需等待 construct() 執行完才能繼續往下走。
toObservable() 使用方法:
(1)調用 observe() 會返回一個 Observable 對象
(2)調用這個 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 容錯
Hystrix 的容錯主要是通過添加容許延遲和容錯方法,幫助控制這些分佈式服務之間的交互。還通過隔離服務之間的訪問點,阻止它們之間的級聯故障以及提供退回選項來實現這一點,從而提高系統的整體彈性。 Hystrix 主要提供了一下幾種容錯方法:
-
資源隔離
-
熔斷
-
降級
(一)資源熔斷
資源隔離主要指對線程的隔離。 Hystrix 提供了兩種線程隔離的方式:線程池和信號量。
1、線程隔離 - 線程池
Hystrix 還通過命令模式對發送請求的對象和執行請求的對象進行解耦,將不同類型的業務請求封裝爲對應的命令請求。如訂單服務查詢商品,查詢商品請求 -> 商品 command;商品服務查詢庫存,查詢庫存請求 -> 庫存 command。並且爲每個類型的 command 配置一個線程池,當第一次創建 command 時,根據配置創建一個線程池,並放入 ConcurrentHashMap,如商品 command:
final static ConcurrentHashMap<String, HystrixThreadPool> threadPools = new ConcurrentHashMap<String, HystrixThreadPool>();
...
if (!threadPools.containsKey(key)) {
threadPools.put(key, new HystrixThreadPoolDefault(threadPoolKey, propertiesBuilder));
}
後續查詢商品的請求創建 command 時,將會重用已創建的線程池。線程池隔離之後的服務依賴關係:
通過發送請求線程與執行請求的線程分離,可有效防止發生級聯故障。當線程池或請求隊列飽和時,Hystrix 將拒絕服務,使得請求線程可以快速失敗,從而避免依賴問題擴散。
線程池隔離優點:
-
保護應用程序以免受來自依賴故障的影響,指定依賴線程池飽和不會影響應用程序的其餘部分。
-
當引入新客戶端 lib 時,即使發生問題,也是在 lib 中,並不會影響其他內容。
-
當依賴從故障恢復正常時,應用程序會立即恢復正常的性能。
-
當應用程序一些配置參數錯誤時,線程池的運行狀況會很快檢測到這一點(通過增加錯誤、延遲、超時、拒絕等),同時可以通過動態屬性進行實時糾正錯誤的參數配置。
-
如果服務的性能有變化,需要實時調整,比如增加或減少超時時間,更改重試次數,可以通過線程池指標狀態屬性修改,而且不會影響到其它調用請求。
-
除了隔離優勢外, Hystrix 擁有專門的線程可提供內置的併發功能,使得可以在同步調用之上構建異步門面(外觀模式),爲異步編程提供了支持( Hystrix 引入了 R 小 Java 異步框架)。
注意:儘管線程池提供了線程隔離,我們的客戶端底層代碼也必須要有超時設置或響應線程中斷,不能無限制的阻塞以致線程池一直飽和。
缺點:
線程池的主要缺點是增加了計算開銷。每個命令的執行都在單獨的線程完成,增加了排隊、調度和上下文切換的開銷。因此,要使用 Hystrix ,就必須接受它帶來的開銷,以換取它所提供的的好處。
通常情況下,線程池引入的開銷足夠小,不會有重大的成本和性能影響。但對於一些訪問延遲極低的服務,如只依賴內存緩存,線程池引入的開銷就比較明顯了,這時候使用線程池隔離技術就不合適了,我們需要考慮更輕量級的方式,如信號量隔離。
2、線程隔離 - 信號量
上面提到了線程池隔離的缺點,當依賴延遲極低的服務時,線程池隔離技術引入的開銷超過了它所帶來的好處。這時候可以使用信號量隔離技術來代替,通過設置信號量來限制對任何給定依賴的併發調用量。下圖說明了線程池隔離和信號量隔離的主要區別:
使用線程池時,發送請求的線程和執行依賴服務的線程不是同一個,而使用信號量時,發送請求的線程和執行依賴服務的線程時同一個, 都是發起請求的線程。
3、線程隔離總結
線程池和信號量都可以做線程隔離,但各有各的優缺點和支持的場景,對比如下:
線程池和信號量都支持熔斷和限流。相比線程池,信號量不需要線程切換,因此避免了不必要的開銷。但是信號量不支持異步,也不支持超時,也就是說當所請求的服務不可用時,信號量會控制超過限制的請求立即返回,但是已經持有信號量的線程只能等待服務響應或從超時中返回,即可能出現長時間等待。線程池模式下,當超過指定時間未響應的服務, Hystrix 會通過響應中斷的方式通知線程立即結束並返回。
(二)熔斷器
現實生活中,可能大家都有注意到家庭電路中通常會安裝一個保險盒,當負載過載時,保險盒中的保險絲會自動熔斷,以保護電路及家裏的各種電器,這就是熔斷器的一個常見例子。Hystrix 中的熔斷器 (Circuit Breaker) 也是起類似作用,Hystrix 在運行過程中會向每個 commandKey 對應的熔斷器報告成功、失敗、超時和拒絕的狀態,熔斷器維護並統計這些數據,並根據這些統計信息來決策熔斷開關是否打開。如果打開,熔斷後續請求,快速返回。隔一段時間(默認是 5s)之後熔斷器嘗試半開,放入一部分流量請求進來,相當於對依賴服務進行一次健康檢查,如果請求成功,熔斷器關閉。
熔斷器配置,Circuit Breaker 主要包括如下 6 個參數:
1、circuitBreaker.enabled
是否啓用熔斷器,默認是 TRUE。
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() 判斷是否允許將請求提交到線程池
1、允許熔斷器強制打開, circuitBreaker.forceOpen 爲 true,不允許放行,返回。
2、如果熔斷器強制關閉, circuitBreaker.forceOpen 爲 true,允許放行。 此外不必關注熔斷器實際狀態,也就是說熔斷器仍然會維護統計數據和開關狀態,只是不生效而已。
第二步,調用 isOpen() 判斷熔斷器開關是否打開
1、 如果熔斷器開關打開,進入第三步,否則繼續;
2、 如果一個週期內總的請求數小於 circuitBreaker.requestVolumeThreshold 的值,允許請求放行,否則繼續;
3、 如果一個週期內錯誤率小於 circuitBreaker.errorThresholdPercentage 的值,允許請求放行。否則,打開熔斷器開關,進入第三步。
第三步, 調用 allowSingleTest() 判斷是否允許單個請求通行,檢查依賴服務是否恢復
如果熔斷器打開,且距離熔斷器打開的時間或上一次試探請求放行的時間超過 circuitBreaker.sleepWindowInMilliseconds 的值時,熔斷器器進入半開狀態,允許放行一個試探請求;否則,不允許放行。
此外,爲了提供決策依據,每個熔斷默認維護了 10 個 bucket,每秒一個 bucket,當心的 bucket 被創建時,最舊的 bucket 會被拋棄。其中每個 bucket 維護了請求、失敗、超時、拒絕的計數器,Hystrix 負責收集並統計這些計數器。
(四)回退降級
降級,通常指事務高峯期,爲了保證核心服務正常運行,需要停掉一些不太重要的業務,或者某些服務不可用時,執行備用邏輯從故障服務中快速失敗或快速返回,以保障主體業務不受影響。 Hystrix 提供的降級主要是爲了容錯,保證當前服務不受依賴服務故障的影響,從而提高服務的健壯性。要支持回退或降級處理,可以重寫 HystrixCommand 的 getFallBack 方法或 HystrixObservableCommand 的 resumeWithFallback 方法。
1、Hystrix 在以下幾種情況下會走降級邏輯:
-
執行 construct() 或 run() 拋出異常
-
熔斷器打開導致命令短路
-
命令的線程池和隊列或信號量的容量超額,命令被拒絕
-
命令執行超時
2、降級回退方式
(1)Fail Fast 快速失敗
快速失敗是最普通的命令執行方法,命令沒有重寫降級邏輯。 如果命令執行發生任何類型的故障,它將直接拋出異常。
(2)Fail Fast 無聲失敗
指在降級方法中通過返回 null,空 Map,空 List 或其他類似的響應來完成。
(3)FallBack:Static
指在降級方法中返回靜態默認值。這不會導致服務以 “無聲失敗” 的方式被刪除,而是導致默認行爲發生。如:應用根據命令執行返回 true / false 執行相應邏輯,但命令執行失敗,則默認爲 true。
(4)FallBack:Stubbed
當命令返回一個包含多個字段的複合對象時,適合以 Stubbed 的方式回退。
(5)FallBack:Cache via Network
有時,如果調用依賴服務失敗,可以從緩存服務(如 redis)中查詢舊數據版本。由於又會發起遠程調用,所以建議重新封裝一個 Command,使用不同的 ThreadPoolKey,與主線程池進行隔離。
(6)Primary+Secondary with FallBack
有時系統具有兩種行爲 - 主要和次要,或主要和故障轉移。主要和次要邏輯涉及到不同的網絡調用和業務邏輯,所以需要將主次邏輯封裝在不同的 Command 中,使用線程池進行隔離。爲了實現主從邏輯切換,可以將主次 command 封裝在外觀 HystrixCommand 的 run 方法中,並結合配置中心設置的開關切換主從邏輯。由於主次邏輯都是經過線程池隔離的 HystrixCommand,因此外觀 HystrixCommand 可以使用信號量隔離,而沒有必要使用線程池隔離引入不必要的開銷。原理圖如下:
主次模型的使用場景還是很多的。如當系統升級新功能時,如果新版本的功能出現問題,通過開關控制降級調用舊版本的功能。
通常情況下,建議重寫 getFallBack 或 resumeWithFallback 提供自己的備用邏輯,但不建議在回退邏輯中執行任何可能失敗的操作。
六、總結
本文介紹了 Hystrix 及其工作原理,還介紹了 Hystrix 線程池隔離、信號量隔離和熔斷器的工作原理,以及如何使用 Hystrix 的資源隔離,熔斷和降級等技術實現服務容錯,從而提高系統的整體健壯性。
雖然 Hystrix 已經停更很久了,Spring Cloud 體系的使用者和擁護者一片哀嚎,實際上,spring 作爲 java 最大的家族,根本不需要擔心其中一兩個組件的廢棄, Hystrix 的停更,只會催生更多更好的組件替代它,但是 Hystrix 既然存在過,就一定就它存在的價值,既然存在,我就必須搞懂它。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/paZmZJ5MmcsuiJ65RHqirg