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如何設計和實現的。

1、Hystrix設計目標

  • 對來自依賴的延遲和故障進行防護和控制,這些依賴通常都是通過網絡訪問的。
  • 阻止失敗並迅速恢復
  • 回退並優雅降級
  • 提供近實時的監控與告警

2、Hystrix遵循的設計原則

  • 防止任何單獨的依賴耗盡資源(線程)
  • 過載立即切斷並快速失敗,防止排隊
  • 儘可能提供回退以保護用戶免受故障
  • 使用隔離技術(例如隔板、泳道和斷路器模式)來限制任何一個依賴的影響
  • 通過近實時的指標,監控和告警,確保故障被及時發現
  • 通過動態修改配置屬性,確保故障及時恢復
  • 防止整個依賴客戶端執行失敗,而不僅僅是網絡通信

3、Hystrix如何實現這些設計目標

  • 使用命令模式將所有對外部服務(或依賴關係)的調用包裝在HystrixCommand或 HystrixObservableCommand對象中,並將該對象放在單獨的線程中執行。
  • 每個依賴都維護着一個線程池(或信號量),線程池被耗盡則拒絕請求(而不是讓請求排隊)。
  • 記錄請求成功,失敗,超時和線程拒絕。
  • 服務錯誤百分比超過了閾值,熔斷器開關自動打開,一段時間內停止對該服務的所有請求。
  • 請求失敗,被拒絕,超時或熔斷時執行降級邏輯。
  • 近實時地監控指標和配置的修改。

四、Hystrix處理流程

1、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步無論成功與否,都會更新熔斷器統計信息。

2、執行命令的幾種方法

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()使用方法:

  • 調用 observe()會返回一個Observable對象
  • 調用這個 Observable對象的subscribe()方法完成事件註冊,從而獲取結果

(4)toObservable()

事件註冊後執行run()/construct(),支持接收多個值對象,取決於發射源。調用 toObservable() 會返回一個cold  Observable,也就是說,調用 toObservable() 不會立即觸發執行run()/construct(),必須有訂閱者訂閱 Observable 時纔會執行。

如果繼承的是 HystrixComman,hystrix會從線程池中取一個線程以非阻塞方式執行run(),調用線程不必等待run();如果繼承的是 HystrixObservableCommand ,將以調用線程堵塞執行construct(),調用線程需等待construct()執行完才能繼續往下走。

toObservable()使用方法:

  • 調用observe()會返回一個Observable對象
  • 調用這個 Observable對象的subscribe()方法完成事件註冊,從而獲取結果

需注意的是, HystrixCommand也支持 toObservable()和observe(), 但是即使將 HystrixCommand 轉換成Observable,它也只能發射一個值對象。只有 HystrixObservableCommand才支持發射多個值對象。

3、幾種方法的關係

  • 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既然存在過,就一定就它存在的價值,既然存在,我就必須搞懂它。

 

每一篇博客都是一種經歷,程序猿生涯的痕跡,知識改變命運,命運要由自己掌控,願你遊歷半生,歸來仍是少年。

欲速則不達,欲達則欲速!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章