採用斷路器設計模式來保護軟體

ImportNew發表於2015-08-04

程式設計師的人生就像在一個快車道上行駛。幾周甚至幾小時完成某些特性編碼,打包測試沒有問題,蓋上QA認證,程式碼部署到生產環境。接著最壞的事情發生了,部署的軟體在執行中掛掉了。用墨菲法則來說,就是“會出錯的,終將出錯”。但是,如果我們寫程式碼時就能考慮到這些情況會怎樣?

那麼我們應對不好的事情並將其轉變為好的事情呢?

採用斷路器設計模式來保護軟體

電子技術拯救了我們

我至今記得我和我哥因為電湧不得不更換家裡的保險絲情景,那時我甚至對事件的嚴重程度一無所知,而他卻是電力方面的小能手。保險絲完全燒壞了,但它卻保護了我家的電視機。在電子工程領域,保險絲和斷路器用(Circuit Breaker)來處理這樣的問題,超大功率可能帶來一些嚴重的破壞,毀壞電子裝置甚至燒掉整個屋子。保險絲包含一個小電線絲,電量過大時就會融化,就像燒掉的電燈泡,阻止危險的電流,保障電器和房屋安全。

保險絲演變成斷路器,通常利用電磁鐵就可以斷開電路,而不用燒掉它,這樣斷路器就可以重置反覆地用。不過,它們的功能都是一樣的,檢測負載,接著迅速停止工作,保全其它部分不受破壞。

回過頭再想,這是一個多麼神奇的概念。僅僅壞掉某個控制元件——保險絲徹底壞掉,就可以避免了整個系統嚴重的損壞。多虧電湧後保險絲自熔,保住了電視機。那麼為何我們不能在軟體裡面做同樣的事情?壞事發生後,軟體中的某個控制元件會迅速停止工作。模仿現實生活中的場景,我們創造了斷路器設計模式

在分散式系統中,某些故障是短暫的,通過快速連續重試就可以解決問題;但在某些場景中,關鍵依賴的連線丟失了,短時間無法恢復。比如,某個應用失去了與雲中的持續化儲存連線。在這樣的場景中,關閉服務就可以避免錯誤的資料處理過程、甚至資料丟失或者級聯故障,進而防止對系統其它部分的進一步損壞。

藉助於迅速停止工作,運維部門就可以容易地進行監控和響應。在他們重視起來之前,那些徒勞嘗試重新連線的服務看起來仍然是正常的,因為本應該拉響的警報沒有響起。倘若某個服務在恰當的時候徹底失效,警告燈熄滅了,運維就會知曉問題所在,及時做出響應。

斷路器設計模式

在系統中可重用基礎架構實現斷路器設計模式是很容易實現的,它是這麼發揮作用的:

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;
        }
    }
}

斷路器模式簡單實現

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;
}

斷路器單元測試

[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);
    }
}

上面程式碼案例採用Console.WriteLine,你可以選擇自己喜歡的logger。

最後結語

斷路器是現代社會重要的組成部分,可以說是最重要的安全裝置之一。不論是一個熔化的保險絲或者跳閘的斷路器,其背後都有充足的理由。

監控重要的資源,一旦它們無法響應,就迅速停止工作,進而確保整個運維團隊做出正確的響應。

如果你想對這些設計模式做進一步瞭解,請看Michael T. Nygard 的《Release It》,它是本相當不錯的讀物。

相關文章