net6 polly 故障处理组件简单介绍+ 实战封装
1 polly 主要是故障处理组件
Polly是一个开源的弹性跟瞬态故障处理类库。它可以在你的程序出现故障,超时,或者返回值达成某种条件的时候进行多种策略处理,比如重试、超时、降级、熔断等等。
源码地址:https://github.com/App-vNext/Polly
1.1.超时策略
1.2重试策略
1.3降级策略
1.4熔断策略
1.5策略包裹(多重策略组装)
创建一个控制台 一个webapi 控制调用api
在控制台引入nuget 包
Install-Package Polly
webapi 项目创建几个接口 如下
// 定义超时调用的APi app.MapGet("/api/polly/timeout", () => { Thread.Sleep(6000); return "Polly Timeout"; }); // 定义500结果的APi app.MapGet("/api/polly/500", (HttpContext context) => { context.Response.StatusCode = 500; return "fail"; }); // 定义/api/user app.MapGet("/api/user/1", () => { var user = new Entiyt_memlst { u_id=10000, u_nme = "Jason Chen" }; return user; });
控制台代码如下
#region 超时策略 设定5秒超时 api 需要6秒返回结果 所以回执行超时 //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://localhost:5050"); // var memberResult = await httpClient.GetAsync("/api/polly/timeout"); // memberResult.EnsureSuccessStatusCode(); // var json = await memberResult.Content.ReadAsStringAsync(); // Console.WriteLine(json); // return json; //}); #endregion #region Polly 重试策略 // 重试有2种方式 一种是异常重试 一种是根据状态码重试 // 1当发生 HttpRequestException 的时候触发 RetryAsync 重试,并且最多重试3次。 //var memberJson = await Policy.Handle<HttpRequestException>().RetryAsync(3).ExecuteAsync(async () => //{ // Console.WriteLine("重试中....."); // using var httpClient = new HttpClient(); // httpClient.BaseAddress = new Uri($"http://localhost:5050"); // var memberResult = await httpClient.GetAsync("/member/1001"); // memberResult.EnsureSuccessStatusCode(); // var json = await memberResult.Content.ReadAsStringAsync(); // return json; //}); //2使用 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://localhost:5050"); // var memberResult = await httpClient.GetAsync("/api/polly/500"); // return memberResult; //}); #endregion #region 服务降级 //首先我们使用 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://localhost:5050"); // var result = await httpClient.GetAsync("/api/user/" + 1); // result.EnsureSuccessStatusCode(); // var json = await result.Content.ReadAsStringAsync(); // return json; //}); //Console.WriteLine(memberJson);// 当我们服务需要降级的时候会返回 "FALLBACK" 的固定值 //if (memberJson != "FALLBACK") //{ // var member = JsonConvert.DeserializeObject<Entiyt_memlst>(memberJson); // Console.WriteLine($"{member!.u_id}---{member.u_nme}"); //} #endregion //#region 服务熔断 ////定义熔断策略 //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://localhost:5050"); // 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<Entiyt_memlst>(memberJsonResult); // Console.WriteLine($"{member!.u_id}---{member.u_nme}"); //} //#endregion
包裹策略 效果展示如下:
2 实际开发中我们不可能一个方法写一次 polly 策略。所以要aop 一下。 这里用了autofac 里面的aop
需要新建一进程用来调用上面的webapi 新建的类有IUserService 、UserService (实现接口) CustomPollyPolicyInterceptor (polly 切面逻辑)
定义 IUserService
[Intercept(typeof(CustomPollyPolicyInterceptor))]//表示要polly生效 public interface IUserService { [PollyPolicyConfig(FallBackMethod = "UserServiceFallback",//UserServiceFallback 方法名称 IsEnableCircuitBreaker = true, ExceptionsAllowedBeforeBreaking = 3, MillisecondsOfBreak = 1000 * 5, CacheTTLMilliseconds = 1000 * 20)] User AOPGetById(int id); Task<User> GetById(int id); } public record User(int Id, string Name, string Account, string Password);
UserService 类如下
/// <summary> /// 【AOP方式定义】根据用户ID查询用户信息 /// </summary> /// <param name="id"></param> /// <returns></returns> /// <exception cref="NotImplementedException"></exception> public User AOPGetById(int id) { string url = $"http://localhost:5050/api/user/1"; // string realUrl = abstractConsulDispatcher.MapAddress(url); string content = httpAPIInvoker.HttpInvoke(url); var user = JsonConvert.DeserializeObject<User>(content)!; return user; }
autofac CustomPollyPolicyInterceptor切面逻辑逻辑如下:
[AttributeUsage(AttributeTargets.Method)] public class CustomPollyPolicyInterceptor : 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(); } } } }
在autofac 里面注册 单例 这样polly 才能触发 熔断机制
buider.RegisterType<UserService>().As<IUserService>().SingleInstance().EnableInterfaceInterceptors();
buider.RegisterType<CustomPollyPolicyInterceptor>();