采用斷路器設計模式來保護軟件
程序員的人生就像在一個快車道上行駛。幾周甚至幾小時完成某些特性編碼,打包測試沒有問題,蓋上QA認證,代碼部署到生產環境。然而最壞的事情發生了,你所部署的軟件在運行中掛掉了。用墨菲法則來說,就是“會出錯的,終將出錯”。但是,如果我們在寫代碼時就能考慮到這些問題會怎樣?
那么我們該如何應對,將不好的事情轉變為好的事情呢?
電子技術拯救了我們
至今記得我和哥哥因為電涌不得不更換家里的保險絲情景,那時我對事件的嚴重程度一無所知,而他卻已經是電力方面的小能手了。保險絲完全燒壞了,但它卻保護了我家的電視機。在電子工程領域,保險絲和斷路器用(Circuit Breaker)來處理這樣的問題,即超大功率可能帶來一些嚴重的破壞,譬如毀壞電子設備甚至燒掉整個屋子。保險絲包含一個小電線絲,電量過大時就會融化,就像燒掉的電燈泡,阻止危險的電流,保護了電器和房屋。
保險絲演變成斷路器,通常利用電磁鐵就可以斷開電路,而不用燒掉它,這樣斷路器就可以重置反復地用。不過,它們的功能都是一樣的,檢測負載,接著迅速停止工作,保全其它部分不受破壞。
回過頭再想,這是一個多么神奇的概念。僅僅壞掉某個控件——保險絲徹底壞掉,就可以避免了整個系統的嚴重損壞。多虧電涌后保險絲自熔,保住了電視機。那么我們可不可在軟件中做同樣的事情?壞事發生后,軟件中的某個控件會迅速停止工作。模仿現實生活中的場景,由此我們創造了斷路器設計模式。
在分布式系統中,某些故障是短暫的,通過快速連續重試就可以解決問題;但在某些場景中,關鍵依賴的連接丟失了,短時間無法恢復。比如,某個應用失去了與云中的持續化存儲連接。在這樣的場景中,關閉服務就可以避免錯誤的數據處理過程、甚至數據丟失或者級聯故障,進而防止對系統其它部分的進一步損壞。
借助于迅速停止工作(failing fast),運維系統就可以容易地進行監控和響應。在它們重視起來之前,那些徒勞嘗試重新連接的服務看起來仍然是正常的,因為本應該拉響的警報沒有響起。倘若某個服務在恰當的時候徹底失效,警告燈熄滅了,運維人員就會知曉問題所在,并及時做出響應。
斷路器設計模式
在系統中可重用基礎架構實現斷路器設計模式是很容易實現的,它是這么發揮作用的:
1 定義一個可重用的CircuitBreaker類,包含Trip和Reset方法,以及斷路器跳閘就可以調用的action
2 利用CircuitBreaker去監控系統依賴。針對每個單一的故障,斷路器跳閘就會將其設置在一種布防狀態,就像電涌出現時那樣。
3 倘若接下來在特定的時間窗口內嘗試成功,那么就重置此斷路器,一切恢復正常。
4 倘若斷路器沒有在特定的時間重置,異常會持續發生,此時斷路器就會調用你提供的action。你可以在斷路器跳閘時選擇快速停止工作(終止進程)或者其他action。
應用案例
本例中ExternalServiceAdapter類幫助系統與外部依賴建立連接。或許有個網絡程序產生請求頻繁地執行DoStuff操作。一旦執行,若此時GetConnection執行出錯,異常就會發生,斷路器就會被跳閘。倘若連接重新建立起來,斷路器就會被重置。不過連接異常持續發生時,斷路器就會跳閘,特定的跳閘action就會執行,在本例中將會迅速停止工作。
public class ExternalServiceAdapter { private CircuitBreaker circuitBreaker;public ExternalServiceAdapter() { circuitBreaker = new CircuitBreaker("CheckConnection", /*斷路器名稱 */ exception => /* 一旦斷路器跳閘此action就會被調用 */ { Console.WriteLine("Circuit breaker tripped! Fail fast!"); //終止進程,略過接下來的任何try/finally塊或者finalizers Environment.FailFast(exception.Message); }, 3, /* 斷路器跳閘前的最大閾值*/ TimeSpan.FromSeconds(2)); /* Time to wait between each try before attempting to trip the circuit breaker */ } public void DoStuff() { var externalService = GetConnection(); externalService.DoStuff(); } ConnectionDependency GetConnection() { try { var newConnection = new ConnectionDependency(); circuitBreaker.Reset(); return newConnection; } catch (Exception exception) { circuitBreaker.Trip(exception); throw; } }
}</pre>
斷路器模式簡單實現
using System; using System.Threading;public class CircuitBreaker { public CircuitBreaker(string name, /操作名稱/ Action<Exception> tripAction, / 一旦斷路器跳閘action就會被調用/ int maxTimesToRetry, / 斷路器跳閘前重試的時間/ TimeSpan delayBetweenRetries /每一次重試的時間間隔/) { this.name = name; this.tripAction = tripAction; this.maxTimesToRetry = maxTimesToRetry; this.delayBetweenRetries = delayBetweenRetries;
// 一旦用戶迫使斷路器跳閘,計時器就會開啟 timer = new Timer(CircuitBreakerTripped, null, Timeout.Infinite, (int)delayBetweenRetries.TotalMilliseconds); } public void Reset() { var oldValue = Interlocked.Exchange(ref failureCount, 0); timer.Change(Timeout.Infinite, Timeout.Infinite); Console.WriteLine("The circuit breaker for {0} is now disarmed", name); } public void Trip(Exception ex) { lastException = ex; var newValue = Interlocked.Increment(ref failureCount); if (newValue == 1) { // 開啟重試計時器. timer.Change(delayBetweenRetries, TimeSpan.FromMilliseconds(-1)); // 記錄已觸發的斷路器. Console.WriteLine("The circuit breaker for {0} is now in the armed state", name); } } void CircuitBreakerTripped(object state) { Console.WriteLine("Check to see if we need to trip the circuit breaker. Retry:{0}", failureCount); if (Interlocked.Increment(ref failureCount) > maxTimesToRetry) { Console.WriteLine("The circuit breaker for {0} is now tripped. Calling specified action", name); tripAction(lastException); return; } timer.Change(delayBetweenRetries, TimeSpan.FromMilliseconds(-1)); } readonly string name; readonly int maxTimesToRetry; long failureCount; readonly Action<Exception> tripAction; Exception lastException; readonly TimeSpan delayBetweenRetries; readonly Timer timer;
}</pre>
斷路器單元測試
[TestFixture] public class CircuitBreakerTests { [Test] public void When_the_circuit_breaker_is_tripped_the_trip_action_is_called_after_reaching_max_threshold() { bool circuitBreakerTripActionCalled = false; var connectionException = new Exception("Something bad happened.");var circuitBreaker = new CircuitBreaker("CheckServiceConnection", exception => { Console.WriteLine("Circuit breaker tripped - fail fast"); circuitBreakerTripActionCalled = true; // You would normally fail fast here in the action to faciliate the process shutdown by calling: // Environment.FailFast(connectionException.Message); }, 3, TimeSpan.FromSeconds(1)); circuitBreaker.Trip(connectionException); System.Threading.Thread.Sleep(5000); Assert.IsTrue(circuitBreakerTripActionCalled); } [Test] public void When_the_circuit_breaker_is_reset_the_trip_action_is_not_called() { bool circuitBreakerTripActionCalled = false; var connectionException = new Exception("Something bad happened."); var circuitBreaker = new CircuitBreaker("CheckServiceConnection", exception => { Console.WriteLine("Circuit breaker tripped - fail fast"); circuitBreakerTripActionCalled = true; // You would normally fail fast here in the action to faciliate the process shutdown by calling: // Environment.FailFast(connectionException.Message); }, 3, TimeSpan.FromSeconds(2)); circuitBreaker.Trip(connectionException); System.Threading.Thread.Sleep(1000); circuitBreaker.Reset(); Assert.False(circuitBreakerTripActionCalled); }
}</pre>
上面代碼案例采用Console.WriteLine,你可以選擇自己喜歡的logger。
最后結語
斷路器是現代社會重要的組成部分,可以說是最重要的安全設備之一。不論是一個熔化的保險絲,或者是跳閘的斷路器,它們的存在背后都有其充足的理由。
監控重要的資源,一旦它們無法響應,斷路器就迅速停止工作,進而確保整個運維團隊做出正確的響應。
如果你想進一步了解這些設計模式,請看Michael T. Nygard 的《Release It》,這是一本相當不錯的讀物。
原文鏈接: Indu Alagarsamy 翻譯: ImportNew.com - 喬永琪
譯文鏈接: http://www.importnew.com/16101.html