【西天取经】实现net core自定义服务端熔断
网上关于net core实现服务熔断的都是客户端的例子,下面我来分享一个服务器端的例子,可以针对每一个用户请求做到服务器端的熔断操作。
在.neter里面,很多programer对流控里面熔断的知识了解不够全面,以为单纯用AspNetCoreRateLimit这样的包做一些流量入口的限制就可以了,其实流控里面还有很多的知识点,比如:超时,熔断,漏斗等等,像类似这样的流控规则必须要让用户请求真正进到程序内部才能做更多的规则限制。
.net core里面目前做的最好最权威是第三方包Polly。网上关于Polly的HttpClientFactory文章很多,这些例子它们的共同点都是在客户端发起请求的时候做了熔断的控制,可是真实的互联网环境里你只要把业务公开,暴露URL在互联网上,就会有很多客户或者流量进来,不管是好的坏的流量全都会过来,这时候服务器端的压力自然就会上来,能多一个流控规则就显得非常重要了,加上之前大家已经了解过的AspNetCoreRateLimit可以限制访问数量一起配合使用,会让你的程序运行的时候更加稳定。
正式介绍代码之前,这里多啰嗦一下,做一个简单的比喻,AspNetCoreRateLimit和熔断器之间的关系是:AspNetCoreRateLimit好比是家里的电源总开关,熔断器好比是家里电源总开关后面的那个保险丝。只有双剑合璧珠联璧合才能更好地保护运行时的程序,特别是现在程序都运行在docker或者k8s里面,更需要一个类似像保险丝一样的功能出现来保护我们运行的程序。于是乎这篇文章就诞生了。其实在java微服务里面早就有了这块知识,我这里只是把java里面的砖头搬了过来,并且没有使用任何和微服务框架有关系的框架实现。做到真正意义上可独立在微服务体系之外的断路器。因为我认为在k8s里面,类似微服务框架(Eureka,Nacos,Ocelot)可以省下不用了,k8s里面本身就是用服务名进行通信的,在多用一层感觉有点多余了。另外这些大的框架在运行像网站这样的程序时还是代码重了一些,只选择微服务里面的某几个功能拆出来用会更轻便更好掌握。虽然轮子我不会造,但车子我还是有自信能攒出一辆性能非常好的跑车来。
首先就是用VS创建一个WebAPI的项目出来,然后把下面的代码copy过去就可以用了。
通过middleware可以实现服务器端针对用户级别的熔断。感谢张浩帮忙。
CircuitBreakerMiddleware.cs
1 using System; 2 using System.Collections.Concurrent; 3 using System.Net; 4 using System.Threading.Tasks; 5 using Microsoft.AspNetCore.Http; 6 using Microsoft.AspNetCore.Http.Extensions; 7 using Microsoft.Extensions.Configuration; 8 using Microsoft.Extensions.Logging; 9 using Polly; 10 using Polly.CircuitBreaker; 11 12 namespace CircuitBreakerDemo 13 { 14 /// <summary> 15 /// net core 实现自定义断路器中间件 16 /// </summary> 17 public class CircuitBreakerMiddleware : IDisposable 18 { 19 private readonly RequestDelegate _next; 20 private readonly ConcurrentDictionary<string, AsyncPolicy> _asyncPolicyDict; 21 private readonly ILogger<CircuitBreakerMiddleware> _logger; 22 private readonly IConfiguration _configuration; 23 24 public CircuitBreakerMiddleware(RequestDelegate next, ILogger<CircuitBreakerMiddleware> logger, IConfiguration configuration) 25 { 26 this._next = next; 27 this._logger = logger; 28 this._configuration = configuration; //未来url的断路规则可以从config文件里读取,增加灵活性 29 logger.LogInformation($"{nameof(CircuitBreakerMiddleware)}.Ctor()"); 30 this._asyncPolicyDict = new ConcurrentDictionary<string, AsyncPolicy>(Environment.ProcessorCount, 31); 31 } 32 33 public async Task InvokeAsync(HttpContext context) 34 { 35 var request = context.Request; 36 try 37 { 38 await this._asyncPolicyDict.GetOrAdd(string.Concat(request.Method, request.GetEncodedPathAndQuery()) 39 , key => Policy.Handle<Exception>() 40 .AdvancedCircuitBreakerAsync 41 ( 42 //备注:20秒内,请求次数达到10次以上,失败率达到20%后开启断路器,断路器一旦被打开至少要保持5秒钟的打开状态。 43 failureThreshold: 0.2D, //失败率达到20%熔断开启 44 minimumThroughput: 10, //最多调用10次 45 samplingDuration: TimeSpan.FromSeconds(20), //评估故障持续时长20秒 46 durationOfBreak: TimeSpan.FromSeconds(5), //恢复正常使用前电路保持打开状态的最少时长5秒 47 onBreak: (exception, breakDelay, context) => //断路器打开时触发事件,程序不能使用 48 { 49 var ex = exception.InnerException ?? exception; 50 this._logger.LogError($"{key} => 进入打开状态,中断持续时长:{breakDelay},错误类型:{ex.GetType().Name},信息:{ex.Message}"); 51 }, 52 onReset: context => //断路器关闭状态触发事件,断路器关闭 53 { 54 this._logger.LogInformation($"{key} => 进入关闭状态,程序恢复正常使用"); 55 }, 56 onHalfOpen: () => //断路器进入半打开状态触发事件,断路器准备再次尝试操作执行 57 { 58 this._logger.LogInformation($"{key} => 进入半开状态,重新尝试接收请求"); 59 } 60 ) 61 ) 62 .ExecuteAsync(async () => await this._next(context)) 63 ; 64 } 65 catch (BrokenCircuitException exception) 66 { 67 this._logger.LogError($"{nameof(BrokenCircuitException)}.InnerException.Message:{exception.InnerException.Message}"); 68 var response = context.Response; 69 response.StatusCode = (int)HttpStatusCode.BadRequest; 70 response.ContentType = "text/plain; charset=utf-8"; 71 await response.WriteAsync("Circuit Broken o(╥﹏╥)o"); 72 } 73 74 //var endpoint = context.GetEndpoint(); 75 //if (endpoint != null) 76 //{ 77 // var controllerActionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>(); 78 // var controllerName = controllerActionDescriptor.ControllerName; 79 // var actionName = controllerActionDescriptor.ActionName; 80 // if (string.Equals(controllerName, "WeatherForecast", StringComparison.OrdinalIgnoreCase) 81 // && string.Equals(actionName, "Test", StringComparison.OrdinalIgnoreCase)) 82 // {//针对某一个控制器的某一个action,单独设置断路 83 // await Policy.Handle<Exception>().CircuitBreakerAsync(3, TimeSpan.FromSeconds(10)).ExecuteAsync(async () => await this._next(context)); 84 // } 85 // else 86 // { 87 // await this._next(context); 88 // } 89 //} 90 } 91 92 public void Dispose() 93 { 94 this._asyncPolicyDict.Clear(); 95 this._logger.LogInformation($"{nameof(CircuitBreakerMiddleware)}.Dispose()"); 96 } 97 } 98 }
Startup.cs
1 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 2 { 3 if (env.IsDevelopment()) 4 { 5 app.UseDeveloperExceptionPage(); 6 } 7 8 app.UseRouting(); 9 10 //注意位置在Routing下面,UseEndpoints上面 11 app.UseMiddleware<CircuitBreakerMiddleware>(); 12 13 app.UseEndpoints(endpoints => 14 { 15 endpoints.MapControllers(); 16 }); 17 }
WeatherForecastController.cs
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using Microsoft.AspNetCore.Mvc; 5 using Microsoft.Extensions.Logging; 6 7 namespace CircuitBreakerDemo.Controllers 8 { 9 [ApiController] 10 [Route("[controller]")] 11 public class WeatherForecastController : ControllerBase 12 { 13 private static readonly string[] Summaries = new[] 14 { 15 "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 16 }; 17 18 private static Random _Random = new Random(); 19 20 private readonly ILogger<WeatherForecastController> _logger; 21 22 public WeatherForecastController(ILogger<WeatherForecastController> logger) 23 { 24 this._logger = logger; 25 } 26 27 [HttpGet("test")] 28 public string Test() 29 { 30 int index = _Random.Next(Summaries.Length); 31 if (index % 3 == 0) 32 { 33 throw new Exception("程序运行错误"); 34 } 35 return Summaries[index]; 36 } 37 } 38 }
项目跑起来就是各种刷新页面了,然后就可以看到结果了。看不到熔断后的降级处理页面就一直刷新,一直到你看到Circuit Broken o(╥﹏╥)o的页面出现为止。
还有一个地方需要引起注意,就是项目里面如果使用了自定义异常处理的过滤器,这里添加的断路器的降级处理就会失效。
public class ExceptionFilter : IExceptionFilter
所以如果选择使用断路器,一定要把之前过滤器这里注释了。每一家这里开发可能不一样,我说的是我自己遇见的情况。
// options.Filters.Add<ExceptionFilter>(); //统一异常处理过滤器,改到断路器里面
错误显示结果分为页面,JSON包结构
1、 项目里面实际显示的错误结果页面(需要用到在中间件里面返回页面的知识点)
- 500的错误页面:
- 400的断路处理后的降级页面:
2、项目里面实际显示的错误结果包结构JSON
- 500的错误包结构内容:
- 400的错误包结构内容: