新聞中心
如果大家有印象的話,尤其是夏天,如果家里用電負載過大,比如開了很多家用電器,就會”自動跳閘”,此時電路就會斷開。在以前更古老的一種方式是”保險絲”,當負載過大,或者電路發(fā)生故障或異常時,電流會不斷升高,為防止升高的電流有可能損壞電路中的某些重要器件或貴重器件,燒毀電路甚至造成火災。保險絲會在電流異常升高到一定的高度和熱度的時候,自身熔斷切斷電流,從而起到保護電路安全運行的作用。

同樣,在大型的軟件系統(tǒng)中,如果調(diào)用的遠程服務或者資源由于某種原因無法使用時,如果沒有這種過載保護,就會導致請求的資源阻塞在服務器上等待從而耗盡系統(tǒng)或者服務器資源。很多時候剛開始可能只是系統(tǒng)出現(xiàn)了局部的、小規(guī)模的故障,然而由于種種原因,故障影響的范圍越來越大,最終導致了全局性的后果。軟件系統(tǒng)中的這種過載保護就是本文將要談到的熔斷器模式(Circuit Breaker)
一 問題的產(chǎn)生
在大型的分布式系統(tǒng)中,通常需要調(diào)用或操作遠程的服務或者資源,這些遠程的服務或者資源由于調(diào)用者不可以控的原因比如網(wǎng)絡連接緩慢,資源被占用或者暫時不可用等原因,導致對這些遠程資源的調(diào)用失敗。這些錯誤通常在稍后的一段時間內(nèi)可以恢復正常。
但是,在某些情況下,由于一些無法預知的原因?qū)е陆Y(jié)果很難預料,遠程的方法或者資源可能需要很長的一段時間才能修復。這種錯誤嚴重到系統(tǒng)的部分失去響應甚至導致整個服務的完全不可用。在這種情況下,采用不斷地重試可能解決不了問題,相反,應用程序在這個時候應該立即返回并且報告錯誤。
通常,如果一個服務器非常繁忙,那么系統(tǒng)中的部分失敗可能會導致 “連鎖失效”(cascading failure)。比如,某個操作可能會調(diào)用一個遠程的WebService,這個service會設置一個超時的時間,如果響應時間超過了該時間就會拋出一個異常。但是這種策略會導致并發(fā)的請求調(diào)用同樣的操作會阻塞,一直等到超時時間的到期。這種對請求的阻塞可能會占用寶貴的系統(tǒng)資源,如內(nèi)存,線程,數(shù)據(jù)庫連接等等,***這些資源就會消耗殆盡,使得其他系統(tǒng)不相關(guān)的部分所使用的資源也耗盡從而拖累整個系統(tǒng)。在這種情況下,操作立即返回錯誤而不是等待超時的發(fā)生可能是一種更好的選擇。只有當調(diào)用服務有可能成功時我們再去嘗試。
二 解決方法
熔斷器模式可以防止應用程序不斷地嘗試執(zhí)行可能會失敗的操作,使得應用程序繼續(xù)執(zhí)行而不用等待修正錯誤,或者浪費CPU時間去等到長時間的超時產(chǎn)生。熔斷器模式也可以使應用程序能夠診斷錯誤是否已經(jīng)修正,如果已經(jīng)修正,應用程序會再次嘗試調(diào)用操作。
熔斷器模式就像是那些容易導致錯誤的操作的一種代理。這種代理能夠記錄最近調(diào)用發(fā)生錯誤的次數(shù),然后決定使用允許操作繼續(xù),或者立即返回錯誤。
熔斷器可以使用狀態(tài)機來實現(xiàn),內(nèi)部模擬以下幾種狀態(tài)。
- 閉合(closed)狀態(tài): 對應用程序的請求能夠直接引起方法的調(diào)用。代理類維護了最近調(diào)用失敗的次數(shù),如果某次調(diào)用失敗,則使失敗次數(shù)加1。如果最近失敗次數(shù)超過了在給定時間內(nèi)允許失敗的閾值,則代理類切換到斷開(Open)狀態(tài)。此時代理開啟了一個超時時鐘,當該時鐘超過了該時間,則切換到半斷開(Half-Open)狀態(tài)。該超時時間的設定是給了系統(tǒng)一次機會來修正導致調(diào)用失敗的錯誤。
- 斷開(Open)狀態(tài):在該狀態(tài)下,對應用程序的請求會立即返回錯誤響應。
- 半斷開(Half-Open)狀態(tài):允許對應用程序的一定數(shù)量的請求可以去調(diào)用服務。如果這些請求對服務的調(diào)用成功,那么可以認為之前導致調(diào)用失敗的錯誤已經(jīng)修正,此時熔斷器切換到閉合狀態(tài)(并且將錯誤計數(shù)器重置);如果這一定數(shù)量的請求有調(diào)用失敗的情況,則認為導致之前調(diào)用失敗的問題仍然存在,熔斷器切回到斷開方式,然后開始重置計時器來給系統(tǒng)一定的時間來修正錯誤。半斷開狀態(tài)能夠有效防止正在恢復中的服務被突然而來的大量請求再次拖垮。
各個狀態(tài)之間的轉(zhuǎn)換如下圖:
在Close狀態(tài)下,錯誤計數(shù)器是基于時間的。在特定的時間間隔內(nèi)會自動重置。這能夠防止由于某次的偶然錯誤導致熔斷器進入斷開狀態(tài)。觸發(fā)熔斷器進入斷開狀態(tài)的失敗閾值只有在特定的時間間隔內(nèi),錯誤次數(shù)達到指定錯誤次數(shù)的閾值才會產(chǎn)生。在Half-Open狀態(tài)中使用的連續(xù)成功次數(shù)計數(shù)器記錄調(diào)用的成功次數(shù)。當連續(xù)調(diào)用成功次數(shù)達到某個指定值時,切換到閉合狀態(tài),如果某次調(diào)用失敗,立即切換到斷開狀態(tài),連續(xù)成功調(diào)用次數(shù)計時器在下次進入半斷開狀態(tài)時歸零。
實現(xiàn)熔斷器模式使得系統(tǒng)更加穩(wěn)定和有彈性,在系統(tǒng)從錯誤中恢復的時候提供穩(wěn)定性,并且減少了錯誤對系統(tǒng)性能的影像。它通過快速的拒絕那些試圖有可能調(diào)用會導致錯誤的服務,而不會去等待操作超時或者永遠不會不返回結(jié)果來提高系統(tǒng)的響應事件。如果熔斷器設計模式在每次狀態(tài)切換的時候會發(fā)出一個事件,這種信息可以用來監(jiān)控服務的運行狀態(tài),能夠通知管理員在熔斷器切換到斷開狀態(tài)時進行處理。
可以對熔斷器模式進行定制以適應一些可能會導致遠程服務失敗的特定場景。比如,可以在熔斷器中對超時時間使用不斷增長的策略。在熔斷器開始進入斷開狀態(tài)的時候,可以設置超時時間為幾秒鐘,然后如果錯誤沒有被解決,然后將該超時時間設置為幾分鐘,依次類推。在一些情況下,在斷開狀態(tài)下我們可以返回一些錯誤的默認值,而不是拋出異常。
#p#
三 要考慮的因素
在實現(xiàn)熔斷器模式的時候,以下這些因素需可能需要考慮:
- 異常處理:調(diào)用受熔斷器保護的服務的時候,我們必須要處理當服務不可用時的異常情況。這些異常處理通常需要視具體的業(yè)務情況而定。比如,如果應用程序只是暫時的功能降級,可能需要切換到其它的可替換的服務上來執(zhí)行相同的任務或者獲取相同的數(shù)據(jù),或者給用戶報告錯誤然后提示他們稍后重試。
- 異常的類型:請求失敗的原因可能有很多種。一些原因可能會比其它原因更嚴重。比如,請求會失敗可能是由于遠程的服務崩潰,這可能需要花費數(shù)分鐘來恢復;也可能是由于服務器暫時負載過重導致超時。熔斷器應該能夠檢查錯誤的類型,從而根據(jù)具體的錯誤情況來調(diào)整策略。比如,可能需要很多次超時異常才可以斷定需要切換到斷開狀態(tài),而只需要幾次錯誤提示就可以判斷服務不可用而快速切換到斷開狀態(tài)。
- 日志:熔斷器應該能夠記錄所有失敗的請求,以及一些可能會嘗試成功的請求,使得的管理員能夠監(jiān)控使用熔斷器保護的服務的執(zhí)行情況。
- 測試服務是否可用:在斷開狀態(tài)下,熔斷器可以采用定期的ping遠程的服務或者資源,來判斷是否服務是否恢復,而不是使用計時器來自動切換到半斷開狀態(tài)。這種ping操作可以模擬之前那些失敗的請求,或者可以使用通過調(diào)用遠程服務提供的檢查服務是否可用的方法來判斷。
- 手動重置:在系統(tǒng)中對于失敗操作的恢復時間是很難確定的,提供一個手動重置功能能夠使得管理員可以手動的強制將熔斷器切換到閉合狀態(tài)。同樣的,如果受熔斷器保護的服務暫時不可用的話,管理員能夠強制的將熔斷器設置為斷開狀態(tài)。
- 并發(fā)問題:相同的熔斷器有可能被大量并發(fā)請求同時訪問。熔斷器的實現(xiàn)不應該阻塞并發(fā)的請求或者增加每次請求調(diào)用的負擔。
- 資源的差異性:使用單個熔斷器時,一個資源如果??有分布在多個地方就需要小心。比如,一個數(shù)據(jù)可能存儲在多個磁盤分區(qū)上(shard),某個分區(qū)可以正常訪問,而另一個可能存在暫時性的問題。在這種情況下,不同的錯誤響應如果混為一談,那么應用程序訪問的這些存在問題的分區(qū)的失敗的可能性就會高,而那些被認為是正常的分區(qū),就有可能被阻塞。
- 加快熔斷器的熔斷操作:有時候,服務返回的錯誤信息足夠讓熔斷器立即執(zhí)行熔斷操作并且保持一段時間。比如,如果從一個分布式資源返回的響應提示負載超重,那么可以斷定出不建議立即重試,而是應該等待幾分鐘后再重試。(HTTP協(xié)議定義了”HTTP 503 Service Unavailable”來表示請求的服務當前不可用,他可以包含其他信息比如,超時等)
- 重復失敗請求:當熔斷器在斷開狀態(tài)的時候,熔斷器可以記錄每一次請求的細節(jié),而不是僅僅返回失敗信息,這樣當遠程服務恢復的時候,可以將這些失敗的請求再重新請求一次。
四 使用場景
應該使用該模式來:
- 防止應用程序直接調(diào)用那些很可能會調(diào)用失敗的遠程服務或共享資源。
不適合的場景
- 對于應用程序中的直接訪問本地私有資源,比如內(nèi)存中的數(shù)據(jù)結(jié)構(gòu),如果使用熔斷器模式只會增加系統(tǒng)額外開銷。
- 不適合作為應用程序中業(yè)務邏輯的異常處理替代品
五 實現(xiàn)
根據(jù)上面的狀態(tài)切換圖,我們很容易實現(xiàn)一個基本的熔斷器,只需要在內(nèi)部維護一個狀態(tài)機,并定義好狀態(tài)轉(zhuǎn)移的規(guī)則,可以使用State模式來實現(xiàn)。首先,我們定義一個表示狀態(tài)轉(zhuǎn)移操作的抽象類CircuitBreakerState:
- public abstract class CircuitBreakerState
- {
- protected CircuitBreakerState(CircuitBreaker circuitBreaker)
- {
- this.circuitBreaker = circuitBreaker;
- }
- ///
- /// 調(diào)用受保護方法之前處理的操作
- ///
- public virtual void ProtectedCodeIsAboutToBeCalled() {
- //如果是斷開狀態(tài),直接返回
- //然后坐等超時轉(zhuǎn)換到半斷開狀態(tài)
- if (circuitBreaker.IsOpen)
- {
- throw new OpenCircuitException();
- }
- }
- ///
- /// 受熔斷器保護的方法調(diào)用成功之后的操作
- ///
- public virtual void ProtectedCodeHasBeenCalled()
- {
- circuitBreaker.IncreaseSuccessCount();
- }
- ///
- ///受熔斷器保護的方法調(diào)用發(fā)生異常操作后的操作
- ///
- ///
- public virtual void ActUponException(Exception e)
- {
- //增加失敗次數(shù)計數(shù)器,并且保存錯誤信息
- circuitBreaker.IncreaseFailureCount(e);
- //重置連續(xù)成功次數(shù)
- circuitBreaker.ResetConsecutiveSuccessCount();
- }
- protected readonly CircuitBreaker circuitBreaker;
- }
抽象類中,狀態(tài)機CircuitBreaker通過構(gòu)造函數(shù)注入;當發(fā)生錯誤時,我們增加錯誤計數(shù)器,并且重置連續(xù)成功計數(shù)器,在增加錯誤計數(shù)器操作中,同時也記錄了出錯的異常信息。
然后在分別實現(xiàn)表示熔斷器三個狀態(tài)的類。首先實現(xiàn)閉合狀態(tài)CloseState:
- public class ClosedState : CircuitBreakerState
- {
- public ClosedState(CircuitBreaker circuitBreaker)
- : base(circuitBreaker)
- {
- //重置失敗計數(shù)器
- circuitBreaker.ResetFailureCount();
- }
- public override void ActUponException(Exception e)
- {
- base.ActUponException(e);
- //如果失敗次數(shù)達到閾值,則切換到斷開狀態(tài)
- if (circuitBreaker.FailureThresholdReached())
- {
- circuitBreaker.MoveToOpenState();
- }
- }
- }
在閉合狀態(tài)下,如果發(fā)生錯誤,并且錯誤次數(shù)達到閾值,則狀態(tài)機切換到斷開狀態(tài)。斷開狀態(tài)OpenState的實現(xiàn)如下:
- 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();
- }
- //斷開超過設定的閾值,自動切換到半斷開狀態(tài)
- private void TimeoutHasBeenReached(object sender, ElapsedEventArgs e)
- {
- circuitBreaker.MoveToHalfOpenState();
- }
- public override void ProtectedCodeIsAboutToBeCalled()
- {
- base.ProtectedCodeIsAboutToBeCalled();
- throw new OpenCircuitException();
- }
- }
斷開狀態(tài)內(nèi)部維護一個計數(shù)器,如果斷開達到一定的時間,則自動切換到版斷開狀態(tài),并且,在斷開狀態(tài)下,如果需要執(zhí)行操作,則直接拋出異常。
#p#
***半斷開Half-Open狀態(tài)實現(xiàn)如下:
- public class HalfOpenState : CircuitBreakerState
- {
- public HalfOpenState(CircuitBreaker circuitBreaker)
- : base(circuitBreaker)
- {
- //重置連續(xù)成功計數(shù)
- circuitBreaker.ResetConsecutiveSuccessCount();
- }
- public override void ActUponException(Exception e)
- {
- base.ActUponException(e);
- //只要有失敗,立即切換到斷開模式
- circuitBreaker.MoveToOpenState();
- }
- public override void ProtectedCodeHasBeenCalled()
- {
- base.ProtectedCodeHasBeenCalled();
- //如果連續(xù)成功次數(shù)達到閾值,切換到閉合狀態(tài)
- if (circuitBreaker.ConsecutiveSuccessThresholdReached())
- {
- circuitBreaker.MoveToClosedState();
- }
- }
- }
切換到半斷開狀態(tài)時,將連續(xù)成功調(diào)用計數(shù)重置為0,當執(zhí)行成功的時候,自增改字段,當達到連讀調(diào)用成功次數(shù)的閾值時,切換到閉合狀態(tài)。如果調(diào)用失敗,立即切換到斷開模式。
有了以上三種狀態(tài)切換之后,我們要實現(xiàn)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;
- }
- }
在該類中首先:
- 定義了一些記錄狀態(tài)的變量,如FailureCount,ConsecutiveSuccessCount 記錄失敗次數(shù),連續(xù)成功次數(shù),以及FailureThreshold,ConsecutiveSuccessThreshold記錄***調(diào)用失敗次數(shù),連續(xù)調(diào)用成功次數(shù)。這些對象對外部來說是只讀的。
- 定義了一個 CircuitBreakerState類型的state變量,以表示當前系統(tǒng)的狀態(tài)。
- 定義了一些列獲取當前狀態(tài)的方法IsOpen,IsClose,IsHalfOpen,以及表示狀態(tài)轉(zhuǎn)移的方法MoveToOpenState,MoveToClosedState等,這些方法比較簡單,根據(jù)名字即可看出用意。
然后,可以通過構(gòu)造函數(shù)將在Close狀態(tài)下***失敗次數(shù),HalfOpen狀態(tài)下使用的***連續(xù)成功次數(shù),以及Open狀態(tài)下的超時時間通過構(gòu)造函數(shù)傳進來:
- 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();
- }
在初始狀態(tài)下,熔斷器切換到閉合狀態(tài)。
然后,可以通過AttempCall調(diào)用,傳入期望執(zhí)行的代理方法,該方法的執(zhí)行受熔斷器保護。這里使用了鎖來處理并發(fā)問題。
- 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兩個方法來手動切換當前狀態(tài)。
- public void Close()
- {
- using (TimedLock.Lock(monitor))
- {
- MoveToClosedState();
- }
- }
- public void Open()
- {
- using (TimedLock.Lock(monitor))
- {
- MoveToOpenState();
- }
- }
#p#
六 測試
以上的熔斷模式,我們可以對其建立單元測試。
首先我們編寫幾個幫助類以模擬連續(xù)執(zhí)行次數(shù):
- private static void CallXAmountOfTimes(Action codeToCall, int timesToCall)
- {
- for (int i = 0; i < timesToCall; i++)
- {
- codeToCall();
- }
- }
以下類用來拋出特定異常:
- private static void AssertThatExceptionIsThrown
(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次進入斷開狀態(tài)
- //5秒后進入半斷開狀態(tài)
- //在半斷開狀態(tài)下,連續(xù)成功15次,進入閉合狀態(tài)
- var circuitBreaker = new CircuitBreaker(10, 15, TimeSpan.FromMilliseconds(5000));
- Assert.That(circuitBreaker.IsClosed);
- //失敗10次調(diào)用
- CallXAmountOfTimes(() => AssertThatExceptionIsThrown
(() => circuitBreaker.AttemptCall(stub.DoStuff)), 10); - Assert.AreEqual(10, circuitBreaker.FailureCount);
- Assert.That(circuitBreaker.IsOpen);
- //等待從Open轉(zhuǎn)到HalfOpen
- Thread.Sleep(6000);
- Assert.That(circuitBreaker.IsHalfOpen);
- //成功調(diào)用15次
- CallXAmountOfTimes(()=>circuitBreaker.AttemptCall(stub.DoStuff), 15);
- Assert.AreEqual(15, circuitBreaker.ConsecutiveSuccessCount);
- Assert.AreEqual(0, circuitBreaker.FailureCount);
- Assert.That(circuitBreaker.IsClosed);
- }
這個Case模擬了熔斷器中狀態(tài)的轉(zhuǎn)換。首先初始化時,熔斷器處于閉合狀態(tài),然后連續(xù)10次調(diào)用拋出異常,這時熔斷器進去了斷開狀態(tài),然后讓線程等待6秒,此時在第5秒的時候,狀態(tài)切換到了半斷開狀態(tài)。然后連續(xù)15次成功調(diào)用,此時狀態(tài)又切換到了閉合狀態(tài)。
七 結(jié)論
在應用系統(tǒng)中,我們通常會去調(diào)用遠程的服務或者資源(這些服務或資源通常是來自第三方),對這些遠程服務或者資源的調(diào)用通常會導致失敗,或者掛起沒有響應,直到超時的產(chǎn)生。在一些極端情況下,大量的請求會阻塞在對這些異常的遠程服務的調(diào)用上,會導致一些關(guān)鍵性的系統(tǒng)資源耗盡,從而導致級聯(lián)的失敗,從而拖垮整個系統(tǒng)。熔斷器模式在內(nèi)部采用狀態(tài)機的形式,使得對這些可能會導致請求失敗的遠程服務進行了包裝,當遠程服務發(fā)生異常時,可以立即對進來的請求返回錯誤響應,并告知系統(tǒng)管理員,將錯誤控制在局部范圍內(nèi),從而提高系統(tǒng)的穩(wěn)定性和可靠性。
本文首先介紹了熔斷器模式使用的場景,能夠解決的問題,以及需要考慮的因素,***使用代碼展示了如何實現(xiàn)一個簡單的熔斷器,并且給出了測試用例,希望這些對您有幫助,尤其是在當您的系統(tǒng)調(diào)用了外部的遠程服務或者資源,同時訪問量又很大的情況下對提高系統(tǒng)的穩(wěn)定性和可靠性有所幫助。
八 參考文獻
1. 互聯(lián)網(wǎng)巨頭為什么會“宕機”, http://edge.iteye.com/blog/1933145
2. 互聯(lián)網(wǎng)巨頭為什么會“宕機”(二), 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
網(wǎng)頁題目:軟件系統(tǒng)過載保護:熔斷器設計模式
鏈接地址:http://www.dlmjj.cn/article/dpjgghd.html


咨詢
建站咨詢
