001熔斷器設計模式


若是你們有印象的話,尤爲是夏天,若是家裏用電負載過大,好比開了不少家用電器,就會」自動跳閘」,此時電路就會斷開。在之前更古老的一種方式是」保險絲」,當負載過大,或者電路發生故障或異常時,電流會不斷升高,爲防止升高的電流有可能損壞電路中的某些重要器件或貴重器件,燒燬電路甚至形成火災。保險絲會在電流異常升高到必定的高度和熱度的時候,自身熔斷切斷電流,從而起到保護電路安全運行的做用。html

一樣,在大型的軟件系統中,若是調用的遠程服務或者資源因爲某種緣由沒法使用時,若是沒有這種過載保護,就會致使請求的資源阻塞在服務器上等待從而耗盡系統或者服務器資源。不少時候剛開始可能只是系統出現了局部的、小規模的故障,然而因爲種種緣由,故障影響的範圍愈來愈大,最終致使了全局性的後果。軟件系統中的這種過載保護就是本文將要談到的熔斷器模式(Circuit Breaker)數據庫

一 問題的產生

在大型的分佈式系統中,一般須要調用或操做遠程的服務或者資源,這些遠程的服務或者資源因爲調用者不能夠控的緣由好比網絡鏈接緩慢,資源被佔用或者暫時不可用等緣由,致使對這些遠程資源的調用失敗。這些錯誤一般在稍後的一段時間內能夠恢復正常。設計模式

可是,在某些狀況下,因爲一些沒法預知的緣由致使結果很難預料,遠程的方法或者資源可能須要很長的一段時間才能修復。這種錯誤嚴重到系統的部分失去響應甚至致使整個服務的徹底不可用。在這種狀況下,採用不斷地重試可能解決不了問題,相反,應用程序在這個時候應該當即返回而且報告錯誤。安全

一般,若是一個服務器很是繁忙,那麼系統中的部分失敗可能會致使 「連鎖失效」(cascading failure)。好比,某個操做可能會調用一個遠程的WebService,這個service會設置一個超時的時間,若是響應時間超過了該時間就會拋出一個異常。可是這種策略會致使併發的請求調用一樣的操做會阻塞,一直等到超時時間的到期。這種對請求的阻塞可能會佔用寶貴的系統資源,如內存,線程,數據庫鏈接等等,最後這些資源就會消耗殆盡,使得其餘系統不相關的部分所使用的資源也耗盡從而拖累整個系統。在這種狀況下,操做當即返回錯誤而不是等待超時的發生多是一種更好的選擇。只有當調用服務有可能成功時咱們再去嘗試。服務器

二 解決方法

熔斷器模式能夠防止應用程序不斷地嘗試執行可能會失敗的操做,使得應用程序繼續執行而不用等待修正錯誤,或者浪費CPU時間去等到長時間的超時產生。熔斷器模式也可使應用程序可以診斷錯誤是否已經修正,若是已經修正,應用程序會再次嘗試調用操做。網絡

熔斷器模式就像是那些容易致使錯誤的操做的一種代理。這種代理可以記錄最近調用發生錯誤的次數,而後決定使用容許操做繼續,或者當即返回錯誤。數據結構

Circuit Breaker

熔斷器可使用狀態機來實現,內部模擬如下幾種狀態。併發

  • 閉合(closed)狀態: 對應用程序的請求可以直接引發方法的調用。代理類維護了最近調用失敗的次數,若是某次調用失敗,則使失敗次數加1。若是最近失敗次數超過了在給定時間內容許失敗的閾值,則代理類切換到斷開(Open)狀態。此時代理開啓了一個超時時鐘,當該時鐘超過了該時間,則切換到半斷開(Half-Open)狀態。該超時時間的設定是給了系統一次機會來修正致使調用失敗的錯誤。
  • 斷開(Open)狀態:在該狀態下,對應用程序的請求會當即返回錯誤響應。
  • 半斷開(Half-Open)狀態:容許對應用程序的必定數量的請求能夠去調用服務。若是這些請求對服務的調用成功,那麼能夠認爲以前致使調用失敗的錯誤已經修正,此時熔斷器切換到閉合狀態(而且將錯誤計數器重置);若是這必定數量的請求有調用失敗的狀況,則認爲致使以前調用失敗的問題仍然存在,熔斷器切回到斷開方式,而後開始重置計時器來給系統必定的時間來修正錯誤。半斷開狀態可以有效防止正在恢復中的服務被忽然而來的大量請求再次拖垮。

各個狀態之間的轉換以下圖:分佈式

Circuit Breaker State Change

在Close狀態下,錯誤計數器是基於時間的。在特定的時間間隔內會自動重置。這可以防止因爲某次的偶然錯誤致使熔斷器進入斷開狀態。觸發熔斷器進入斷開狀態的失敗閾值只有在特定的時間間隔內,錯誤次數達到指定錯誤次數的閾值纔會產生。在Half-Open狀態中使用的連續成功次數計數器記錄調用的成功次數。當連續調用成功次數達到某個指定值時,切換到閉合狀態,若是某次調用失敗,當即切換到斷開狀態,連續成功調用次數計時器在下次進入半斷開狀態時歸零。ide

實現熔斷器模式使得系統更加穩定和有彈性,在系統從錯誤中恢復的時候提供穩定性,而且減小了錯誤對系統性能的影響。它經過快速的拒絕那些試圖有可能調用會致使錯誤的服務,而不會去等待操做超時或者永遠不會不返回結果來提升系統的響應事件。若是熔斷器設計模式在每次狀態切換的時候會發出一個事件,這種信息能夠用來監控服務的運行狀態,可以通知管理員在熔斷器切換到斷開狀態時進行處理。

能夠對熔斷器模式進行定製以適應一些可能會致使遠程服務失敗的特定場景。好比,能夠在熔斷器中對超時時間使用不斷增加的策略。在熔斷器開始進入斷開狀態的時候,能夠設置超時時間爲幾秒鐘,而後若是錯誤沒有被解決,而後將該超時時間設置爲幾分鐘,依次類推。在一些狀況下,在斷開狀態下咱們能夠返回一些錯誤的默認值,而不是拋出異常。

三 要考慮的因素

在實現熔斷器模式的時候,如下這些因素需可能須要考慮:

  • 異常處理:調用受熔斷器保護的服務的時候,咱們必需要處理當服務不可用時的異常狀況。這些異常處理一般須要視具體的業務狀況而定。好比,若是應用程序只是暫時的功能降級,可能須要切換到其它的可替換的服務上來執行相同的任務或者獲取相同的數據,或者給用戶報告錯誤而後提示他們稍後重試。
  • 異常的類型:請求失敗的緣由可能有不少種。一些緣由可能會比其它緣由更嚴重。好比,請求會失敗多是因爲遠程的服務崩潰,這可能須要花費數分鐘來恢復;也多是因爲服務器暫時負載太重致使超時。熔斷器應該可以檢查錯誤的類型,從而根據具體的錯誤狀況來調整策略。好比,可能須要不少次超時異常才能夠判定須要切換到斷開狀態,而只須要幾回錯誤提示就能夠判斷服務不可用而快速切換到斷開狀態。
  • 日誌:熔斷器應該可以記錄全部失敗的請求,以及一些可能會嘗試成功的請求,使得的管理員可以監控使用熔斷器保護的服務的執行狀況。
  • 測試服務是否可用:在斷開狀態下,熔斷器能夠採用按期的ping遠程的服務或者資源,來判斷是否服務是否恢復,而不是使用計時器來自動切換到半斷開狀態。這種ping操做能夠模擬以前那些失敗的請求,或者可使用經過調用遠程服務提供的檢查服務是否可用的方法來判斷。
  • 手動重置:在系統中對於失敗操做的恢復時間是很難肯定的,提供一個手動重置功能可以使得管理員能夠手動的強制將熔斷器切換到閉合狀態。一樣的,若是受熔斷器保護的服務暫時不可用的話,管理員可以強制的將熔斷器設置爲斷開狀態。
  • 併發問題:相同的熔斷器有可能被大量併發請求同時訪問。熔斷器的實現不該該阻塞併發的請求或者增長每次請求調用的負擔。
  • 資源的差別性:使用單個熔斷器時,一個資源若是​​有分佈在多個地方就須要當心。好比,一個數據可能存儲在多個磁盤分區上(shard),某個分區能夠正常訪問,而另外一個可能存在暫時性的問題。在這種狀況下,不一樣的錯誤響應若是混爲一談,那麼應用程序訪問的這些存在問題的分區的失敗的可能性就會高,而那些被認爲是正常的分區,就有可能被阻塞。
  • 加快熔斷器的熔斷操做:有時候,服務返回的錯誤信息足夠讓熔斷器當即執行熔斷操做而且保持一段時間。好比,若是從一個分佈式資源返回的響應提示負載超重,那麼能夠判定出不建議當即重試,而是應該等待幾分鐘後再重試。(HTTP協議定義了」HTTP 503 Service Unavailable」來表示請求的服務當前不可用,他能夠包含其餘信息好比,超時等)
  • 重複失敗請求:當熔斷器在斷開狀態的時候,熔斷器能夠記錄每一次請求的細節,而不是僅僅返回失敗信息,這樣當遠程服務恢復的時候,能夠將這些失敗的請求再從新請求一次。

四 使用場景

應該使用該模式來:

  • 防止應用程序直接調用那些極可能會調用失敗的遠程服務或共享資源。

不適合的場景

  • 對於應用程序中的直接訪問本地私有資源,好比內存中的數據結構,若是使用熔斷器模式只會增長系統額外開銷。
  • 不適合做爲應用程序中業務邏輯的異常處理替代品

五 實現

根據上面的狀態切換圖,咱們很容易實現一個基本的熔斷器,只須要在內部維護一個狀態機,並定義好狀態轉移的規則,可使用State模式來實現。首先,咱們定義一個表示狀態轉移操做的抽象類CircuitBreakerState:

public abstract class CircuitBreakerState
{
    protected CircuitBreakerState(CircuitBreaker circuitBreaker)
    {
        this.circuitBreaker = circuitBreaker;
    }

    /// <summary>
    /// 調用受保護方法以前處理的操做
    /// </summary>
    public virtual void ProtectedCodeIsAboutToBeCalled() {
        //若是是斷開狀態,直接返回
        //而後坐等超時轉換到半斷開狀態
        if (circuitBreaker.IsOpen)
        {
            throw new OpenCircuitException();
        }
    }

    /// <summary>
    /// 受熔斷器保護的方法調用成功以後的操做
    /// </summary>
    public virtual void ProtectedCodeHasBeenCalled()
    {
        circuitBreaker.IncreaseSuccessCount();
    }

    /// <summary>
    ///受熔斷器保護的方法調用發生異常操做後的操做
    /// </summary>
    /// <param name="e"></param>
    public virtual void ActUponException(Exception e)
    {
        //增長失敗次數計數器,而且保存錯誤信息
        circuitBreaker.IncreaseFailureCount(e);
        //重置連續成功次數
        circuitBreaker.ResetConsecutiveSuccessCount();
    }

    protected readonly CircuitBreaker circuitBreaker;
}

抽象類中,狀態機CircuitBreaker經過構造函數注入;當發生錯誤時,咱們增長錯誤計數器,而且重置連續成功計數器,在增長錯誤計數器操做中,同時也記錄了出錯的異常信息。

而後在分別實現表示熔斷器三個狀態的類。首先實現閉合狀態CloseState:

public class ClosedState : CircuitBreakerState
{
    public ClosedState(CircuitBreaker circuitBreaker)
        : base(circuitBreaker)
    {
        //重置失敗計數器
        circuitBreaker.ResetFailureCount();
    }

    public override void ActUponException(Exception e)
    {
        base.ActUponException(e);
        //若是失敗次數達到閾值,則切換到斷開狀態
        if (circuitBreaker.FailureThresholdReached())
        {
            circuitBreaker.MoveToOpenState();
        }
    }
}

在閉合狀態下,若是發生錯誤,而且錯誤次數達到閾值,則狀態機切換到斷開狀態。斷開狀態OpenState的實現以下:

public class OpenState : CircuitBreakerState
{
    private readonly Timer timer;

    public OpenState(CircuitBreaker circuitBreaker)
        : base(circuitBreaker)
    {
        timer = new Timer(circuitBreaker.Timeout.TotalMilliseconds);
        timer.Elapsed += TimeoutHasBeenReached;
        timer.AutoReset = false;
        timer.Start();
    }

    //斷開超過設定的閾值,自動切換到半斷開狀態
    private void TimeoutHasBeenReached(object sender, ElapsedEventArgs e)
    {
        circuitBreaker.MoveToHalfOpenState();
    }

    public override void ProtectedCodeIsAboutToBeCalled()
    {
        base.ProtectedCodeIsAboutToBeCalled();
        throw new OpenCircuitException();
    }
}

斷開狀態內部維護一個計數器,若是斷開達到必定的時間,則自動切換到版斷開狀態,而且,在斷開狀態下,若是須要執行操做,則直接拋出異常。

最後半斷開Half-Open狀態實現以下:

public class HalfOpenState : CircuitBreakerState
{
    public HalfOpenState(CircuitBreaker circuitBreaker)
        : base(circuitBreaker)
    {
        //重置連續成功計數
        circuitBreaker.ResetConsecutiveSuccessCount();
    }

    public override void ActUponException(Exception e)
    {
        base.ActUponException(e);
        //只要有失敗,當即切換到斷開模式
        circuitBreaker.MoveToOpenState();
    }

    public override void ProtectedCodeHasBeenCalled()
    {
        base.ProtectedCodeHasBeenCalled();
        //若是連續成功次數達到閾值,切換到閉合狀態
        if (circuitBreaker.ConsecutiveSuccessThresholdReached())
        {
            circuitBreaker.MoveToClosedState();
        }
    }
}

切換到半斷開狀態時,將連續成功調用計數重置爲0,當執行成功的時候,自增改字段,當達到連讀調用成功次數的閾值時,切換到閉合狀態。若是調用失敗,當即切換到斷開模式。

有了以上三種狀態切換以後,咱們要實現CircuitBreaker類了:

public class CircuitBreaker
{
    private readonly object monitor = new object();
    private CircuitBreakerState state;
    public int FailureCount { get; private set; }
    public int ConsecutiveSuccessCount { get; private set; }
    public int FailureThreshold { get; private set; }
    public int ConsecutiveSuccessThreshold { get; private set; }
    public TimeSpan Timeout { get; private set; }
    public Exception LastException { get; private set; }

    public bool IsClosed
    {
        get { return state is ClosedState; }
    }

    public bool IsOpen
    {
        get { return state is OpenState; }
    }

    public bool IsHalfOpen
    {
        get { return state is HalfOpenState; }
    }

    internal void MoveToClosedState()
    {
        state = new ClosedState(this);
    }

    internal void MoveToOpenState()
    {
        state = new OpenState(this);
    }

    internal void MoveToHalfOpenState()
    {
        state = new HalfOpenState(this);
    }

    internal void IncreaseFailureCount(Exception ex)
    {
        LastException = ex;
        FailureCount++;
    }

    internal void ResetFailureCount()
    {
        FailureCount = 0;
    }

    internal bool FailureThresholdReached()
    {
        return FailureCount >= FailureThreshold;
    }

    internal void IncreaseSuccessCount()
    {
        ConsecutiveSuccessCount++;
    }

    internal void ResetConsecutiveSuccessCount()
    {
        ConsecutiveSuccessCount = 0;
    }

    internal bool ConsecutiveSuccessThresholdReached()
    {
        return ConsecutiveSuccessCount >= ConsecutiveSuccessThreshold;
    }

}

在該類中首先:

  • 定義了一些記錄狀態的變量,如FailureCount,ConsecutiveSuccessCount 記錄失敗次數,連續成功次數,以及FailureThreshold,ConsecutiveSuccessThreshold記錄最大調用失敗次數,連續調用成功次數。這些對象對外部來講是隻讀的。
  • 定義了一個 CircuitBreakerState類型的state變量,以表示當前系統的狀態。
  • 定義了一些列獲取當前狀態的方法IsOpen,IsClose,IsHalfOpen,以及表示狀態轉移的方法MoveToOpenState,MoveToClosedState等,這些方法比較簡單,根據名字便可看出用意。

而後,能夠經過構造函數將在Close狀態下最大失敗次數,HalfOpen狀態下使用的最大連續成功次數,以及Open狀態下的超時時間經過構造函數傳進來:

public CircuitBreaker(int failedthreshold, int consecutiveSuccessThreshold, TimeSpan timeout)
{
    if (failedthreshold < 1 || consecutiveSuccessThreshold < 1)
    {
        throw new ArgumentOutOfRangeException("threshold", "Threshold should be greater than 0");
    }

    if (timeout.TotalMilliseconds < 1)
    {
        throw new ArgumentOutOfRangeException("timeout", "Timeout should be greater than 0");
    }

    FailureThreshold = failedthreshold;
    ConsecutiveSuccessThreshold = consecutiveSuccessThreshold;
    Timeout = timeout;
    MoveToClosedState();
}

在初始狀態下,熔斷器切換到閉合狀態。

而後,能夠經過AttempCall調用,傳入指望執行的代理方法,該方法的執行受熔斷器保護。這裏使用了鎖來處理併發問題。

public void AttemptCall(Action protectedCode)
{
    using (TimedLock.Lock(monitor))
    {
        state.ProtectedCodeIsAboutToBeCalled();
    }

    try
    {
        protectedCode();
    }
    catch (Exception e)
    {
        using (TimedLock.Lock(monitor))
        {
            state.ActUponException(e);
        }
        throw;
    }

    using (TimedLock.Lock(monitor))
    {
        state.ProtectedCodeHasBeenCalled();
    }
}

最後,提供Close和Open兩個方法來手動切換當前狀態。

public void Close()
{
    using (TimedLock.Lock(monitor))
    {
        MoveToClosedState();
    }
}

public void Open()
{
    using (TimedLock.Lock(monitor))
    {
        MoveToOpenState();
    }
}

六 測試

以上的熔斷模式,咱們能夠對其創建單元測試。

首先咱們編寫幾個幫助類以模擬連續執行次數:

private static void CallXAmountOfTimes(Action codeToCall, int timesToCall)
{
    for (int i = 0; i < timesToCall; i++)
    {
        codeToCall();
    }
}

如下類用來拋出特定異常:

private static void AssertThatExceptionIsThrown<T>(Action code) where T : Exception
{
    try
    {
        code();
    }
    catch (T)
    {
        return;
    }

    Assert.Fail("Expected exception of type {0} was not thrown", typeof(T).FullName);
}

而後,使用NUnit,能夠創建以下Case:

[Test]
public void ClosesIfProtectedCodeSucceedsInHalfOpenState()
{
    var stub = new Stub(10);
    //定義熔斷器,失敗10次進入斷開狀態
    //5秒後進入半斷開狀態
    //在半斷開狀態下,連續成功15次,進入閉合狀態
    var circuitBreaker = new CircuitBreaker(10, 15, TimeSpan.FromMilliseconds(5000));
    Assert.That(circuitBreaker.IsClosed);
    //失敗10次調用
    CallXAmountOfTimes(() => AssertThatExceptionIsThrown<ApplicationException>(() => circuitBreaker.AttemptCall(stub.DoStuff)), 10);

    Assert.AreEqual(10, circuitBreaker.FailureCount);

    Assert.That(circuitBreaker.IsOpen);

    //等待從Open轉到HalfOpen
    Thread.Sleep(6000);
    Assert.That(circuitBreaker.IsHalfOpen);
    //成功調用15次
    CallXAmountOfTimes(()=>circuitBreaker.AttemptCall(stub.DoStuff), 15);

    Assert.AreEqual(15, circuitBreaker.ConsecutiveSuccessCount);
    Assert.AreEqual(0, circuitBreaker.FailureCount);
    Assert.That(circuitBreaker.IsClosed);
}

這個Case模擬了熔斷器中狀態的轉換。首先初始化時,熔斷器處於閉合狀態,而後連續10次調用拋出異常,這時熔斷器進去了斷開狀態,而後讓線程等待6秒,此時在第5秒的時候,狀態切換到了半斷開狀態。而後連續15次成功調用,此時狀態又切換到了閉合狀態。

七 結論

在應用系統中,咱們一般會去調用遠程的服務或者資源(這些服務或資源一般是來自第三方),對這些遠程服務或者資源的調用一般會致使失敗,或者掛起沒有響應,直到超時的產生。在一些極端狀況下,大量的請求會阻塞在對這些異常的遠程服務的調用上,會致使一些關鍵性的系統資源耗盡,從而致使級聯的失敗,從而拖垮整個系統。熔斷器模式在內部採用狀態機的形式,使得對這些可能會致使請求失敗的遠程服務進行了包裝,當遠程服務發生異常時,能夠當即對進來的請求返回錯誤響應,並告知系統管理員,將錯誤控制在局部範圍內,從而提升系統的穩定性和可靠性。

本文首先介紹了熔斷器模式使用的場景,可以解決的問題,以及須要考慮的因素,最後使用代碼展現瞭如何實現一個簡單的熔斷器,而且給出了測試用例,但願這些對您有幫助,尤爲是在當您的系統調用了外部的遠程服務或者資源,同時訪問量又很大的狀況下對提升系統的穩定性和可靠性有所幫助。

八 參考文獻

1. 互聯網巨頭爲何會「宕機」, http://edge.iteye.com/blog/1933145

2. 互聯網巨頭爲何會「宕機」(二), http://edge.iteye.com/blog/1936151

3. Circuit Breaker, http://martinfowler.com/bliki/CircuitBreaker.html

4. Circuit Breaker Pattern, http://msdn.microsoft.com/en-us/library/dn589784.aspx

相關文章
相關標籤/搜索