熔斷器設計模式

發表於2014-08-14

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

同樣,在大型的軟體系統中,如果呼叫的遠端服務或者資源由於某種原因無法使用時,如果沒有這種過載保護,就會導致請求的資源阻塞在伺服器上等待從而耗盡系統或者伺服器資源。很多時候剛開始可能只是系統出現了區域性的、小規模的故障,然而由於種種原因,故障影響的範圍越來越大,最終導致了全域性性的後果。軟體系統中的這種過載保護就是本文將要談到的熔斷器模式(Circuit Breaker)。

一 問題的產生

在大型的分散式系統中,通常需要呼叫或操作遠端的服務或者資源,這些遠端的服務或者資源由於呼叫者不可以控的原因比如網路連線緩慢,資源被佔用或者暫時不可用等原因,導致對這些遠端資源的呼叫失敗。這些錯誤通常在稍後的一段時間內可以恢復正常。

但是,在某些情況下,由於一些無法預知的原因導致結果很難預料,遠端的方法或者資源可能需要很長的一段時間才能修復。這種錯誤嚴重到系統的部分失去響應甚至導致整個服務的完全不可用。在這種情況下,採用不斷地重試可能解決不了問題,相反,應用程式在這個時候應該立即返回並且報告錯誤。

通常,如果一個伺服器非常繁忙,那麼系統中的部分失敗可能會導致 “連鎖失效”(cascading failure)。比如,某個操作可能會呼叫一個遠端的WebService,這個service會設定一個超時的時間,如果響應時間超過了該時間就會丟擲一個異常。但是這種策略會導致併發的請求呼叫同樣的操作會阻塞,一直等到超時時間的到期。這種對請求的阻塞可能會佔用寶貴的系統資源,如記憶體,執行緒,資料庫連線等等,最後這些資源就會消耗殆盡,使得其他系統不相關的部分所使用的資源也耗盡從而拖累整個系統。在這種情況下,操作立即返回錯誤而不是等待超時的發生可能是一種更好的選擇。只有當呼叫服務有可能成功時我們再去嘗試。

二 解決方法

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

熔斷器模式就像是那些容易導致錯誤的操作的一種代理。這種代理能夠記錄最近呼叫發生錯誤的次數,然後決定使用允許操作繼續,或者立即返回錯誤。

122149014989815

 

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

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

各個狀態之間的轉換如下圖:

122149027489531

 

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

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

可以對熔斷器模式進行定製以適應一些可能會導致遠端服務失敗的特定場景。比如,可以在熔斷器中對超時時間使用不斷增長的策略。在熔斷器開始進入斷開狀態的時候,可以設定超時時間為幾秒鐘,然後如果錯誤沒有被解決,然後將該超時時間設定為幾分鐘,依次類推。在一些情況下,在斷開狀態下我們可以返回一些錯誤的預設值,而不是丟擲異常。

三 要考慮的因素

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

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

四 使用場景

應該使用該模式來:

  • 防止應用程式直接呼叫那些很可能會呼叫失敗的遠端服務或共享資源。

不適合的場景

  • 對於應用程式中的直接訪問本地私有資源,比如記憶體中的資料結構,如果使用熔斷器模式只會增加系統額外開銷。
  • 不適合作為應用程式中業務邏輯的異常處理替代品

五 實現

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

斷開狀態內部維護一個計數器,如果斷開達到一定的時間,則自動切換到版斷開狀態,並且,在斷開狀態下,如果需要執行操作,則直接丟擲異常。

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

切換到半斷開狀態時,將連續成功呼叫計數重置為0,當執行成功的時候,自增改欄位,當達到連讀呼叫成功次數的閾值時,切換到閉合狀態。如果呼叫失敗,立即切換到斷開模式。

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

在該類中首先:

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

然後,可以通過建構函式將在Close狀態下最大失敗次數,HalfOpen狀態下使用的最大連續成功次數,以及Open狀態下的超時時間通過建構函式傳進來:

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

然後,可以通過AttempCall呼叫,傳入期望執行的代理方法,該方法的執行受熔斷器保護。這裡使用了鎖來處理併發問題。

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

六 測試

以上的熔斷模式,我們可以對其建立單元測試。

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

以下類用來丟擲特定異常:

然後,使用NUnit,可以建立如下Case:

這個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

 

相關文章