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