乘风破浪,遇见最佳跨平台跨终端框架.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);

总结

限流熔断策略是有状态的,它的状态是指我们设置这些并发数、队列数、熔断采样时间、吞吐量、错误数这些计数器的状态,是由一个策略的实例去承载的。

如果希望对不同服务进行不同策略定义时,单独的计算它的熔断限流的数值时,就需要单独为他们定义不同的策略实例来去完成不同服务之间的定义的隔离。

另外,我们可以使用组合的方式来达到熔断、限流、服务降级。

参考

posted @ 2022-10-22 22:12  TaylorShi  阅读(269)  评论(0编辑  收藏  举报