乘風破浪,遇見最佳跨平臺跨終端框架.Net Core/.Net生態 - 淺析ASP.NET Core可用性設計,使用Polly定義重試、熔斷、限流、降級策略

什麼是Polly

https://github.com/App-vNext/Polly

http://www.thepollyproject.org

image

Polly是一個.NET彈性和瞬時故障處理庫,它允許開發者以流暢和線程安全的方式表達諸如重試、斷路、超時、隔板隔離、速率限制和回退等策略。

image

Polly是.Net生態非常著名的一個組件包。

Polly針對.NET標準1.1(覆蓋範圍:.NET Core 1.0、Mono、Xamarin、UWP、WP8.1+)和.NET標準2.0+(覆蓋範圍:.NET Core 2.0+、.NET Core 3.0,以及後來的Mono、Xamarin和UWP目標)。NuGet軟件包還包括.NET框架4.6.1和4.7.2的直接目標。

Polly組件包

  • Polly,這是Polly的核心包
  • Polly.Extensions.Http,Polly基於Http的一些擴展
  • Microsoft.Extensions.Http.Polly,HttpClientFactory組件包的Polly擴展包

Polly的能力

  • 失敗重試,當調用失敗時能夠自動重試
  • 服務熔斷,當部分服務不可用時,應用可以快速響應一個熔斷的結果,避免持續的請求這些不可用的服務而導致整個應用程序跪掉
  • 超時處理,指爲服務的請求設置一個超時時間,當超過超時時間時可以按照預定的操作進行處理,比如說返回一個緩存結果
  • 艙壁隔離,實際上是一個限流功能,可以爲服務定義最大的流量和隊列,這樣子避免我們的服務因爲請求量過大而被壓崩
  • 緩存策略,讓我們與類似於AOP的方式爲應用嵌入緩存的機制,可以當緩存命中時可以快速地響應緩存,而不是持續地請求服務
  • 失敗降級,指當服務不可用時,可以響應一個更友好的結果而不是報錯
  • 組合策略,可以讓我們將上面的策略組合在一起,按照一定的順序,可以對不同場景組合不同的策略類,實現應用程序

Polly的使用步驟

整個Polly的使用步驟是分三步走的:

  • 定義要處理的異常類型或返回值
  • 定義要處理的動作(重試、熔斷、降級響應等)
  • 使用定義的策略來執行代碼

使用Polly的失敗重試提高服務可用性

https://github.com/TaylorShi/HelloHighAvailability

適合失敗重試的場景

適合失敗重試的條件

  • 服務"失敗"是短暫的,可自愈的,在失敗重試的場景中,可以非常有效的避免這種網絡閃斷的情況
  • 服務是冪等的,重複調用不會有副作用,在失敗重試的場景下,有可能會造成多次調用的情況,有些失敗可能是命令已經發出了,但是還沒收到響應,它會重試,所以需要服務是冪等的,重複調用不能有副作用,這樣纔可以使用失敗重試

場景舉例

  • 網絡閃斷
  • 部分服務節點異常

最佳實踐

  • 設置失敗重試的次數,儘量設置重試的次數
  • 設置帶有步長策略的失敗等待間隔,儘量設置不同的間隔,重試的間隔時間需要設置,否則它會持續不斷地去重試,會造成類似於DDOS的情況
  • 設置降級響應,當我們失敗重試的次數達到上限以後,應該爲服務提供一個降級的響應,提供更友好的響應結果
  • 設置斷路器,爲我們的服務設置斷路器,就是熔斷,當我們重試一定次數可能服務還是不可用,那麼我們應該設置斷路器

針對HttpClientFactory的失敗重試策略

依賴包

https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly

dotnet add package Microsoft.Extensions.Http.Polly

image

因爲Grpc也是基於HttpClientFactory的,所以我們可以直接在之前Grpc的請求之上來添加Polly失敗重試策略,通過AddTransientHttpErrorPolicy

先查看下它的定義

namespace Microsoft.Extensions.DependencyInjection
{
    /// <summary>
    /// Extensions methods for configuring <see cref="PolicyHttpMessageHandler"/> message handlers as part of
    /// and <see cref="HttpClient"/> message handler pipeline.
    /// </summary>
    public static class PollyHttpClientBuilderExtensions
    {
        public static IHttpClientBuilder AddTransientHttpErrorPolicy(
            this IHttpClientBuilder builder,
            Func<PolicyBuilder<HttpResponseMessage>, IAsyncPolicy<HttpResponseMessage>> configurePolicy)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            if (configurePolicy == null)
            {
                throw new ArgumentNullException(nameof(configurePolicy));
            }

            var policyBuilder = HttpPolicyExtensions.HandleTransientHttpError();

            // Important - cache policy instances so that they are singletons per handler.
            var policy = configurePolicy(policyBuilder);

            builder.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(policy));
            return builder;
        }

    }
}

這裏通過RetryAsync可以自定義重試的次數

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpcClient<OrderGrpc.OrderGrpcClient>(grpcClientFactoryOptions =>
    {
        grpcClientFactoryOptions.Address = new Uri("https://localhost:5001");
    }).ConfigurePrimaryHttpMessageHandler(serviceProvider =>
    {
        var handler = new SocketsHttpHandler();
        // 允許無效或自簽名證書
        handler.SslOptions.RemoteCertificateValidationCallback = (a, b, c, d) => true;
        return handler;
    })
    .AddTransientHttpErrorPolicy(policyBuilder =>
    {
        // 當服務報錯,重試多少次
        // 當應用程序拋HttpRequestException或者響應500、408纔會去執行這個重試策略
        return policyBuilder.RetryAsync(3);
    });
}

還可以通過WaitAndRetryAsync設置每次重試的時間間隔

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpcClient<OrderGrpc.OrderGrpcClient>(grpcClientFactoryOptions =>
    {
        grpcClientFactoryOptions.Address = new Uri("https://localhost:5001");
    }).ConfigurePrimaryHttpMessageHandler(serviceProvider =>
    {
        var handler = new SocketsHttpHandler();
        // 允許無效或自簽名證書
        handler.SslOptions.RemoteCertificateValidationCallback = (a, b, c, d) => true;
        return handler;
    })
    .AddTransientHttpErrorPolicy(policyBuilder =>
    {
        // 當遇到HttpRequestException或者響應500、408時,等待10*N秒,然後重試,總共重試3次
        return policyBuilder.WaitAndRetryAsync(3, retryIndex =>
        {
            return TimeSpan.FromSeconds(retryIndex * 10);
        });
    });
}

還可以通過WaitAndRetryForeverAsync一直重試直到響應成功爲止

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpcClient<OrderGrpc.OrderGrpcClient>(grpcClientFactoryOptions =>
    {
        grpcClientFactoryOptions.Address = new Uri("https://localhost:5001");
    }).ConfigurePrimaryHttpMessageHandler(serviceProvider =>
    {
        var handler = new SocketsHttpHandler();
        // 允許無效或自簽名證書
        handler.SslOptions.RemoteCertificateValidationCallback = (a, b, c, d) => true;
        return handler;
    })
    .AddTransientHttpErrorPolicy(policyBuilder =>
    {
        // 當遇到HttpRequestException或者響應500、408時,等待10*N秒,然後重試,直到響應成功
        return policyBuilder.WaitAndRetryForeverAsync(retryIndex =>
        {
            return TimeSpan.FromSeconds(retryIndex * 10);
        });
    });
}

自定義失敗重試的策略

以上是HttpClientFactory Polly內置的一個策略,實際上還可以自定義自己的策略。

public void ConfigureServices(IServiceCollection services)
{
    var reg = services.AddPolicyRegistry();
    // 添加定義永遠重試的策略
    reg.Add("RetryForever", Policy.HandleResult<HttpResponseMessage>(message =>
    {
        // 當響應代碼爲201的時候滿足策略觸發條件
        return message.StatusCode == System.Net.HttpStatusCode.Created;
    }).RetryForeverAsync());

    // 給OrderClient添加這個自定義的策略
    services.AddHttpClient("OrderClient").AddPolicyHandlerFromRegistry("RetryForever");
}

這裏先定義了一個名爲RetryForever的策略,然後在後面的HttpClient上通過AddPolicyHandlerFromRegistry追加了這個策略。

還可以根據請求的場景來應用這個自定義策略,當請求的方式是Get的時候,應用RetryForever策略,否則不執行任何策略

public void ConfigureServices(IServiceCollection services)
{
    var reg = services.AddPolicyRegistry();
    // 添加定義永遠重試的策略
    reg.Add("RetryForever", Policy.HandleResult<HttpResponseMessage>(message =>
    {
        // 當響應代碼爲201的時候滿足策略觸發條件
        return message.StatusCode == System.Net.HttpStatusCode.Created;
    }).RetryForever());

    // 給OrderClient V2添加這個自定義的策略
    services.AddHttpClient("OrderClient-V2").AddPolicyHandlerFromRegistry((registry, message) =>
    {
        // 當請求的方式是Get的時候,應用RetryForever策略,否則不執行任何策略
        return message.Method == HttpMethod.Get ?
            registry.Get<IAsyncPolicy<HttpResponseMessage>>("RetryForever") :
            Policy.NoOpAsync<HttpResponseMessage>();
    });
}

基於這樣的方式,我們就可以對冪等性的接口來進行Retry,對於不支持冪等性的接口,還是直接拋出異常。

根據異常類型來提供應對策略

通過Policy.Handle方法可以針對某種類型的異常來應用策略,這裏可選的策略很多。

public void ConfigureServices(IServiceCollection services)
{
    // 當處理Exception異常時應用重試策略
    Policy.Handle<Exception>().WaitAndRetryAsync(3, retryIndex =>
    {
        return TimeSpan.FromSeconds(retryIndex * 10);
    });
}

看下AsyncRetrySyntax擴展方法的定義

namespace Polly
{
    /// <summary>
    ///     Fluent API for defining a <see cref="AsyncRetryPolicy" />.
    /// </summary>
    public static class AsyncRetrySyntax
    {
        public static AsyncRetryPolicy RetryAsync(this PolicyBuilder policyBuilder)
            => policyBuilder.RetryAsync(1);

        public static AsyncRetryPolicy RetryAsync(this PolicyBuilder policyBuilder, int retryCount)
        {
            Action<Exception, int, Context> doNothing = (_, __, ___) => { };

            return policyBuilder.RetryAsync(retryCount, onRetry: doNothing);
        }

        public static AsyncRetryPolicy RetryForeverAsync(this PolicyBuilder policyBuilder, Func<Exception, int, Context, Task> onRetryAsync)
        {
            if (onRetryAsync == null) throw new ArgumentNullException(nameof(onRetryAsync));

            return new AsyncRetryPolicy(
                policyBuilder,
                (outcome, timespan, i, ctx) => onRetryAsync(outcome, i, ctx)
            );
        }

        public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount, Func<int, TimeSpan> sleepDurationProvider)
        {
            Action<Exception, TimeSpan> doNothing = (_, __) => { };

            return policyBuilder.WaitAndRetryAsync(retryCount, sleepDurationProvider, doNothing);
        }

        public static AsyncRetryPolicy WaitAndRetryForeverAsync(this PolicyBuilder policyBuilder, Func<int, TimeSpan> sleepDurationProvider)
        {
            if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider));

            Action<Exception, TimeSpan> doNothing = (_, __) => { };

            return policyBuilder.WaitAndRetryForeverAsync(sleepDurationProvider, doNothing);
        }
    }
}

看下FallbackSyntax擴展方法的定義

namespace Polly
{
    /// <summary>
    /// Fluent API for defining a Fallback policy. 
    /// </summary>
    public static class FallbackSyntax
    {
        public static FallbackPolicy Fallback(this PolicyBuilder policyBuilder, Action fallbackAction)
        {
            if (fallbackAction == null) throw new ArgumentNullException(nameof(fallbackAction));

            Action<Exception> doNothing = _ => { };
            return policyBuilder.Fallback(fallbackAction, doNothing);
        }

        public static FallbackPolicy Fallback(this PolicyBuilder policyBuilder, Action<CancellationToken> fallbackAction)
        {
            if (fallbackAction == null) throw new ArgumentNullException(nameof(fallbackAction));

            Action<Exception> doNothing = _ => { };
            return policyBuilder.Fallback(fallbackAction, doNothing);
        }
    }

    /// <summary>
    /// Fluent API for defining a Fallback policy governing executions returning TResult. 
    /// </summary>
    public static class FallbackTResultSyntax
    {
        public static FallbackPolicy<TResult> Fallback<TResult>(this PolicyBuilder<TResult> policyBuilder, TResult fallbackValue)
        {
            Action<DelegateResult<TResult>> doNothing = _ => { };
            return policyBuilder.Fallback(() => fallbackValue, doNothing);
        }

        public static FallbackPolicy<TResult> Fallback<TResult>(this PolicyBuilder<TResult> policyBuilder, Func<TResult> fallbackAction)
        {
            if (fallbackAction == null) throw new ArgumentNullException(nameof(fallbackAction));

            Action<DelegateResult<TResult>> doNothing = _ => { };
            return policyBuilder.Fallback(fallbackAction, doNothing);
        }
    }
}

使用Polly熔斷慢請求雪崩效應

策略的類型

  • 被動策略(異常處理、結果處理),當服務響應出現一些異常或結果時進行處理
  • 主動策略(超時處理、斷路器、艙壁隔離、緩存),判斷實例是否超時、觸發足夠多異常、請求隊列已滿、異常命中,由策略來主動觸發的一些操作

組合策略

  • 降級響應
  • 失敗重試
  • 斷路器
  • 艙壁隔離

策略於狀態共享

Policy類型 狀態 說明
斷路器(Circuit Breaker) 有狀態 共享成功失敗率,以決定是否熔斷
艙壁隔離(Bulkhead) 有狀態 共享容量使用情況,以決定是否執行動作
緩存(Cache) 有狀態 共享緩存的對象,以決定是否命中
其它策略 無狀態

添加標準的熔斷策略

通過CircuitBreakerAsync可添加一個標準的熔斷策略,它是按失敗次數來作爲觸發條件的。

services.AddHttpClient("OrderClient-V3")
    // 添加一個策略
    .AddPolicyHandler(Policy<HttpResponseMessage>.Handle<HttpRequestException>()
    // 定義熔斷策略
    .CircuitBreakerAsync
    (
        // 報錯多次以後進行熔斷,這裏設置10次
        handledEventsAllowedBeforeBreaking: 10,
        // 熔斷的時間,這裏設置10秒
        durationOfBreak: TimeSpan.FromSeconds(10),
        // 當發生熔斷時觸發的一個事件
        onBreak: (r, t) => { },
        // 當熔斷恢復時觸發的一個事件
        onReset: () => { },
        // 在恢復之前進行驗證服務是否可用的請求時,打一部分流量去驗證我們的服務是否可用的事件
        onHalfOpen: () => { }
    ));

添加高級的熔斷策略

通過AdvancedCircuitBreakerAsync可添加一個高級的熔斷策略,它是按失敗比例來作爲觸發條件的。

services.AddHttpClient("OrderClient-V4")
    // 添加一個策略
    .AddPolicyHandler(Policy<HttpResponseMessage>.Handle<HttpRequestException>()
    // 定義高級熔斷策略,支持按採樣和失敗比例來觸發
    .AdvancedCircuitBreakerAsync
    (
        // 失敗的比例,有多少比例的請求失敗時進行熔斷,這裏設置80%
        failureThreshold: 0.8,
        // 採樣的時間,多少時間範圍內請求的80%的失敗,這裏設置10秒
        samplingDuration: TimeSpan.FromSeconds(10),
        // 最小的吞吐量,當請求量比較小的時候,10秒之內採樣如果失敗兩三個請求就會造成80%的失敗,所以我們設置請求數最少有100個的時候,纔會去觸發熔斷策略
        minimumThroughput: 100,
        // 熔斷的時間,這裏設置10秒
        durationOfBreak: TimeSpan.FromSeconds(10),
        // 當發生熔斷時觸發的一個事件
        onBreak: (r, t) => { },
        // 當熔斷恢復時觸發的一個事件
        onReset: () => { },
        // 在恢復之前進行驗證服務是否可用的請求時,打一部分流量去驗證我們的服務是否可用的事件
        onHalfOpen: () => { }
    ));

當熔斷觸發時,會觸發一個BrokenCircuitException類型的熔斷異常。

添加降級響應策略

先定義一個友好的響應message,然後針對熔斷異常,通過FallbackAsync來添加降級響應策略,以便做出一個友好的響應。

// 定義一個友好的響應
var message = new HttpResponseMessage
{
    Content = new StringContent("{}"),
};
// 針對熔斷異常BrokenCircuitException做出一個友好的響應
var fallbackPolicy = Policy<HttpResponseMessage>.Handle<BrokenCircuitException>().FallbackAsync(message);

組合多個策略並應用

通過Policy.WrapAsync可以將多個策略組合在一起,通過AddPolicyHandler應用到HTTPClient上。

var breakPolicy = Policy<HttpResponseMessage>.Handle<HttpRequestException>()
    // 定義高級熔斷策略,支持按採樣和失敗比例來觸發
    .AdvancedCircuitBreakerAsync
    (
        // 失敗的比例,有多少比例的請求失敗時進行熔斷,這裏設置80%
        failureThreshold: 0.8,
        // 採樣的時間,多少時間範圍內請求的80%的失敗,這裏設置10秒
        samplingDuration: TimeSpan.FromSeconds(10),
        // 最小的吞吐量,當請求量比較小的時候,10秒之內採樣如果失敗兩三個請求就會造成80%的失敗,所以我們設置請求數最少有100個的時候,纔會去觸發熔斷策略
        minimumThroughput: 100,
        // 熔斷的時間,這裏設置10秒
        durationOfBreak: TimeSpan.FromSeconds(10),
        // 當發生熔斷時觸發的一個事件
        onBreak: (r, t) => { },
        // 當熔斷恢復時觸發的一個事件
        onReset: () => { },
        // 在恢復之前進行驗證服務是否可用的請求時,打一部分流量去驗證我們的服務是否可用的事件
        onHalfOpen: () => { }
    );

// 定義一個友好的響應
var message = new HttpResponseMessage
{ 
    Content = new StringContent("{}"),
};
// 針對熔斷異常BrokenCircuitException做出一個友好的響應
var fallbackPolicy = Policy<HttpResponseMessage>.Handle<BrokenCircuitException>().FallbackAsync(message);

var retryPolicy = Policy<HttpResponseMessage>.Handle<Exception>()
    // 當遇到HttpRequestException或者響應500、408時,等待10*N秒,然後重試,總共重試3次
    .WaitAndRetryAsync(3, retryIndex => { return TimeSpan.FromSeconds(retryIndex * 10); });

// 組合多個策略
var wrapPolicy = Policy.WrapAsync(fallbackPolicy, retryPolicy, breakPolicy);

// 給HttpClient應用組合策略
services.AddHttpClient("OrderClient-V5").AddPolicyHandler(wrapPolicy);

在10秒鐘內有80%的請求出現異常時就熔斷,熔斷了以後拋出異常時會進行重試三次,如果三次內服務恢復了就響應正確的結果,如果最終我們的響應是失敗的,那我們就響應一個友好的結果。

添加限流的策略

通過Policy.BulkheadAsync可以自定義一個限流策略

// 限制最大併發數爲30的時候進行限流
var bulkPolicy = Policy.BulkheadAsync<HttpResponseMessage>(30);
// 給HttpClient應用限流策略
services.AddHttpClient("OrderClient-V6").AddPolicyHandler(bulkPolicy);

針對限流異常提供友好響應

Policy.BulkheadAsync還有更多參數可以設置,我們把它包裝成策略bulkAdvancedPolicy,限流發生後會觸發BulkheadRejectedException異常,我們可以根據這個異常類型,設計一個降級響應策略fallbackBulkPolicy,然後將他們組合起來,提供給HttpClient使用。

var bulkAdvancedPolicy = Policy.BulkheadAsync<HttpResponseMessage>
(
    // 最大併發數量,這裏設置30
    maxParallelization: 30,
    // 最大隊列數量,當我們請求超過30的併發數量時,定義了隊列數就有這麼多請求排隊,超出隊列的拋異常,否則沒有定義隊列數那麼就直接拋異常
    maxQueuingActions: 20,
    // 當請求被拒絕時(超過併發數被限流)做什麼操作
    onBulkheadRejectedAsync: context => Task.CompletedTask
);

// 定義一個友好的響應
var bulkFriendlymessage = new HttpResponseMessage
{
    Content = new StringContent("{}"),
};
// 針對限流異常BulkheadRejectedException做出一個友好的響應
var fallbackBulkPolicy = Policy<HttpResponseMessage>.Handle<BulkheadRejectedException>().FallbackAsync(bulkFriendlymessage);
// 組合多個策略
var wrapBulkPolicy = Policy.WrapAsync(fallbackBulkPolicy, bulkAdvancedPolicy);
// 給HttpClient應用限流策略
services.AddHttpClient("OrderClient-V7").AddPolicyHandler(wrapBulkPolicy);

總結

限流熔斷策略是有狀態的,它的狀態是指我們設置這些併發數、隊列數、熔斷採樣時間、吞吐量、錯誤數這些計數器的狀態,是由一個策略的實例去承載的。

如果希望對不同服務進行不同策略定義時,單獨的計算它的熔斷限流的數值時,就需要單獨爲他們定義不同的策略實例來去完成不同服務之間的定義的隔離。

另外,我們可以使用組合的方式來達到熔斷、限流、服務降級。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章