1. 程式人生 > >ASP.NET Core 微服務初探[2]:熔斷降級之Polly

ASP.NET Core 微服務初探[2]:熔斷降級之Polly

當我們從單體架構遷移到微服務模式時,其中一個比較大的變化就是模組(業務,服務等)間的呼叫方式。在以前,一個業務流程的執行在一個程序中就完成了,但是在微服務模式下可能會分散到2到10個,甚至更多的機器(微服務)上,這必然就要使用網路進行通訊。而網路本身就是不可靠的,並隨著每個服務都根據自身的情況進行的動態擴容,以及機器漂移等等。可以說,在微服務中,網路連線緩慢,資源繁忙,暫時不可用,服務離線等異常情況已然變成了一種常態。因此我們必須要有一種機制來保證服務整體的穩定性,而本文要介紹的熔斷降級就是一種很好的應對方案。

服務熔斷

avalanche

在介紹熔斷之前,我們先來談談微服務中的雪崩效應。在微服務中,服務A呼叫服務B,服務B可能會呼叫服務C,服務C又可能呼叫服務D等等,這種情況非常常見。如果服務D出現不可用或響應時間過長,就會導致服務C原來越多的執行緒處於網路呼叫等待狀態,進而影響到服務B,再到服務A等,最後會耗盡整個系統的資源,導致整體的崩潰,這就是微服務中的“雪崩效應”。

而熔斷機制就是應對雪崩效應的一種鏈路保護機制。其實,對於熔斷這個詞我們並不陌生,在日常生活中經常會接觸到,比如:家用電力過載保護器,一旦電壓過高(發生漏電等),就會立即斷電,有些還會自動重試,以便在電壓正常時恢復供電。再比如:股票交易中,如果股票指數過高,也會採用熔斷機制,暫停股票的交易。同樣,在微服務中,熔斷機制就是對超時的服務進行短路,直接返回錯誤的響應資訊,而不再浪費時間去等待不可用的服務,防止故障擴充套件到整個系統,並在檢測到該服務正常時恢復呼叫鏈路。

IM1L漏電斷路器

服務降級

當我們談到服務熔斷時,經常會提到服務降級,它可以看成是熔斷器的一部分,因為在熔斷器框架中,通常也會包含服務降級功能。

降級的目的是當某個服務提供者發生故障的時候,向呼叫方返回一個錯誤響應或者替代響應。從整體負荷來考慮,某個服務熔斷後,伺服器將不再被呼叫,此時客戶端可以自己準備一個本地的fallback回撥,這樣,雖然服務水平下降,但總比直接掛掉的要好。比如:呼叫聯通介面伺服器傳送簡訊失敗之後,改用移動簡訊伺服器傳送,如果移動簡訊伺服器也失敗,則改用電信簡訊伺服器,如果還失敗,則返回“失敗”響應;再比如:在從推薦商品伺服器載入資料的時候,如果失敗,則改用從快取中載入,如果快取也載入失敗,則返回一些本地替代資料。

在某些情況下,我們也會採取主動降級的機制,比如雙十一活動等,由於資源的有限,我們也可以把少部分不重要的服務進行降級,以保證重要服務的穩定,待度過難關,再重新開啟。

Polly基本使用

在.Net Core中有一個被.Net基金會認可的庫Polly,它一種彈性和瞬態故障處理庫,可以用來簡化對服務熔斷降級的處理。主要包含以下功能:重試(Retry),斷路器(Circuit-breaker),超時檢測(Timeout),艙壁隔離(Bulkhead Isolation), 快取(Cache),回退(FallBack)。

該專案作者現已成為.NET基金會一員,一直在不停的迭代和更新,專案地址: https://github.com/App-vNext/Polly

策略

在Polly中,有一個重要的概念:Policy,策略有“故障定義”和“故障恢復”兩部分組成。故障是指異常、非預期的返回值等情況,而動作則包括重試(Retry)、熔斷(Circuit-Breaker)、Fallback(降級)等。

故障定義

故障也可以說是觸發條件,它使用Handle<T>來定義,表示在什麼情況下,才對其進行處理(熔斷,降級,重試等)。

一個簡單的異常故障定義如下:

Policy.Handle<HttpRequestException>()

如上,表示當我們的程式碼觸發HttpRequestException異常時,才進行處理。

我們也可以對異常的資訊進行過濾:

Policy.Handle<SqlException>(ex => ex.Number == 1205)

如上,只有觸發SqlException異常,並且其異常號為1205的時候才進行處理。

如果我們希望同時處理多種異常,可以使用Or<T>來實現:

Policy.Handle<HttpRequestException>().Or<OperationCanceledException>()

Policy.Handle<SqlException>(ex => ex.Number == 1205).Or<ArgumentException>(ex => ex.ParamName == "example")

除此之外,我們還可以根據返回結果進行故障定義:

Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.NotFound)

如上,當返回值為HttpResponseMessage,並且其StatusCodeNotFound時,才對其進行處理。更多用法參考:usage--fault-handling-policies

故障恢復

當定義了故障後,要考慮便是如何對故障進行恢復了,Polly中常用的有以下幾種恢復策略:

重試(Retry)策略

重試就是指Polly在呼叫失敗時捕獲我們指定的異常,並重新發起呼叫,如果重試成功,那麼對於呼叫者來說,就像沒有發生過異常一樣。在網路呼叫中經常出現瞬時故障,那麼重試機制就非常重要。

一個簡單的重試策略定義如下:

// 當發生HttpRequestException異常時,重試3次
var retryPolicy = Policy.Handle<HttpRequestException>().Retry(3);

有些情況下,如果故障恢復的太慢,我們重試的過快是沒有任何任何意義的,這時可以指定重試的時間間隔:

Policy.Handle<HttpRequestException>().WaitAndRetryAsync(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt - 1)));

如上,重試五次,並且重試時間指數級增加。

超時(Timeout)策略

超時是我們比較常見的,比如HttpClient就可以設定超時時間,如果在指定的時間內還沒有返回,就觸發一個TimeoutException異常,而Polly的超時機制與其類似,只不過超時時觸發的是一個TimeoutRejectedException異常。

// 如果30秒種內沒有執行完成,就觸發`TimeoutRejectedException`異常
Policy.TimeoutAsync(30);

// 設定超時回撥
Policy.TimeoutAsync(30, onTimeout: (context, timespan, task) =>
{
    // do something 
});

由於超時策略本身就是丟擲一個超時異常,所以不需要設定觸發條件。

回退(FallBack)策略

回退也稱服務降級,用來指定發生故障時的備用方案。

Policy<string>.Handle<HttpRequestException>().FallbackAsync("substitute data", (exception, context) =>
{
    // do something 
});

如上,如果觸發HttpRequestException異常時,就返回固定的substitute data

熔斷(Circuit-breaker)策略

斷路器用於在服務多次不可用時,快速響應失敗,保護系統故障免受過載。

Policy.Handle<HttpRequestException>().Or<TimeoutException>()
    .CircuitBreakerAsync(
        // 熔斷前允許出現幾次錯誤
        exceptionsAllowedBeforeBreaking: 3,
        // 熔斷時間
        durationOfBreak: TimeSpan.FromSeconds(100),
        // 熔斷時觸發
        onBreak: (ex, breakDelay) =>
        {
            // do something 
        },
        // 熔斷恢復時觸發
        onReset: () =>
        {
            // do something 
        },
        // 在熔斷時間到了之後觸發
        onHalfOpen: () =>
        {
            // do something 
        }
    );

如上,如果我們的業務程式碼連續失敗3次,就觸發熔斷(onBreak),就不會再呼叫我們的業務程式碼,而是直接丟擲BrokenCircuitException異常。當熔斷時間(100s)過後,切換為HalfOpen狀態,觸發onHalfOpen事件,此時會再呼叫一次我們的業務程式碼,如果呼叫成功,則觸發onReset事件,並解除熔斷,恢復初始狀態,否則立即切回熔斷狀態。

更多策略的用法檢視:usage--general-resilience-policie

執行

在上面的示例中,我們熟悉了各種策略的定義,那麼接下來就是執行它。也就是使用Polly包裹我們的業務程式碼,Polly會攔截業務程式碼中的故障,並根據指定的策略進行恢復。

最簡單的策略執行方式如下:

var policy = /*策略定義*/;
var res = await policy.ExecuteAsync(/*業務程式碼*/);

如果需要同時指定多個策略,可以使用Policy.Wrap來完成:

Policy.Wrap(retry, breaker, timeout).ExecuteAsync(/*業務程式碼*/);

其實Warp本質就是多個策略的巢狀執行,使用如下寫法效果是一樣的:

fallback.Execute(() => waitAndRetry.Execute(() => breaker.Execute(action)));

關於Polly更詳細的用法可以檢視Polly Github上的https://github.com/App-vNext/Polly/wiki,本文就不再過多介紹。

Polly熔斷降級實戰

場景:輪詢呼叫服務A和服務B,單次呼叫時間不得超過1s,呼叫失敗時自動切換到另外一個服務重試一次,如果都失敗,進行優雅的降級,返回模擬資料,並在2個服務都多次失敗後進行熔斷。

首先建立一個ASP.NET Core Console程式,命名為PollyDemo。

然後引入Polly的官方Nuge包:

dotnet add package Polly

在我們首先定義一個超時策略:

var timeoutPolicy = Policy.TimeoutAsync(1, (context, timespan, task) =>
{
    Console.WriteLine("It's Timeout, throw TimeoutRejectedException.");
    return Task.CompletedTask;
});

可以根據實際情況來設定超時時間,我這裡為了方便測試,就設定為1s。

然後定義重試策略:

var retryPolicy = Policy.Handle<HttpRequestException>().Or<TimeoutException>().Or<TimeoutRejectedException>()
    .WaitAndRetryAsync(
        retryCount: 2,
        sleepDurationProvider: retryAttempt =>
        {
            var waitSeconds = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt - 1));
            Console.WriteLine(DateTime.Now.ToString() + "-Retry:[" + retryAttempt + "], wait " + waitSeconds + "s!");
            return waitSeconds;
        });

再定義一個熔斷策略:

var circuitBreakerPolicy = Policy.Handle<HttpRequestException>().Or<TimeoutException>().Or<TimeoutRejectedException>()
    .CircuitBreakerAsync(
        // 熔斷前允許出現幾次錯誤
        exceptionsAllowedBeforeBreaking: 2,
        // 熔斷時間
        durationOfBreak: TimeSpan.FromSeconds(3),
        // 熔斷時觸發
        onBreak: (ex, breakDelay) =>
        {
            Console.WriteLine(DateTime.Now.ToString() + "Breaker->Breaking the circuit for " + breakDelay.TotalMilliseconds + "ms! Exception: ", ex.Message);
        },
        // 熔斷恢復時觸發
        onReset: () =>
        {
            Console.WriteLine(DateTime.Now.ToString() + "Breaker->Call ok! Closed the circuit again.");
        },
        // 在熔斷時間到了之後觸發
        onHalfOpen: () =>
        {
            Console.WriteLine(DateTime.Now.ToString() + "Breaker->Half-open, next call is a trial.");
        }
    );

如上,連續錯誤2次就熔斷3秒。

最後,再定義一個回退策略:

var fallbackPolicy = Policy<string>.Handle<Exception>()
    .FallbackAsync(
        fallbackValue: "substitute data",
        onFallbackAsync: (exception, context) =>
        {
            Console.WriteLine("It's Fallback,  Exception->" + exception.Exception.Message + ", return substitute data.");
            return Task.CompletedTask;
        });

我們的業務程式碼如下:

private List<string> services = new List<string> { "localhost:5001", "localhost:5002" };
private int serviceIndex = 0;
private HttpClient client = new HttpClient();

private Task<string> HttpInvokeAsync()
{
    if (serviceIndex >= services.Count)
    {
        serviceIndex = 0;
    }
    var service = services[serviceIndex++];
    Console.WriteLine(DateTime.Now.ToString() + "-Begin Http Invoke->" + service);
    return client.GetStringAsync("http://" + service + "/api/values");
}

這裡方便測試,直接寫死了兩個服務,對其輪詢呼叫,在生產環境中可以參考上一篇《服務發現之Consul》來實現服務發現和負載均衡。

現在,我們組合這些策略來呼叫我們的業務程式碼:

for (int i = 0; i < 100; i++)
{
    Console.WriteLine(DateTime.Now.ToString() + "-Run[" + i + "]-----------------------------");
    var res = await fallbackPolicy.WrapAsync(Policy.WrapAsync(circuitBreakerPolicy, retryPolicy, timeoutPolicy)).ExecuteAsync(HttpInvokeAsync);
    Console.WriteLine(DateTime.Now.ToString() + "-Run[" + i + "]->Response" + ": Ok->" + res);
    await Task.Delay(1000);
    Console.WriteLine("--------------------------------------------------------------------------------------------------------------------");
}

如上,迴圈執行100次,策略的執行是非常簡單的,唯一需要注意的就是呼叫的順序:如上是依次從右到左進行呼叫,首先是進行超時的判斷,一旦超時就觸發TimeoutRejectedException異常,然後就進入到了重試策略中,如果重試了一次就成功了,那就直接返回,不再觸發其他策略,否則就進入到熔斷策略中:

breaking

如上圖,服務A(localhost:5001)和服務B(localhost:5001),都沒有啟動,所以會一直呼叫失敗,最後熔斷器開啟,並最終被降級策略攔截,返回substitute data

現在我們啟動服務A,可以看到服務會自動恢復,解除熔斷狀態:

reset

總結

本篇首先講解了一下微服務中熔斷、降級的基本概念,然後對.Net Core中的Polly框架做了一個基本介紹,最後基於Polly演示瞭如何在.NET Core中實現熔斷降級來提高服務質量。而熔斷本質上只是一個保護殼,在周圍出現異常的時候保全自身,從長遠來看,平時定期做好壓力測試才能防範於未然,降低觸發熔斷的次數。如果清楚的知道每個服務的承載量,並做好服務限流的控制,就能將“高壓”下觸發熔斷的概率降到最低了。那下一篇就來介紹一下速率限制(Rate Limiting),敬請期待!

附本篇示例原始碼地址:https://github.com/RainingNight/AspNetCoreSample/tree/master/src/Microservice/CircuitBreaker/PollyDemo

參考資料