SpringCloud的限流、降級和熔斷——Hystrix

1、前言java

分佈式系統環境中,服務間相似依賴很是常見,一個業餘調用一般依賴多個基礎服務。以下圖,對於同步調用,當庫存服務不可用時,商品服務請求線程被阻塞,當有大批量請求調用庫存服務時,最終可能致使整個商品服務資源耗盡,沒法繼續對外提供服務。而且這種不可用可能沿請求調用鏈向上傳遞,這種現象稱爲雪崩效應。redis

2、雪崩效應spring

一、常見場景編程

(1)硬件故障:如服務器宕機,機房斷電,光纖被挖斷等。後端

(2)流量激增:如異常流量,重試加大流量等。緩存

(3)緩存穿透:通常發生在應用重啓,全部緩存失效時,以及短期內大量緩存失效時。大量的緩存不命中,使請求直擊後端服務,形成服務提供者超負荷運行,引發服務不可用。服務器

(4)程序bug:如程序邏輯致使內存泄漏,JVM長時間FullGC等。網絡

(5)同步等待:服務間採用同步調用模式,同步等待形成的資源耗盡。併發

二、應對策略框架

針對形成雪崩效應的不一樣場景,可使用不一樣的應對策略,沒有一種通用全部場景的策略。

(1)硬件故障:多機房容災、異地多活等。

(2)流量激增:服務自動擴容、流量控制(限流、關閉重試)等。

(3)緩存穿透:緩存預加載、緩存異步加載等。

(4)程序bug:修改程序bug、及時釋放資源等。

(5)同步等待:資源隔離、MQ解耦。、不可用服務調用快速失敗等。資源隔離一般指不一樣服務調用採起不一樣的線程池;不可用服務調用快速失敗通常經過熔斷模式結合超時機制實現。

綜上所述,若是一個應用不能對來自依賴的故障進行隔離,那該應用自己就處在被拖垮的風險中。所以,爲了構建穩定、可靠的分佈式系統,咱們的服務應當具備自我保護能力,當依賴服務不可用時,當前服務啓動自我保護功能,從而避免發生雪崩效應。本文將重點介紹使用Hystrix解決同步等待的雪崩問題。

3、初探Hystrix

Hystrix,中文含義是豪豬,因其背上長滿荊棘,從而擁有了自我保護的能力。本文所說的Hystrix是Netflix公司開源的一款容錯框架,一樣具備自我保護能力。爲了實現容錯和自我保護,下面咱們看看Hystrix如何設計和實現的。

Hystrix設計目標:

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

Hystrix遵循的設計原則:

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

Hystrix如何實現這些設計目標?

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

4、Hystrix處理流程

(一)Hystrix 整個工做流程以下:

一、構造一個 HystrixCommand或HystrixObservableCommand對象, 用於封裝請求,並在構造方法配置請求被執行須要的參數;

二、執行命令, Hystrix 提供了4種執行命令的方法,後面詳述;

三、判斷是否使用緩存響應請求,若啓用了緩存,且緩存可用,直接使用緩存響應請求。 Hystrix 支持請求緩存,但須要用戶自定義啓動;

四、判斷熔斷器是否打開,若是打開,調到第8步;

五、判斷線程池、隊列、信號量是否已滿,已滿則調到第8步;

六、執行 HystrixObservableCommand.construct()或HystrixCommand.run(), 若是執行失敗或者超時,跳到第8步;否者,跳到第9步;

七、統計熔斷器監控指標;

八、走Fallback降級方法;

九、返回請求響應。

從流程圖上可知道,第5步線程池、隊列、信號量已滿時,還會執行第7步邏輯,更新熔斷器統計信息,而第6步不管成功與否,都會更新熔斷器統計信息。

(二)執行命令的幾種方法:

Hystrix提供了4種執行命令的方法,execute()和queue()適用於 HystrixCommand 對象,而observer()和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()使用方法:

(1)調用 observe()會返回一個Observable對象

(2)調用這個 Observable對象的subscribe()方法完成事件註冊,從而獲取結果

四、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的形式做爲相應返回,不一樣執行命令的方法只是進行了相應的轉換。

5、 Hystrix 容錯

Hystrix 的容錯主要是經過添加允許延遲和容錯方法,幫助控制這些分佈式服務之間的交互。還經過隔離服務之間的訪問點,阻止它們之間的級聯故障以及提供退回選項來實現這一點,從而提升系統的總體彈性。 Hystrix主要提供了一下幾種容錯方法:

  • 資源隔離
  • 熔斷
  • 降級

(一)資源熔斷

資源隔離主要指對線程的隔離。 Hystrix提供了兩種線程隔離的方式:線程池和信號量。

一、線程隔離-線程池

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 ,就必須接受它帶來的開銷,以換取它所提供的的好處。

一般狀況下,線程池引入的開銷足夠小,不會有重大的成本和性能影響。但對於一些訪問延遲極低的服務,如只依賴內存緩存,線程池引入的開銷就比較明顯了,這時候使用線程池隔離技術就不合適了,咱們須要考慮更輕量級的方式,如信號量隔離。

二、線程隔離-信號量

上面提到了線程池隔離的缺點,當依賴延遲極低的服務時,線程池隔離技術引入的開銷超過了它所帶來的好處。這時候可使用信號量隔離技術來代替,經過設置信號量來限制對任何給定依賴的併發調用量。下圖說明了線程池隔離和信號量隔離的主要區別:

使用線程池時,發送請求的線程和執行依賴服務的線程不是同一個,而使用信號量時,發送請求的線程和執行依賴服務的線程時同一個, 都是發起請求的線程。

三、線程隔離總結

線程池和信號量均可以作線程隔離,但各有各的優缺點和支持的場景,對好比下:

  線程切換 支持異步 支持超時 支持熔斷 限流 開銷
信號量
線程池

線程池和信號量都支持熔斷和限流。相比線程池,信號量不須要線程切換,所以避免了沒必要要的開銷。可是信號量不支持異步,也不支持超時,也就是說當所請求的服務不可用時,信號量會控制超過限制的請求當即返回,可是已經持有信號量的線程只能等待服務響應或從超時中返回,便可能出現長時間等待。線程池模式下,當超過指定時間未響應的服務, Hystrix會經過響應中斷的方式通知線程當即結束並返回。

(二)熔斷器

現實生活中,可能你們都有注意到家庭電路中一般會安裝一個保險盒,當負載過載時,保險盒中的保險絲會自動熔斷,以保護電路及家裏的各類電器,這就是熔斷器的一個常見例子。Hystrix中的熔斷器(Circuit Breaker)也是起相似做用,Hystrix在運行過程當中會向每一個commandKey對應的熔斷器報告成功、失敗、超時和拒絕的狀態,熔斷器維護並統計這些數據,並根據這些統計信息來決策熔斷開關是否打開。若是打開,熔斷後續請求,快速返回。隔一段時間(默認是5s)以後熔斷器嘗試半開,放入一部分流量請求進來,至關於對依賴服務進行一次健康檢查,若是請求成功,熔斷器關閉。

熔斷器配置,Circuit Breaker主要包括以下6個參數:

一、circuitBreaker.enabled

是否啓用熔斷器,默認是TRUE。
2 、circuitBreaker.forceOpen

熔斷器強制打開,始終保持打開狀態,不關注熔斷開關的實際狀態。默認值FLASE。
三、circuitBreaker.forceClosed
熔斷器強制關閉,始終保持關閉狀態,不關注熔斷開關的實際狀態。默認值FLASE。

四、circuitBreaker.errorThresholdPercentage
錯誤率,默認值50%,例如一段時間(10s)內有100個請求,其中有54個超時或者異常,那麼這段時間內的錯誤率是54%,大於了默認值50%,這種狀況下會觸發熔斷器打開。

五、circuitBreaker.requestVolumeThreshold

默認值20。含義是一段時間內至少有20個請求才進行errorThresholdPercentage計算。好比一段時間了有19個請求,且這些請求所有失敗了,錯誤率是100%,但熔斷器不會打開,總請求數不知足20。

六、circuitBreaker.sleepWindowInMilliseconds

半開狀態試探睡眠時間,默認值5000ms。如:當熔斷器開啓5000ms以後,會嘗試放過去一部分流量進行試探,肯定依賴服務是否恢復。

(三)熔斷器工做原理

下圖展現了HystrixCircuitBreaker的工做原理:

熔斷器工做的詳細過程以下:

第一步,調用 allowRequest() 判斷是否容許將請求提交到線程池

一、容許熔斷器強制打開, circuitBreaker.forceOpen爲true,不容許放行,返回。

二、若是熔斷器強制關閉, circuitBreaker.forceOpen爲true,容許放行。 此外沒必要關注熔斷器實際狀態,也就是說熔斷器仍然會維護統計數據和開關狀態,只是不生效而已。

第二步,調用isOpen()判斷熔斷器開關是否打開

一、 若是熔斷器開關打開,進入第三步,不然繼續;

二、 若是一個週期內總的請求數小於circuitBreaker.requestVolumeThreshold的值,容許請求放行,不然繼續;

三、 若是一個週期內錯誤率小於circuitBreaker.errorThresholdPercentage的值,容許請求放行。不然,打開熔斷器開關,進入第三步。

第三步, 調用allowSingleTest()判斷是否容許單個請求通行,檢查依賴服務是否恢復

若是熔斷器打開,且距離熔斷器打開的時間或上一次試探請求放行的時間超過circuitBreaker.sleepWindowInMilliseconds的值時,熔斷器器進入半開狀態,容許放行一個試探請求;不然,不容許放行。

此外,爲了提供決策依據,每一個熔斷默認維護了10個bucket,每秒一個bucket,小心的bucket被建立時,最舊的bucket會被拋棄。其中每一個bucket維護了請求、失敗、超時、拒絕的計數器,Hystrix負責收集並統計這些計數器。

(四)回退降級

降級,一般指事務高峯期,爲了保證核心服務正常運行,須要停掉一些不過重要的業務,或者某些服務不可用時,執行備用邏輯從故障服務中快速失敗或快速返回,以保障主體業務不受影響。 Hystrix提供的降級主要是爲了容錯,保證當前服務不受依賴服務故障的影響,從而提升服務的健壯性。要支持回退或降級處理,能夠重寫 HystrixCommand的getFallBack方法或HystrixObservableCommand的resumeWithFallback方法。

一、Hystrix在如下幾種狀況下會走降級邏輯:

  • 執行construct()或run()拋出異常
  • 熔斷器打開致使命令短路
  • 命令的線程池和隊列或信號量的容量超額,命令被拒絕
  • 命令執行超時

二、降級回退方式

(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提供本身的備用邏輯,但不建議在回退邏輯中執行任何可能失敗的操做。

6、總結

本文介紹了Hystrix及其工做原理,還介紹了Hystrix線程池隔離、信號量隔離和熔斷器的工做原理,以及如何使用Hystrix的資源隔離,熔斷和降級等技術實現服務容錯,從而提升系統的總體健壯性。

雖然Hystrix已經停更好久了,Spring Cloud體系的使用者和擁護者一片哀嚎,實際上,spring做爲java最大的家族,根本不須要擔憂其中一兩個組件的廢棄, Hystrix的停更,只會催生更多更好的組件替代它,可是 Hystrix既然存在過,就必定就它存在的價值,既然存在,我就必須搞懂它。

 

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

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

更多精彩內容,首發公衆號【素小暖】,歡迎關注。

相關文章
相關標籤/搜索