采用斷路器設計模式來保護軟件
程序員的人生就像在一個快車道上行駛。幾周甚至幾小時完成某些特性編碼,打包測試沒有問題,蓋上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