Circuit Breaker模式

Circuit Breaker模式會處理一些需要一定時間來重連遠程服務和遠端資源的錯誤。該模式可以提高一個應用的穩定性和彈性。

問題

在類似於雲的分佈式環境中,當一個應用需要執行一些訪問遠程資源或者是遠端服務的時候,是很容易碰到一些偶然的錯誤的,比如說,網絡連接速度很慢,超時,或者是資源的過量使用,或者臨時資源不再可用等等。這一類的錯誤通常來說會在短暫的時間內,自動恢復過來。一個健壯的雲應用也該能夠通過一些策略能夠處理這類錯誤,比如使用重試模式。

然而,也有一些情況,錯誤是出於一些意想不到的事件,這類事件很難預期,而且需要消耗很多時間來自動恢復。這些錯誤在嚴重性上也從丟失部分連接到整個服務的失敗。在這些情況下,讓應用繼續重試或者執行操作就已經沒有意義了。相對的,應用應該迅速令服務失敗,而根據錯誤類型來嘗試採取對應的措施。

另外,如果一個服務非常繁忙,系統的部分錯誤有可能會導致雪崩效應。舉例來說,一個操作其他服務的操作可以配置超時時間的,如果服務再一段時間無法應答,調用方可以返回錯誤信息的。然而這個策略可能導致大量針對這個服務的請求阻塞直至Timeout時間到了。這些阻塞的請求可能會持有系統關鍵的資源,比如內存,線程,數據庫連接等等信息。因此,引用的資源也可能會被耗盡,造成系統內其他不相關部分得失敗。在這些情況下,最好的方法是令這些配置超時的操作立刻失敗,並且只有當服務可能成功的時候纔去調用。當然,配置較短的超時時間也能改善這一問題,但是超時時間如果配置的太短,服務的調用反而會因爲大量的超時而失敗。

解決方案

Circuit-Breaker模式可以防止應用重複的嘗試調用容易失敗的操作,當Circuit-Breaker模式判斷錯誤會持續的時候,它會令操作不再持續等待,以免繼續浪費CPU資源。當然,Circuit-Breaker模式也令應用本身可以發現錯誤有沒有被修復。如果發生的問題已經被修復了,應用可以重新嘗試去調用服務。

Circuit-Breaker模式的目的和Retry模式的目的是不同的。Retry模式令應用不斷的重試調用,直到最後成功。而Circuit-Breaker模式是阻止應用繼續嘗試無意義的請求。應用可以同時使用兩種模式。然而,重試邏輯應用對於所有的Circuit-Breaker返回的異常十分敏感,這樣可以在Circuit-Breaker發現錯誤短時間無法修復的情況下直接不再繼續重試。

Circuit-Breaker的作用就好似可能失敗操作的代理。代理會監控最近發生的錯誤,然後依據這一信息來決定是否允許操作的繼續執行,或者直接立刻返回異常信息。

Circuit-Breaker可以按照如下的狀態來模仿一個斷路器來實現:

  • 關閉:應用的請求已經路由到了這個操作。代理應該維護最近一段時間的錯誤信息,如果調用操作失敗,那麼大力增加這個錯誤信息的數量。如果這個錯誤數量超過給定時間的閾值,代理進入到打開狀態。這個時候,代理啓動一個超時的Timer,當Timer過期了,代理則進入半開狀態。

超時Timer的目的是爲了給系統一段時間來自我修復之前碰到的問題。


  • 打開:令可能失敗的外部調用操作立刻失敗,所有的外部調用直接拋異常給應用。
  • 半開:只有一定數量的應用請求可以進行操作的調用。如果這些請求成功了,那麼就假定之前發成的錯誤已經被系統自動修復了,而Circuit-Breaker轉換成關閉狀態(同時重置錯誤計數器)。如果任何請求失敗了,那麼Circuit-Breaker會假定錯誤仍然在存在,Circuit-Breaker會重新轉換成打開狀態,並重啓超時Timer給系統更多的時間來自我修復錯誤。

半開狀態可以很有效的阻止一個可以恢復的服務被大量的請求所淹沒。而處於恢復中的服務,可能也能夠承載一定數量的請求,直到完全恢復才能恢復全部的吞吐量。但是,突然大量的錯誤也可能會令恢復中的服務重新crash掉。
參考如下狀態變換圖:

這裏寫圖片描述

需要注意的是,上圖中,關閉狀態所用的錯誤計數器是基於時間的。它會以一定的時間間隔來重置。這也能夠在常見錯誤的情況下不讓Circuit-Breaker模式進入打開狀態。而錯誤計數閾值纔會令Circuit-Breaker進入到打開狀態,只有當指定時間間隔內,錯誤計數達到閾值才能令Circuit-Breaker進入到打開狀態。半開狀態所使用的成功計數器則會記錄成功的調用次數。Circuit-Breaker如果在之後出現了連續的成功的調用,那麼Circuit-Breaker就會進入關閉狀態。如果任何調用的失敗了,那麼Circuit-Breaker也會重新進入到打開狀態,成功計數器也會重置,直到下次重新進入到半開狀態。

通常系統的外部恢復,很多時候都是通過重啓失敗的組件或者修復網絡連接來完成的。

實現Circuit-Breaker模式可以增加系統的穩定性和彈性,當系統從錯誤恢復的時候,可以儘可能所有失敗對系統性能的影響。Circuit-Breaker模式可以通過拒絕外部調用來保證服務的響應時間,而不是等待操作的超時(或者持續阻塞)。如果Circuit-Breaker在每一次狀態改變的時候觸發一些事件的話,這個狀態的改變也可以用來監視Circuit-Breaker保護模塊的健康狀態,或者是對監控Circuit-Breaker的管理員發出警告,Circuit-Breaker已經進入了打開狀態。

Circuit-Breaker模式可以很好的定製並適配很多可能的錯誤。舉例來說,開發者可以應用一個增長的超時Timer,也可以直接令Circuit-Breaker在處於打開狀態幾秒,如果錯誤在之後還沒有解決,就超時幾分鐘等等。在有些場景下,打開狀態的Circuit-Breaker也可以不拋出異常而是返回默認值來改善應用的響應。

需要考慮的問題

開發者在實現Circuit-Breaker模式的時候,有如下的一些地方需要注意:

  • 異常的處理。應用如果通過Circuit-Breaker來調用操作的話,就必須能夠處理操作失效所引起的異常。而處理這些異常的代碼將會和應用是高度相關的。舉例來說,應用可以選擇暫時降低其服務,調用其他的操作來達到完成相同的任務,或者獲取相同的數據,或者拋出異常給用戶令其稍後重試。
  • 異常的類型。請求失敗可能有有多個原因,有些錯誤可能會比其他的錯誤更嚴重。舉個例子,請求失敗可能是因爲遠端的服務crash掉了,需要幾分鐘時間來恢復,或者只是因爲服務過載而造成的暫時性服務超時。Circuit-Breaker也能夠判斷錯誤的類型,來調整不同的策略。舉例來說,針對超時配置的錯誤計數閾值可以配置的比服務失效的閾值更高。
  • 日誌。Circuit-Breaker應該把所有的失敗請求都記錄日誌(和可能成功的請求),這樣可以讓管理員監控到外部調用的健康狀態。
  • 恢復性。開發者應該爲其保護的遠端調用進行合理的配置來匹配遠端調用的恢復模式。舉例來說,如果Circuit-Breaker配置的停留在打開狀態很久的話,就算遠端服務已經可用了,因爲Circuit-Breaker的打開狀態,會令服務的狀態仍然處於不可用狀態。類似的,如果配置Circuit-Breaker的恢復時間太快,也會讓應用在打開狀態和半開狀態之間不斷震盪。
  • 測試失效操作。在打開狀態,相對於使用Timer來判斷什麼時間轉換爲半開狀態,Circuit-Breaker也可以選擇間隔性的ping遠端的服務(資源)來決定其是否可用。ping操作也可以作爲一種嘗試遠端調用的方式,當然,也可以用遠端服務提供的其他接口來測試服務是否正常。這在Health-Endpoint監控模式中有所描述。
  • 手動覆蓋。在某個系統中,如果其失效的時間是可見的,Circuit-Breaker也可以提供一些手動恢復的選項,來令管理員強制的關閉Circuit-Breaker。類似的,管理員也可以在遠端服務暫時失效的情況下強制性配置Circuit-Breaker進入打開狀態(重啓超時Timer)
  • 併發。同一個Circuit-Breaker很可能被應用的實例大量併發訪問。所以其實現應該是非阻塞的或者對於每個請求都增加更多的消費。
  • 資源的不同。需要注意的是,當爲一種類型的資源配置一個Circuit-Breaker但是可能有多個獨立的提供資源的服務時要尤其小心。舉例來說,在一個包含多個Shard的數據倉庫中,其中的一個Shard沒問題而另一個可能短時間內訪問有問題。如果錯誤的返回在上面的場景中混合在了一起,即使錯誤很類似,應用也會嘗試訪問,從而阻塞的。
  • 加速斷路。有的時候,錯誤的應答信息足夠判斷當前的狀態而讓Circuit-Breaker立刻觸發。舉個例子:如果一個Shard的返回信息表示,不建議立刻重試,希望在幾分鐘後重試的時候,那麼Circuit-Breaker就不需要計數器到達閾值在進入Open狀態了。

    HTTP協議定義:503表示服務不可用,如果請求的服務當前在web服務器上面不可用,就可以返回503。這個應答信息就可以包含額外的信息,比如期望的延遲重試時間。

  • 重演失敗請求。在打開狀態,相對於讓服務快速的失敗,Circuit-Breaker也可以記錄具體的請求,然後在稍後的時間,重新令這些失敗的請求再來請求。

  • 外部請求上的不恰當超時時間。如果對於外部請求的超時時間配置的過長的話,Circuit-Breaker可能很難保護應用。如果超時時間過長,運行Circuit-Breaker的線程可能在認爲服務失敗之前,就被完全阻塞了。這種情況下,應用的實例就算是Circuit-Breaker觸發了,進入了Open狀態,也會有相當數量的線程處於阻塞狀態的。

何時使用該模式

使用該模式:

  • 當需要阻止應用不斷嘗試調用遠端服務或者訪問共享資源,並且這些請求很容易失敗的時候使用Circuit-Breaker模式很合適。

什麼場景不適合使用該模式:

  • 當用來處理訪問本地資源,比如內存中的數據結構的時候,不適合使用。在這種場景下,Circuit-Breaker只會給應用帶來額外的負擔。
  • 將Circuit-Breaker作爲處理應用中的業務邏輯中的異常處理的一部分也是不合適的。

Circuit-Breaker使用舉例

在web應用中,有些頁面是需要從外部的服務來獲取數據的。如果系統實現了最小額度的緩存,那麼頁面的大量訪問可能就會引起大量的調用。如果web應用和外部服務之間配置了超時(比如60s)的話,如果外部服務沒有響應,頁面會認爲服務失效並拋出異常。
然而,如果服務失敗了,而系統仍然非常的頻繁訪問,用戶可能會被迫等待60秒,然後看到無結果。最後,像是內存,連接數,以及線程等資源都不足了,就算用戶不再訪問外部資源,可能服務也會被拒絕的。
當然,增加web服務器和使用負載均衡等方式都能一定程度上防止資源的耗盡,但是,這樣仍然無法解決用戶長時間等待沒有響應頁面的問題。
通過使用Circuit-Breaker來包裹鏈接外部服務的邏輯可以有效削弱上面提到的問題。用戶請求將會失敗,但是請求會立刻失敗,但是不會導致請求資源的阻塞。
CircuitBreaker類通過內部一個ICircuitBreakerStateStore對象來維護Circuit-Breaker的狀態信息。參考如下代碼:

interface ICircuitBreakerStateStore
{
    CircuitBreakerStateEnum State { get; }
    Exception LastException { get; }
    DateTime LastStateChangedDateUtc { get; }
    void Trip(Exception ex);
    void Reset();
    void HalfOpen();
    bool IsClosed { get; }
}

其中的State屬性表示Circuit-Breaker當前的狀態,其中包含前面所提到的三個狀態Open,HalfOpen,ClosedIsClose屬性在狀態爲Closed的時候就會返回trueTrip(Exception ex)方法會將Circuit-Breaker的狀態,轉換到Open的狀態,並且記錄引起狀態變化的異常信息,以及發生異常的時間等信息。LastException屬性以及LastStateChangeDateUtc屬性就是用來獲取狀態轉換的異常以及時間信息的。Reset()方法則會關閉Circuit-Breaker,HalfOpen()方法則是將Circuit-Breaker的狀態置爲HalfOpen

public class CircuitBreaker
{
    private readonly ICircuitBreakerStateStore stateStore =
        CircuitBreakerStateStoreFactory.GetCircuitBreakerStateStore();
    private readonly object halfOpenSyncObject = new object ();
    ...

    public bool IsClosed { get { return stateStore.IsClosed; } }

    public bool IsOpen { get { return !IsClosed; } }

    public void ExecuteAction(Action action)
    {
        ...
        if (IsOpen)
        {
            // The circuit breaker is Open.
            ... (see code sample below for details)
        }
        // The circuit breaker is Closed, execute the action.
        try
        {
            action();
        }
        catch (Exception ex)
        {
            // If an exception still occurs here, simply
            // re-trip the breaker immediately.
            this.TrackException(ex);
            // Throw the exception so that the caller can tell
            // the type of exception that was thrown.
            throw;
        }
    }
    private void TrackException(Exception ex)
    {
        // For simplicity in this example, open the circuit breaker on the first exception.
        // In reality this would be more complex. A certain type of exception, such as one
        // that indicates a service is offline, might trip the circuit breaker immediately.
        // Alternatively it may count exceptions locally or across multiple instances and
        // use this value over time, or the exception/success ratio based on the exception
        // types, to open the circuit breaker.
        this.stateStore.Trip(ex);
    }
}

CircuitBreaker會創建一個實現ICircuitBreakerStateStore的實例來維護CircuitBreaker的狀態。其中的ExecuteAction(Action action)方法會包含一個可能出錯的方法。當這個方法運行的時候,會首先檢查Circuit-Breaker的狀態,如果是關閉狀態,則會正常的執行遠端服務的調用。如果這個操作失敗掉了,則會通過TrackException(Exception ex)方法將Circuit-Breaker的狀態置爲打開狀態。下面參考IsOpen中的代碼:

if (IsOpen)
{
    // The circuit breaker is Open. Check if the Open timeout has expired.
    // If it has, set the state to HalfOpen. Another approach may be to simply
    // check for the HalfOpen state that had be set by some other operation.
    if (stateStore.LastStateChangedDateUtc + OpenToHalfOpenWaitTime < DateTime.UtcNow)
    {
        // The Open timeout has expired. Allow one operation to execute. Note that, in
        // this example, the circuit breaker is simply set to HalfOpen after being
        // in the Open state for some period of time. An alternative would be to set
        // this using some other approach such as a timer, test method, manually, and
        // so on, and simply check the state here to determine how to handle execution
        // of the action.
        // Limit the number of threads to be executed when the breaker is HalfOpen.
        // An alternative would be to use a more complex approach to determine which
        // threads or how many are allowed to execute, or to execute a simple test
        // method instead.
        bool lockTaken = false;
        try
        {
            Monitor.TryEnter(halfOpenSyncObject, ref lockTaken)
            if (lockTaken)
            {
                // Set the circuit breaker state to HalfOpen.
                stateStore.HalfOpen();
                // Attempt the operation.
                action();
                // If this action succeeds, reset the state and allow other operations.
                // In reality, instead of immediately returning to the Open state, a counter
                // here would record the number of successful operations and return the
                // circuit breaker to the Open state only after a specified number succeed.
                this.stateStore.Reset();
                return;
            }
        }
        catch (Exception ex)
        {
            // If there is still an exception, trip the breaker again immediately.
            this.stateStore.Trip(ex);
            // Throw the exception so that the caller knows which exception occurred.
            throw;
        } 
        finally
        {
            if (lockTaken)
            {
                Monitor.Exit(halfOpenSyncObject);
            }
        }
    }
}

上面的Circuit-Breaker的策略很簡單,就是等待一定的時間,然後才進入HalfOpen狀態,如果action()成功了,則重新恢復到Close狀態。如果action()失敗了則Circuit-Breaker重新進入到Open狀態。

相關的其他模式

下面的模式跟Circuit-Breaker模式也是相關的:

  • Retry模式:重試模式屬於Circuit-Breaker模式的一個附屬。主要處理的問題是當訪問遠程服務不可用的時候,令應用如何來處理可以預期的短時間的錯誤。
  • Health Endpoint Monitoring模式:Circuit-Breaker可以通過發送請求到遠端的服務提供的特殊的服務來監控對面服務的的健康狀態。該服務需要返回一些信息來展示其健康的狀態。
發佈了44 篇原創文章 · 獲贊 24 · 訪問量 36萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章