.NET 弹性和瞬时处理库Polly

.NET 弹性和瞬时处理库Polly

Polly 是一个 .NET 弹性和瞬态故障处理库,允许开发人员以 Fluent 和线程安全的方式来实现重试、断路、超时、隔离和回退策略。

首先这里的说的瞬态故障包含了程序发生的异常和出现不符合开发者预期的结果。所谓瞬态故障,就是说故障不是必然会发生的,而是偶然可能会发生的,比如网络偶尔会突然出现不稳定或无法访问这种故障。至于弹性,就是指应对故障 Polly 的处理策略具有多样性和灵活性,它的各种策略可以灵活地定义和组合。

Polly可以很好的避免服务雪崩等问题。

故障处理策略示例

Polly 的异常处理策略的基本用法可以分为三个步骤,步骤说明包含在下面代码中:

static void Main(string[] args)
{
    Policy
        // 1. 指定要处理什么异常
        .Handle<HttpRequestException>()
        //    或者指定需要处理什么样的错误返回
        .OrResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.BadGateway)
        // 2. 指定重试次数和重试策略
        .Retry(3, (exception, retryCount, context) =>
        {
            Console.WriteLine($"开始第 {retryCount} 次重试:");
        })
        // 3. 执行具体任务
        .Execute(ExecuteMockRequest);

    Console.WriteLine("程序结束,按任意键退出。");
    Console.ReadKey();
}

static HttpResponseMessage ExecuteMockRequest()
{
    // 模拟网络请求
    Console.WriteLine("正在执行网络请求...");
    Thread.Sleep(3000);
     // 模拟网络错误
    return new HttpResponseMessage(HttpStatusCode.BadGateway);
}

下面具体来看 Polly 支持的各种故障处理策略。

Polly 的七种策略

Polly 可以实现重试、断路、超时、隔离、回退和缓存策略,下面给出这些策略的应用场景说明和基本使用方法。

重试(Retry)

出现故障自动重试,这个是很常见的场景,上面也已经给出例子了,这里不再细述。

断路(Circuit-breaker)

当系统遇到严重问题时,快速回馈失败比让用户/调用者等待要好,限制系统出错的体量,有助于系统恢复。比如,当我们去调一个第三方的 API,有很长一段时间 API 都没有响应,可能对方服务器瘫痪了。如果我们的系统还不停地重试,不仅会加重系统的负担,还会可能导致系统其它任务受影响。所以,当系统出错的次数超过了指定的阈值,就要中断当前线路,等待一段时间后再继续。

下面是一个基本的断路策略的使用方式:

Policy.Handle<SomeException>()
    .CircuitBreaker(2, TimeSpan.FromMinutes(1));

这句代码设定的策略是,当系统出现两次某个异常时,就停下来,等待 1 分钟后再继续。这是基本的用法,你还可以在断路时定义中断的回调和重启的回调。

超时(Timeout)

当系统超过一定时间的等待,我们就几乎可以判断不可能会有成功的结果。比如平时一个网络请求瞬间就完成了,如果有一次网络请求超过了 30 秒还没完成,我们就知道这次大概率是不会返回成功的结果了。因此,我们需要设置系统的超时时间,避免系统长时间做无谓的等待。

下面是超时策略的一个基本用法:

Policy.Timeout(30, onTimeout: (context, timespan, task) =>
{
    // do something
});

这里设置了超时时间不能超过 30 秒,否则就认为是错误的结果,并执行回调。

隔离(Bulkhead Isolation)

当系统的一处出现故障时,可能促发多个失败的调用,很容易耗尽主机的资源(如 CPU)。下游系统出现故障可能导致上游的故障的调用,甚至可能蔓延到导致系统崩溃。所以要将可控的操作限制在一个固定大小的资源池中,以隔离有潜在可能相互影响的操作。

下面是隔离策略的一个基本用法:

Policy.Bulkhead(12, context =>
{
    // do something
});

这个策略是最多允许 12 个线程并发执行,如果执行被拒绝,则执行回调。

回退(Fallback)

有些错误无法避免,就要有备用的方案。这个就像浏览器不支持一些新的 CSS 特性就要额外引用一个 polyfill 一样。一般情况,当无法避免的错误发生时,我们要有一个合理的返回来代替失败。

比如很常见的一个场景是,当用户没有上传头像时,我们就给他一个默认头像,这种策略可以这样定义:

Policy.Handle<Whatever>()
   .Fallback<UserAvatar>(() => UserAvatar.GetRandomAvatar())

缓存(Cache)

一般我们会把频繁使用且不会怎么变化的资源缓存起来,以提高系统的响应速度。如果不对缓存资源的调用进行封装,那么我们调用的时候就要先判断缓存中有没有这个资源,有的话就从缓存返回,否则就从资源存储的地方(比如数据库)获取后缓存起来,再返回,而且有时还要考虑缓存过期和如何更新缓存的问题。Polly 提供了缓存策略的支持,使得问题变得简单。

var memoryCacheProvider = new MemoryCacheProvider(myMemoryCache);
var cachePolicy = Policy.Cache(memoryCacheProvider, TimeSpan.FromMinutes(5));
TResult result = cachePolicy.Execute(context => getFoo(), new Context("FooKey"));

这是官方的一个使用示例用法,它定义了缓存 5 分钟过期的策略,然后把这个策略应用在指定的 Key(即 FooKey)上。

这一块内容值得用一整篇的内容来讲,下次有机会再详细讲讲 Polly 的缓存策略。

策略包(Policy Wrap)

一种操作会有多种不同的故障,而不同的故障处理需要不同的策略。这些不同的策略必须包在一起,作为一个策略包,才能应用在同一种操作上。这就是文章开头说的 Polly 的弹性,即各种不同的策略能够灵活地组合起来。

策略包的基本用法是这样的:

var policyWrap = Policy
  .Wrap(fallback, cache, retry, breaker, timeout, bulkhead);
policyWrap.Execute(...);

先是把预先定义好的多种不同的策略包在一起,作为一个整体策略,然后应用在同一个操作上。

超时策略

var memberJson = await Policy.TimeoutAsync(5, TimeoutStrategy.Pessimistic, (t, s, y) =>
{
    Console.WriteLine("超时了~~~~");
    return Task.CompletedTask;
}).ExecuteAsync(async () =>
{
    // 业务逻辑
    using var httpClient = new HttpClient();
    httpClient.BaseAddress = new Uri($"http://192.168.3.105:7800");
    var memberResult = await httpClient.GetAsync("/api/polly/timeout");
    memberResult.EnsureSuccessStatusCode();
    var json = await memberResult.Content.ReadAsStringAsync();
    Console.WriteLine(json);

    return json;
});

重试策略

//当发生 HttpRequestException 的时候触发 RetryAsync 重试,并且最多重试3次。
var memberJson1 = await Policy.Handle<HttpRequestException>().RetryAsync(3).ExecuteAsync(async () =>
{
    Console.WriteLine("重试中.....");
    using var httpClient = new HttpClient();
    httpClient.BaseAddress = new Uri($"http://localhost:8000");
    var memberResult = await httpClient.GetAsync("/member/1001");
    memberResult.EnsureSuccessStatusCode();
    var json = await memberResult.Content.ReadAsStringAsync();

    return json;
});

//使用 Polly 在出现当请求结果为 http status_code 500 的时候进行3次重试。
var memberResult = await Policy.HandleResult<HttpResponseMessage>
    (x => (int)x.StatusCode == 500).RetryAsync(3).ExecuteAsync(async () =>
{
    Thread.Sleep(1000);
    Console.WriteLine("响应状态码重试中.....");
    using var httpClient = new HttpClient();
    httpClient.BaseAddress = new Uri($"http://192.168.3.105:7800");
    var memberResult = await httpClient.GetAsync("/api/polly/500");

    return memberResult;
});

服务降级

// 首先我们使用 Policy 的 FallbackAsync("FALLBACK") 方法设置降级的返回值。当我们服务需要降级的时候会返回 "FALLBACK" 的固定值。
// 同时使用 WrapAsync 方法把重试策略包裹起来。这样我们就可以达到当服务调用失败的时候重试3次,如果重试依然失败那么返回值降级为固定的 "FALLBACK" 值。
var fallback = Policy<string>.Handle<HttpRequestException>().Or<Exception>().FallbackAsync("FALLBACK", (x) =>
{
    Console.WriteLine($"进行了服务降级 -- {x.Exception.Message}");
    return Task.CompletedTask;
}).WrapAsync(Policy.Handle<HttpRequestException>().RetryAsync(3));

var memberJson = await fallback.ExecuteAsync(async () =>
{
    using var httpClient = new HttpClient();
    httpClient.BaseAddress = new Uri($"http://192.168.3.105:7800");
    var result = await httpClient.GetAsync("/api/user/" + 1);
    result.EnsureSuccessStatusCode();
    var json = await result.Content.ReadAsStringAsync();
    return json;

});
Console.WriteLine(memberJson);
if (memberJson != "FALLBACK")
{
    var member = JsonConvert.DeserializeObject<User>(memberJson);
    Console.WriteLine($"{member!.Id}---{member.Name}");
}

服务熔断

//定义熔断策略
var circuitBreaker = Policy.Handle<Exception>().CircuitBreakerAsync(
   exceptionsAllowedBeforeBreaking: 2, // 出现几次异常就熔断
   durationOfBreak: TimeSpan.FromSeconds(10), // 熔断10秒
   onBreak: (ex, ts) =>
   {
       Console.WriteLine("circuitBreaker onBreak ."); // 打开断路器
   },
   onReset: () =>
   {
       Console.WriteLine("circuitBreaker onReset "); // 关闭断路器
   },
   onHalfOpen: () =>
   {
       Console.WriteLine("circuitBreaker onHalfOpen"); // 半开
   }
);

// 定义重试策略
var retry = Policy.Handle<HttpRequestException>().RetryAsync(3);
// 定义降级策略
var fallbackPolicy = Policy<string>.Handle<HttpRequestException>().Or<BrokenCircuitException>()
    .FallbackAsync("FALLBACK", (x) =>
{
    Console.WriteLine($"进行了服务降级 -- {x.Exception.Message}");
    return Task.CompletedTask;
})
    .WrapAsync(circuitBreaker.WrapAsync(retry));
string memberJsonResult = "";

do
{
    memberJsonResult = await fallbackPolicy.ExecuteAsync(async () =>
    {
        using var httpClient = new HttpClient();
        httpClient.BaseAddress = new Uri($"http://192.168.3.105:7800");
        var result = await httpClient.GetAsync("/api/user/" + 1);
        result.EnsureSuccessStatusCode();
        var json = await result.Content.ReadAsStringAsync();
        return json;
    });
    Thread.Sleep(1000);
} while (memberJsonResult == "FALLBACK");

if (memberJsonResult != "FALLBACK")
{
    var member = JsonConvert.DeserializeObject<User>(memberJsonResult);
    Console.WriteLine($"{member!.Id}---{member.Name}");
}

AOP+Polly

Polly策略特性配置类(用于设计策略参数)

/// <summary>
/// Polly策略特性配置类(用于设计策略参数)
/// </summary>
public class PollyPolicyConfigAttribute : Attribute
    {
        /// <summary>
        /// 最多重试几次,如果为0则不重试
        /// </summary>
        public int MaxRetryTimes { get; set; } = 0;

        /// <summary>
        /// 重试间隔的毫秒数
        /// </summary>
        public int RetryIntervalMilliseconds { get; set; } = 100;

        /// <summary>
        /// 是否启用熔断
        /// </summary>
        public bool IsEnableCircuitBreaker { get; set; } = false;

        /// <summary>
        /// 熔断前出现允许错误几次
        /// </summary>
        public int ExceptionsAllowedBeforeBreaking { get; set; } = 3;

        /// <summary>
        /// 熔断多长时间(毫秒)
        /// </summary>
        public int MillisecondsOfBreak { get; set; } = 1000;

        /// <summary>
        /// 执行超过多少毫秒则认为超时(0表示不检测超时)
        /// </summary>
        public int TimeOutMilliseconds { get; set; } = 0;

        /// <summary>
        /// 缓存多少毫秒(0表示不缓存),用“类名+方法名+所有参数ToString拼接”做缓存Key
        /// </summary>

        public int CacheTTLMilliseconds { get; set; } = 0;

        /// <summary>
        /// 回退方法
        /// </summary>
        public string? FallBackMethod { get; set; }
    }

定义AOP特性类及封装Polly策略

/// <summary>
/// 定义AOP特性类及封装Polly策略
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class PollyPolicyAttribute : Attribute, IInterceptor
    {
        private static ConcurrentDictionary<MethodInfo, AsyncPolicy> policies
            = new ConcurrentDictionary<MethodInfo, AsyncPolicy>();

        private static readonly IMemoryCache memoryCache
            = new MemoryCache(new MemoryCacheOptions());

        public void Intercept(IInvocation invocation)
        {
            if (!invocation.Method.IsDefined(typeof(PollyPolicyConfigAttribute), true))
            {
                // 直接调用方法本身
                invocation.Proceed();
            }
            else
            {
                PollyPolicyConfigAttribute pollyPolicyConfigAttribute = invocation.Method.GetCustomAttribute<PollyPolicyConfigAttribute>()!;
                //一个PollyPolicyAttribute中保持一个policy对象即可
                //其实主要是CircuitBreaker要求对于同一段代码要共享一个policy对象
                //根据反射原理,同一个方法的MethodInfo是同一个对象,但是对象上取出来的PollyPolicyAttribute
                //每次获取的都是不同的对象,因此以MethodInfo为Key保存到policies中,确保一个方法对应一个policy实例
                policies.TryGetValue(invocation.Method, out AsyncPolicy? policy);
                //把本地调用的AspectContext传递给Polly,主要给FallbackAsync中使用
                // 创建Polly上下文对象(字典)
                Context pollyCtx = new();
                pollyCtx["invocation"] = invocation;

                lock (policies)//因为Invoke可能是并发调用,因此要确保policies赋值的线程安全
                {
                    if (policy == null)
                    {
                        policy = Policy.NoOpAsync();//创建一个空的Policy
                        if (pollyPolicyConfigAttribute.IsEnableCircuitBreaker)
                        {
                            policy = policy.WrapAsync(Policy.Handle<Exception>()
                                .CircuitBreakerAsync(pollyPolicyConfigAttribute.ExceptionsAllowedBeforeBreaking,
                                TimeSpan.FromMilliseconds(pollyPolicyConfigAttribute.MillisecondsOfBreak),
                                onBreak: (ex, ts) =>
                                {
                                    Console.WriteLine($"熔断器打开 熔断{pollyPolicyConfigAttribute.MillisecondsOfBreak / 1000}s.");
                                },
                                onReset: () =>
                                {
                                    Console.WriteLine("熔断器关闭,流量正常通行");
                                },
                                onHalfOpen: () =>
                                {
                                    Console.WriteLine("熔断时间到,熔断器半开,放开部分流量进入");
                                }));
                        }
                        if (pollyPolicyConfigAttribute.TimeOutMilliseconds > 0)
                        {
                            policy = policy.WrapAsync(Policy.TimeoutAsync(() =>
                                TimeSpan.FromMilliseconds(pollyPolicyConfigAttribute.TimeOutMilliseconds),
                                Polly.Timeout.TimeoutStrategy.Pessimistic));
                        }
                        if (pollyPolicyConfigAttribute.MaxRetryTimes > 0)
                        {
                            policy = policy.WrapAsync(Policy.Handle<Exception>()
                                .WaitAndRetryAsync(pollyPolicyConfigAttribute.MaxRetryTimes, i =>
                                TimeSpan.FromMilliseconds(pollyPolicyConfigAttribute.RetryIntervalMilliseconds)));
                        }
                        // 定义降级测试
                        var policyFallBack = Policy.Handle<Exception>().FallbackAsync((fallbackContent, token) =>
                        {
                            // 必须从Polly的Context种获取IInvocation对象
                            IInvocation iv = (IInvocation)fallbackContent["invocation"];
                            var fallBackMethod = iv.TargetType.GetMethod(pollyPolicyConfigAttribute.FallBackMethod!);
                            var fallBackResult = fallBackMethod!.Invoke(iv.InvocationTarget, iv.Arguments);
                            iv.ReturnValue = fallBackResult;
                            return Task.CompletedTask;
                        }, (ex, t) =>
                        {
                            Console.WriteLine("====================>触发服务降级");
                            return Task.CompletedTask;
                        });

                        policy = policyFallBack.WrapAsync(policy);
                        //放入到缓存
                        policies.TryAdd(invocation.Method, policy);
                    }
                }

                // 是否启用缓存
                if (pollyPolicyConfigAttribute.CacheTTLMilliseconds > 0)
                {
                    //用类名+方法名+参数的下划线连接起来作为缓存key
                    string cacheKey = "PollyMethodCacheManager_Key_" + invocation.Method.DeclaringType
                                                                       + "." + invocation.Method + string.Join("_", invocation.Arguments);
                    //尝试去缓存中获取。如果找到了,则直接用缓存中的值做返回值
                    if (memoryCache.TryGetValue(cacheKey, out var cacheValue))
                    {
                        invocation.ReturnValue = cacheValue;
                    }
                    else
                    {
                        //如果缓存中没有,则执行实际被拦截的方法
                        Task task = policy.ExecuteAsync(
                            async (context) => {
                                invocation.Proceed();
                                await Task.CompletedTask;
                            }, 
                            pollyCtx
                        );
                        task.Wait();

                        //存入缓存中
                        using var cacheEntry = memoryCache.CreateEntry(cacheKey);
                        {
                            cacheEntry.Value = invocation.ReturnValue;
                            cacheEntry.AbsoluteExpiration = DateTime.Now + TimeSpan.FromMilliseconds(pollyPolicyConfigAttribute.CacheTTLMilliseconds);
                        }
                    }
                }
                else//如果没有启用缓存,就直接执行业务方法
                {
                    Task task = policy.ExecuteAsync(
                            async (context) => {
                                invocation.Proceed();
                                await Task.CompletedTask;
                            },
                            pollyCtx
                        );
                    task.Wait();
                }
            }
        }

    }

好文推荐

Polly项目地址:https://github.com/App-vNext/Polly

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

.NET 开源项目 Polly 介绍:https://www.cnblogs.com/willick/p/polly.html

posted @ 2022-04-19 00:15  AJun816  阅读(291)  评论(0编辑  收藏  举报