Hystrix 自動降級與依賴隔離原理

longmanma發表於2021-09-09

1.背景

目前對於一些非核心操作,如增減庫存後儲存操作日誌 傳送非同步訊息時(具體業務流程),一旦出現MQ服務異常時,會導致介面響應超時,因此可以考慮對非核心操作引入服務降級、服務隔離。

2.Hystrix說明

官方文件 [

hystrix是netflix開源的一個容災框架,解決當外部依賴故障時拖垮業務系統、甚至引起雪崩的問題。

2.1為什麼需要Hystrix?

在大中型分散式系統中,通常系統很多依賴(HTTP,hession,Netty,Dubbo等),在高併發訪問下,這些依賴的穩定性與否對系統的影響非常大,但是依賴有很多不可控問題:如網路連線緩慢,資源繁忙,暫時不可用,服務離線等。

當依賴阻塞時,大多數伺服器的執行緒池就出現阻塞(BLOCK),影響整個線上服務的穩定性,在複雜的分散式架構的應用程式有很多的依賴,都會不可避免地在某些時候失敗。高併發的依賴失敗時如果沒有隔離措施,當前應用服務就有被拖垮的風險。
例如:一個依賴30個SOA服務的系統,每個服務99.99%可用。 99.99%的30次方 ≈ 99.7% 0.3% 意味著一億次請求 會有 3,000,00次失敗 換算成時間大約每月有2個小時服務不穩定. 隨著服務依賴數量的變多,服務不穩定的機率會成指數性提高.
解決問題方案:對依賴做隔離。

2.2Hystrix設計理念

想要知道如何使用,必須先明白其核心設計理念,Hystrix基於命令模式

圖片描述


Command是在Receiver和Invoker之間新增的中間層,Command實現了對Receiver的封裝
那麼Hystrix的應用場景如何與上圖對應呢?


API既可以是Invoker又可以是reciever,透過繼承Hystrix核心類HystrixCommand來封裝這些API(例如,遠端介面呼叫,資料庫查詢之類可能會產生延時的操作)。就可以為API提供彈性保護了。

2.3 Hystrix如何解決依賴隔離

1: Hystrix使用命令模式HystrixCommand(Command)包裝依賴呼叫邏輯,每個命令在單獨執行緒中/訊號授權下執行。
2: 可配置依賴呼叫超時時間,超時時間一般設為比99.5%平均時間略高即可.當呼叫超時時,直接返回或執行fallback邏輯。
3: 為每個依賴提供一個小的執行緒池(或訊號),如果執行緒池已滿呼叫將被立即拒絕,預設不採用排隊.加速失敗判定時間。
4: 依賴呼叫結果分:成功,失敗(丟擲異常),超時,執行緒拒絕,短路。 請求失敗(異常,拒絕,超時,短路)時執行fallback(降級)邏輯。
5: 提供熔斷器元件,可以自動執行或手動呼叫,停止當前依賴一段時間(10秒),熔斷器預設錯誤率閾值為50%,超過將自動執行。
6: 提供近實時依賴的統計和監控

2.4Hystrix流程結構解析

圖片描述


1. 構建一個HystrixCommand或者HystrixObservableCommand

一個HystrixCommand或一個HystrixObservableCommand物件
代表了對某個依賴服務發起的一次請求或者呼叫
同時在建構函式中傳入所有需要的引數

  • HystrixCommand主要用於僅僅會返回一個結果的呼叫


    圖片描述


    圖片描述

  • HystrixObservableCommand主要用於可能會返回多條結果的呼叫


    圖片描述


    圖片描述

HystrixCommand command = new HystrixCommand(arg1, arg2);
HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);

2. 呼叫command的執行方法

要執行Command,需要在4個方法中選擇其中的一個:execute(),queue(),observe(),toObservable()。其中execute()和queue()僅僅對HystrixCommand適用

  • execute():呼叫後直接block,屬於同步呼叫,直到依賴服務返回單條結果,或者丟擲異常


    圖片描述

  • queue():返回一個Future,屬於非同步呼叫,後面可以透過Future獲取單條結果


    圖片描述

  • observe():訂閱一個Observable物件,Observable代表的是依賴服務返回的結果,獲取到一個那個代表結果的 Observable物件的複製物件

  • toObservable():返回一個Observable物件,如果我們訂閱這個物件,就會執行command並且獲取返回結果

R value   = command.execute();
Future<R>     fValue  = command.queue();
Observable<R> ohValue = command.observe();         
Observable<R> ocValue = command.toObservable();

3. 檢查是否開啟快取

如果這個command開啟了請求快取request cache,而且這個呼叫的結果在快取中存在,那麼直接從快取中返回Observer結果。
一般來說,所謂的request cache只是針對一次request context,類似於web應用中響應一個請求去呼叫多個依賴服務,對同一個依賴的相同引數的呼叫可以放到快取從而減少網路請求來提升效能。
具體實現可以透過新增過濾器來新增一個HystrixRequestContext

 public class HystrixRequestContextServletFilter implements Filter {    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
     throws IOException, ServletException {
        HystrixRequestContext context = HystrixRequestContext.initializeContext();        try {
            chain.doFilter(request, response);
        } finally {
            context.shutdown();
        }
    }
}

在Command實現中有CacheKey的設定方法:

public class CommandUsingRequestCache extends HystrixCommand<Boolean> {
    ...    @Override
    protected String getCacheKey() {        return String.valueOf(value);
    }
}

4.檢查是否開啟了斷路器

在命令結果沒有快取命中的時候, Hystrix 會在執行命令前檢查斷路器是否為開啟狀態
即檢查這個command對應的依賴服務是否開啟了斷路器

  • 斷路器被開啟,不執行該command,直接執行fallback降級機制

  • 斷路器被關閉,第5步

斷路器的實現原理:

  1. 控制短路器是否允許工作,包括跟蹤依賴服務呼叫的健康狀況,以及對異常情況過多時是否允許觸發短路,預設是true,一般不需要修改


    圖片描述

  2. 只要執行一個command,這個請求就一定會經過斷路器
    如果在一定時間內經過斷路器的流量超過閾值,才會進行後面的斷路器相關處理
    可以透過以下配置修改,預設值是20


    圖片描述

  3. 如果斷路器統計到的異常呼叫的佔比超過了一定的閾值,才會開啟斷路器開關
    預設是50%的異常比例


    圖片描述

  4. 經過以上步驟,然後斷路器從close狀態轉換到open狀態

  5. 斷路器開啟的時候,所有經過該斷路器的請求全部被短路,不呼叫後端服務,直接走fallback降級(第8步)

  6. 經過了一段時間之後,斷路器會進入half-open狀態,讓一條請求經過斷路器,看能不能正常呼叫

  • 如果呼叫成功了,那麼就自動恢復,轉到close狀態
    時間可以透過以下配置來修改,預設為5000毫秒

    圖片描述

  1. 可以強迫開啟短路器,一般不使用


    圖片描述

  2. 強迫關閉短路器,一般不使用


    圖片描述

5.檢查執行緒池/佇列/semaphore是否已滿

如果command對應的執行緒池/佇列/semaphore(不使用執行緒池時)已經滿了,那麼也不會執行command
直接去呼叫fallback降級機制

6.執行command

呼叫HystrixObservableCommand.construct()或HystrixCommand.run()來實際執行這個command

  • HystrixCommand.run() 返回單條結果,或者丟擲一個異常

  • HystrixObservableCommand.construct() 返回一個Observable物件,可以獲取多條結果或者 onError 傳送錯誤通知

如果HystrixCommand.run()或HystrixObservableCommand.construct()的執行,超過了timeout
那麼command所在的執行緒就會丟擲一個TimeoutException,會去執行fallback降級機制,而且就不會管run()或construct()返回的值
這裡要注意的一點是,我們是不可能終止掉一個呼叫嚴重延遲的依賴服務的執行緒的,只能說給你丟擲來一個TimeoutException,但是還是可能會因為嚴重延遲的呼叫執行緒佔滿整個執行緒池的,即使這個時候新來的流量都被限流了。如果沒有timeout的話,那麼就會拿到一些呼叫依賴服務獲取到的結果,然後hystrix會做一些logging記錄和metric統計。

有一個很重要的點,command的執行強烈建議我們設定一個timeout的時間,來避免所有資源都被佔用導致系統整體效能下降,可以透過以下來配置:

//預設是true開啟超時控制HystrixCommandProperties.Setter()
   .withExecutionTimeoutEnabled(boolean value)//預設1000msHystrixCommandProperties.Setter()
   .withExecutionTimeoutInMilliseconds(int value)

當command執行超時之後會直接進行fallback降級處理

7.短路健康檢查

Hystrix會將每一個依賴服務的呼叫成功,失敗,拒絕,超時,等事件,都會傳送給circuit breaker斷路器
短路器就會對呼叫成功/失敗/拒絕/超時等事件的次數進行統計。短路器會根據這些統計次數來決定,是否要進行短路,如果開啟了短路器,那麼在一段時間內就會直接短路,然後如果在之後第一次檢查發現呼叫成功了,就關閉斷路器

8.呼叫fallback降級機制

一般來說有四種情況會呼叫fallback降級機制

  • Hystrix呼叫各種外部介面,或者訪問外部依賴,mysql,redis,zookeeper,kafka,等等出現了任何異常

  • 對外部的依賴呼叫所使用的執行緒池已滿/訊號量限流資源到達極限

  • 訪問時間過長,可能就會導致超時,報一個TimeoutException異常

  • 基於上述三種情況都會傳送異常事件到斷路器中去進行統計,如果異常達到一定的比例直接開啟斷路器

兩種常見的降級處理是

  • 維護記憶體ECache直接獲取一份過期的資料

  • 設定一個預設值返回

public class CommandHelloFailure extends HystrixCommand<String> {    private final String name;    public CommandHelloFailure(String name) {        super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));        this.name = name;
    }    @Override
    protected String run() {        throw new RuntimeException("this command always fails");
    }    @Override
    protected String getFallback() {        return "Hello Failure " + name + "!";
    }
}

HystrixObservableCommand,是實現resumeWithFallback方法

一般在降級機制中,都建議給出一些預設的返回值,比如靜態的一些程式碼邏輯,或者從記憶體中的快取中提取一些資料,儘量在這裡不要再進行網路請求了。即使在降級中,一定要進行網路呼叫,也應該將那個呼叫放在一個HystrixCommand中,進行隔離

設定fallback.isolation.semaphore.maxConcurrentRequests,這個引數設定了HystrixCommand.getFallback()最大允許的併發請求數量,預設值是10,也是透過semaphore訊號量的機制去限流。如果超出了這個最大值,那麼直接被reject

HystrixCommandProperties.Setter().withFallbackIsolationSemaphoreMaxConcurrentRequests(int value)
流程說明:1:每次呼叫建立一個新的HystrixCommand,把依賴呼叫封裝在run()方法中2:執行execute()/queue做同步或非同步呼叫.3:判斷熔斷器(circuit-breaker)是否開啟,如果開啟跳到步驟8,進行降級策略,如果關閉進入步驟.4:判斷執行緒池/佇列/訊號量是否跑滿,如果跑滿進入降級步驟8,否則繼續後續步驟.5:呼叫HystrixCommand的run方法.執行依賴邏輯5a:依賴邏輯呼叫超時,進入步驟8.6:判斷邏輯是否呼叫成功6a:返回成功呼叫結果6b:呼叫出錯,進入步驟8.7:計算熔斷器狀態,所有的執行狀態(成功, 失敗, 拒絕,超時)上報給熔斷器,用於統計從而判斷熔斷器狀態.8:getFallback()降級邏輯.
  以下四種情況將觸發getFallback呼叫:
 (1):run()方法丟擲非HystrixBadRequestException異常。
 (2):run()方法呼叫超時
 (3):熔斷器開啟攔截呼叫
 (4):執行緒池/佇列/訊號量是否跑滿8a:沒有實現getFallback的Command將直接丟擲異常8b:fallback降級邏輯呼叫成功直接返回8c:降級邏輯呼叫失敗丟擲異常9:返回執行成功結果

2.5 熔斷器:Circuit Breaker

每個熔斷器預設維護10個bucket,每秒一個bucket,每個bucket記錄成功,失敗,超時,拒絕的狀態


圖片描述

預設錯誤超過50%且10秒內超過20個請求進行中斷攔截.

圖片描述


2.6 Hystrix隔離分析

Hystrix隔離方式採用執行緒/訊號的方式,透過隔離限制依賴的併發量和阻塞擴散

2.6.1 執行緒隔離

  • 把執行依賴程式碼的執行緒與請求執行緒分離,請求執行緒可以自由控制離開的時間(非同步)

  • 透過執行緒池大小可以控制併發量,當執行緒池飽和時可以提前拒絕服務,防止依賴問題擴散

  • 線上建議執行緒池不要設定過大,否則大量堵塞執行緒有可能會拖慢伺服器

2.6.2 特點分析

  • 優點

    • 使用執行緒可以完全隔離第三方程式碼,請求執行緒可以快速返回

    • 當一個失敗的依賴再次變成可用時,執行緒池將清理,並立即恢復可用,而不是一個長時間的恢復

    • 可以完全模擬非同步呼叫,方便非同步程式設計

  • 缺點

    • 執行緒池的主要缺點是它增加了cpu,因為每個命令的執行涉及到排隊(預設使用SynchronousQueue避免排隊),排程和上下文切換。

    • 對使用ThreadLocal等依賴執行緒狀態的程式碼增加複雜性,需要手動傳遞和清理執行緒狀態。

NOTE: Netflix公司內部認為執行緒隔離開銷足夠小,不會造成重大的成本或效能的影響。
Netflix 內部API 每天100億的HystrixCommand依賴請求使用執行緒隔,每個應用大約40多個執行緒池,每個執行緒池大約5-20個執行緒。

2.6.3 訊號隔離

  • 訊號隔離也可以用於限制併發訪問,防止阻塞擴散, 與執行緒隔離最大不同在於執行依賴程式碼的執行緒依然是請求執行緒(該執行緒需要透過訊號申請)

  • 如果客戶端是可信的且可以快速返回,可以使用訊號隔離替換執行緒隔離,降低開銷.

  • 訊號量的大小可以動態調整, 執行緒池大小不可以.

    圖片描述


4 引數配置

圖片描述

圖片描述

image



4.1 引數說明

其他引數可參見

圖片描述

圖片描述

image

圖片描述

image

圖片描述

image






作者:芥末無疆sss
連結:
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2730/viewspace-2815644/,如需轉載,請註明出處,否則將追究法律責任。

相關文章