采用斷路器設計模式來保護軟件

ff6m 9年前發布 | 10K 次閱讀 設計模式

 程序員的人生就像在一個快車道上行駛。幾周甚至幾小時完成某些特性編碼,打包測試沒有問題,蓋上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
 

 本文由用戶 ff6m 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!