若是你們有印象的話,尤爲是夏天,若是家裏用電負載過大,好比開了不少家用電器,就會」自動跳閘」,此時電路就會斷開。在之前更古老的一種方式是」保險絲」,當負載過大,或者電路發生故障或異常時,電流會不斷升高,爲防止升高的電流有可能損壞電路中的某些重要器件或貴重器件,燒燬電路甚至形成火災。保險絲會在電流異常升高到必定的高度和熱度的時候,自身熔斷切斷電流,從而起到保護電路安全運行的做用。html
一樣,在大型的軟件系統中,若是調用的遠程服務或者資源因爲某種緣由沒法使用時,若是沒有這種過載保護,就會致使請求的資源阻塞在服務器上等待從而耗盡系統或者服務器資源。不少時候剛開始可能只是系統出現了局部的、小規模的故障,然而因爲種種緣由,故障影響的範圍愈來愈大,最終致使了全局性的後果。軟件系統中的這種過載保護就是本文將要談到的熔斷器模式(Circuit Breaker)數據庫
在大型的分佈式系統中,一般須要調用或操做遠程的服務或者資源,這些遠程的服務或者資源因爲調用者不能夠控的緣由好比網絡鏈接緩慢,資源被佔用或者暫時不可用等緣由,致使對這些遠程資源的調用失敗。這些錯誤一般在稍後的一段時間內能夠恢復正常。設計模式
可是,在某些狀況下,因爲一些沒法預知的緣由致使結果很難預料,遠程的方法或者資源可能須要很長的一段時間才能修復。這種錯誤嚴重到系統的部分失去響應甚至致使整個服務的徹底不可用。在這種狀況下,採用不斷地重試可能解決不了問題,相反,應用程序在這個時候應該當即返回而且報告錯誤。安全
一般,若是一個服務器很是繁忙,那麼系統中的部分失敗可能會致使 「連鎖失效」(cascading failure)。好比,某個操做可能會調用一個遠程的WebService,這個service會設置一個超時的時間,若是響應時間超過了該時間就會拋出一個異常。可是這種策略會致使併發的請求調用一樣的操做會阻塞,一直等到超時時間的到期。這種對請求的阻塞可能會佔用寶貴的系統資源,如內存,線程,數據庫鏈接等等,最後這些資源就會消耗殆盡,使得其餘系統不相關的部分所使用的資源也耗盡從而拖累整個系統。在這種狀況下,操做當即返回錯誤而不是等待超時的發生多是一種更好的選擇。只有當調用服務有可能成功時咱們再去嘗試。服務器
熔斷器模式能夠防止應用程序不斷地嘗試執行可能會失敗的操做,使得應用程序繼續執行而不用等待修正錯誤,或者浪費CPU時間去等到長時間的超時產生。熔斷器模式也可使應用程序可以診斷錯誤是否已經修正,若是已經修正,應用程序會再次嘗試調用操做。網絡
熔斷器模式就像是那些容易致使錯誤的操做的一種代理。這種代理可以記錄最近調用發生錯誤的次數,而後決定使用容許操做繼續,或者當即返回錯誤。數據結構
熔斷器可使用狀態機來實現,內部模擬如下幾種狀態。併發
各個狀態之間的轉換以下圖:分佈式
在Close狀態下,錯誤計數器是基於時間的。在特定的時間間隔內會自動重置。這可以防止因爲某次的偶然錯誤致使熔斷器進入斷開狀態。觸發熔斷器進入斷開狀態的失敗閾值只有在特定的時間間隔內,錯誤次數達到指定錯誤次數的閾值纔會產生。在Half-Open狀態中使用的連續成功次數計數器記錄調用的成功次數。當連續調用成功次數達到某個指定值時,切換到閉合狀態,若是某次調用失敗,當即切換到斷開狀態,連續成功調用次數計時器在下次進入半斷開狀態時歸零。ide
實現熔斷器模式使得系統更加穩定和有彈性,在系統從錯誤中恢復的時候提供穩定性,而且減小了錯誤對系統性能的影響。它經過快速的拒絕那些試圖有可能調用會致使錯誤的服務,而不會去等待操做超時或者永遠不會不返回結果來提升系統的響應事件。若是熔斷器設計模式在每次狀態切換的時候會發出一個事件,這種信息能夠用來監控服務的運行狀態,可以通知管理員在熔斷器切換到斷開狀態時進行處理。
能夠對熔斷器模式進行定製以適應一些可能會致使遠程服務失敗的特定場景。好比,能夠在熔斷器中對超時時間使用不斷增加的策略。在熔斷器開始進入斷開狀態的時候,能夠設置超時時間爲幾秒鐘,而後若是錯誤沒有被解決,而後將該超時時間設置爲幾分鐘,依次類推。在一些狀況下,在斷開狀態下咱們能夠返回一些錯誤的默認值,而不是拋出異常。
在實現熔斷器模式的時候,如下這些因素需可能須要考慮:
應該使用該模式來:
不適合的場景
根據上面的狀態切換圖,咱們很容易實現一個基本的熔斷器,只須要在內部維護一個狀態機,並定義好狀態轉移的規則,可使用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; } }
在該類中首先:
而後,能夠經過構造函數將在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