• .NetCore中HttpClient使用Polly实现熔断、降级和限流


    上一章节将了HttpClient结合Polly的基本用法,详情请看这里!

    本章节介绍熔断和降级。大家应该都知道每个网关都必备熔断和降级相关策略。而Polly为啥也会有熔断和降级呢?难道是个鸡肋?还是说熔断和鸡肋是让 HttpClient结合Polly专门来做网关用的,而我们在做实际的业务场景中根本用不着?这2个问题我思考了很久,我的答案是否定的。我分析的原因有如下几点,(限于自身能力和眼界只能考虑这几点,若有其他的欢迎告诉我。)

    1、Polly是业界顶顶有名的组件,他既然有熔断和降级这2个功能,那必然有很多用处,绝对不可能是个鸡肋。

    2、如果想要自己开发一个网关,我觉得用Polly完全是可以用的。

    3、如果不是专门做网关,那么他在什么样的场景下使用呢?我们在使用HttpClient的时候,都是调用第3方接口或公司内部接口,那么万一因网络问题、对方服务器异常、超时、接口程序异常等一些不可抗拒因素,调用接口失败了!也就是服务不可用时,我们的程序该如何应对?首先是重试机制,重试够用吗?好像不够用,重试几次一直失败,我们不能因为对方接口挂了影响自己程序的业务逻辑吧,所以得来再使用熔断和降级一起处理,我们就可以非常快速和优雅的给出一个熔断或降级的结果,也避免我们的程序一直 请求这些不可用的服务,造成资源的浪费以及影响性能。

    当然降级和熔断两者也可以单独使用。

     4、那熔断是否还有比用的地方呢?有,比如对方接口有防刷机制,万一我们不小心碰触到该机制了,或者是我们提前知道该机制,我们就可以使用熔断或限流来控制不碰触该机制,并且我们的程序可以优雅的处理这类情况。

    5、限流比较好理解, 调用对方的服务,我们程序作为客户端HttpClient,我们来限制我们调用对方服务的最大的流量和队列,这样可以避免因为我们的请求量过大而让对方的服务奔溃,第4点就是一个例子。

    熔断也叫断路器,英文Circuit Breaker,

    降级也叫 回退,英文Fallback,

    限流 也叫 舱壁隔离,英文Bulkhead。

    步骤一:在Startup的ConfigureServices里配置

    我们先上代码加上完整的注释

     //<1>、降级后的返回值
                HttpResponseMessage fallbackResponse = new HttpResponseMessage
                { 
                    Content = new StringContent("降级,服务暂时不可用"),
                    StatusCode = HttpStatusCode.InternalServerError,
    
                }; 
                //<2>、降级策略
                var FallBackPolicy = Policy<HttpResponseMessage>.Handle<Exception>().FallbackAsync(fallbackResponse, async b =>
                 {
                     // 1、降级打印异常
                     Console.WriteLine($"服务开始降级,异常消息:{b.Exception.Message}");
                     // 2、降级后的数据 
                     Console.WriteLine($"服务降级内容响应:{await fallbackResponse.Content.ReadAsStringAsync()}");
                     await Task.CompletedTask;
                 });
    
                //<3>、熔断策略/断路器策略
                var CircuitBreakPolicy = Policy<HttpResponseMessage> // HttpResponseMessage 为HttpClient的返回值
                    .Handle<Exception>() //捕获Exception异常
                    .OrResult(res => res.StatusCode == HttpStatusCode.InternalServerError || res.StatusCode == HttpStatusCode.RequestTimeout)
                    .CircuitBreakerAsync(
                        3,    // 出现3次异常
                        TimeSpan.FromSeconds(20), // 断路器的时间(例如:设置为20秒,断路器两秒后自动由开启到关闭)
                        (ex, ts) =>
                                {   //熔断器开启事件触发
                                    Console.WriteLine($"服务断路器开启,异常消息:{ex.Result}");
                                    Console.WriteLine($"服务断路器开启的时间:{ts.TotalSeconds}s");
                                }, //断路器重置事件触发
                        () => { Console.WriteLine($"服务断路器重置"); }, //断路器半开启事件触发
                        () => { Console.WriteLine($"服务断路器半开启(一会开,一会关)"); }
                     );
    
                //<4>、限流策略
                var bulk = Policy.BulkheadAsync<HttpResponseMessage>(
                   maxParallelization: 30,//最大请求并发数
                   maxQueuingActions: 20,//可以有20个请求在队列里排队 
                   onBulkheadRejectedAsync: context =>//当我们的请求超出了并发数时怎么处理 这里可以定义自己的规则
                   { 
                       return Task.CompletedTask;  
                   }
                 
    
                   );
    
                services.AddHttpClient();
                services.AddHttpClient("myhttpclienttest", client =>
                {
                    client.BaseAddress = new Uri("http://localhost:9002/");
                    // client.Timeout = TimeSpan.FromSeconds(1);
                })
                //<5>、添加 降级策略
                .AddPolicyHandler(FallBackPolicy)
                //<6>、添加 熔断策略                                                      
                .AddPolicyHandler(CircuitBreakPolicy)
                //<7>、添加限流策略
                .AddPolicyHandler(bulk)
                ;

    先看下带括号<>的大步骤,<1>到<7> ,注释已经写的非常清楚了。

    注意: 第一、  <3>熔断的策略里,我们加了   .OrResult(res => res.StatusCode == HttpStatusCode.InternalServerError || res.StatusCode == HttpStatusCode.RequestTimeout) ,网上的教程一般没有,这句话的意思是说,当请求返回的状态是500服务器异常或者请求超时,都计算在内。若没有这句话,对方的接口必须要抛异常,才计算在内。

             第二、这些策略可以组合起来使用;

    步骤二:HttpClient调用远程接口服务

    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Logging;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace HttpClientDemo.Controllers
    {
    
    
        [ApiController]
        [Route("[controller]")]
        public class WeatherForecastController : ControllerBase
        {
    
            private readonly ILogger<WeatherForecastController> _logger;
    
            private readonly IHttpClientFactory _clientFactory;
            public WeatherForecastController(ILogger<WeatherForecastController> logger, IHttpClientFactory clientFactory)
            {
                _logger = logger;
                _clientFactory = clientFactory;
    
            }
    
            /// <summary>
            /// 测试HttpClient和Polly
            /// </summary>
            /// <returns></returns>
    
            [HttpGet]
            public async Task<MyResult> Get()
            {
                MyResult result = new MyResult();
                try
                {
                   
    
                    Console.WriteLine("请求处理开始。。。。");  
                    var client = _clientFactory.CreateClient("myhttpclienttest"); 
    
                    var request = new HttpRequestMessage(HttpMethod.Get, "api/values/testtimeout");
                    HttpResponseMessage response = await client.SendAsync(request);
                    var content = await response.Content.ReadAsStringAsync();
                    if(response.StatusCode== HttpStatusCode.InternalServerError)
                    {
                        result.code = "400";
                        result.msg = content;
                    }
                    else
                    {
                        result.code = "200";
                        result.msg = content;
                    }
                    return result; 
                }
                catch (Exception ex)
                {
                    result.code = "401";
                    result.msg = ex.Message;
                    Console.WriteLine($"出理异常:{ex.Message}");
                    return result;
                }
            }
        }
    }

    这里有个坑,httpClient最后一定要返回的是 HttpResponseMessage ,不然无法返回降级的返回内容。

    步骤三、Polly在.NetCore项目中封装

     根据步骤一,我们将这些代码封起来。使用配置文件来配置相关参数。

    3.1 新建配置文件:pollyconfig.json,并且将该json文件保存的格式为UTF-8,不然会有中文乱码问题。

    {
      "Polly": [
        {
          "ServiceName": [ "myhttpclienttest", "myhttpclienttest2" ], //服务名称,可以多个服务使用同一个配置
          "TimeoutTime": 5, //超时时间设置,单位为秒
          "RetryCount": 2, //失败重试次数
          "CircuitBreakerOpenFallCount": 2, //执行多少次异常,开启短路器(例:失败2次,开启断路器)
          "CircuitBreakerDownTime": 6, //断路器关闭的时间(例如:设置为2秒,短路器两秒后自动由开启到关闭)
          "HttpResponseMessage": "系统繁忙,请稍后再试!", //降级处理提示信息
          "HttpResponseStatus": 200 //降级处理响应状态码
        },
        {
          "ServiceName": [ "myhttpclienttest3" ], //假如服务名称存在相同的,则后面的会替换掉前面的
          "TimeoutTime": 2,
          "RetryCount": 5,
          "CircuitBreakerOpenFallCount": 2,
          "CircuitBreakerDownTime": 8,
          "HttpResponseMessage": "系统繁忙,请稍后再试~!",
          "HttpResponseStatus": 503
        }
      ]
    }

    3.2 创建配置实体类:PollyHttpClientConfig.cs对应配置文件里的节点

    public class PollyHttpClientConfig
    {
        /// <summary>
        /// 服务名称
        /// </summary>
        public List<string> ServiceName { set; get; }
     
        /// <summary>
        /// 超时时间设置,单位为秒
        /// </summary>
        public int TimeoutTime { set; get; }
     
        /// <summary>
        /// 失败重试次数
        /// </summary>
        public int RetryCount { set; get; }
     
        /// <summary>
        /// 执行多少次异常,开启短路器(例:失败2次,开启断路器)
        /// </summary>
        public int CircuitBreakerOpenFallCount { set; get; }
     
        /// <summary>
        /// 断路器关闭的时间(例如:设置为2秒,短路器两秒后自动由开启到关闭)
        /// </summary>
        public int CircuitBreakerDownTime { set; get; }
     
        /// <summary>
        /// 降级处理消息(将异常消息封装成为正常消息返回,然后进行响应处理,例如:系统正在繁忙,请稍后处理.....)
        /// </summary>
        public string HttpResponseMessage { set; get; }
        /// <summary>
        /// 降级处理状态码(将异常消息封装成为正常消息返回,然后进行响应处理,例如:系统正在繁忙,请稍后处理.....)
        /// </summary>
        public int HttpResponseStatus { set; get; }
    }

    3.3 封装拓展类:PollyHttpClientServiceCollectionExtension.cs 

    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Polly;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Threading.Tasks;
    
    namespace HttpClientDemo
    {
        public static class PollyHttpClientServiceCollectionExtension
        {
            public static void AddPollyHttpClient(this IServiceCollection service)
            {
                //读取服务配置文件
                try
                {
                    var config = new ConfigurationBuilder().AddJsonFile("pollyconfig.json").Build(); //nuget: Microsoft.Extensions.Configuration.Json
                    List<PollyHttpClientConfig> configList = config.GetSection("Polly").Get<List<PollyHttpClientConfig>>(); // nuget: Microsoft.Extensions.Options.ConfigurationExtensions
                    if (configList != null && configList.Count > 0)
                    {
                        configList.ForEach((pollyHttpClientConfig) =>
                        {
                            service.AddPollyHttpClient(pollyHttpClientConfig);
    
                        });
                    }
                }
                catch (Exception ex)
                {
                    throw new Exception("请正确配置pollyconfig.json");
                }
            }
            public static void AddPollyHttpClient(this IServiceCollection service, PollyHttpClientConfig pollyHttpClientConfig)
            {
                if (pollyHttpClientConfig == null)
                    throw new Exception("请配置:pollyHttpClientConfig");
    
                if (pollyHttpClientConfig.ServiceName == null || pollyHttpClientConfig.ServiceName.Count < 1)
                    throw new Exception("请配置:pollyHttpClientConfig.Polly.ServiceName");
    
                for (int i = 0; i < pollyHttpClientConfig.ServiceName.Count; i++)
                {
                    var builder = service.AddHttpClient(pollyHttpClientConfig.ServiceName[i]);
    
                    builder.BuildFallbackAsync(pollyHttpClientConfig.HttpResponseMessage, pollyHttpClientConfig.HttpResponseStatus);
                    builder.BuildCircuitBreakerAsync(pollyHttpClientConfig.CircuitBreakerOpenFallCount, pollyHttpClientConfig.CircuitBreakerDownTime);
                    builder.BuildRetryAsync(pollyHttpClientConfig.RetryCount);
                    builder.BuildTimeoutAsync(pollyHttpClientConfig.TimeoutTime);
                }
            }
    
    
            //降级
            private static void BuildFallbackAsync(this IHttpClientBuilder builder, string httpResponseMessage, int httpResponseStatus)
            {
    
                if (httpResponseStatus < 1 || string.IsNullOrEmpty(httpResponseMessage))
                    return;
    
                HttpResponseMessage fallbackResponse = new HttpResponseMessage
                {
                    Content = new StringContent(httpResponseMessage),
                    StatusCode = (HttpStatusCode)httpResponseStatus
                };
    
                builder.AddPolicyHandler(Policy<HttpResponseMessage>.HandleInner<Exception>().FallbackAsync(fallbackResponse, async b =>
                {
                    // 1、降级打印异常
                    Console.WriteLine($"服务开始降级,异常消息:{b.Exception.Message}");
                    // 2、降级后的数据
                    Console.WriteLine($"服务降级内容响应:{await fallbackResponse.Content.ReadAsStringAsync()}");
                    await Task.CompletedTask;
                }));
            }
            //熔断
            private static void BuildCircuitBreakerAsync(this IHttpClientBuilder builder, int circuitBreakerOpenFallCount, int circuitBreakerDownTime)
            {
                if (circuitBreakerOpenFallCount < 1 || circuitBreakerDownTime < 1)
                    return;
    
                builder.AddPolicyHandler(
                               Policy<HttpResponseMessage> // HttpResponseMessage 为HttpClient的返回值
                               .Handle<Exception>() //捕获Exception异常
                               .OrResult(res => res.StatusCode == HttpStatusCode.InternalServerError || res.StatusCode == HttpStatusCode.RequestTimeout)
                               .CircuitBreakerAsync(
                                   circuitBreakerOpenFallCount,    // 出现3次异常
                                   TimeSpan.FromSeconds(circuitBreakerDownTime), //10秒之内; 结合上面就是:10秒之内出现3次异常就熔断   
                                   (res, ts) =>
                                   {   //熔断器开启事件触发
                                       Console.WriteLine($"服务断路器开启,异常消息:{res.Result}");
                                       Console.WriteLine($"服务断路器开启的时间:{ts.TotalSeconds}s");
                                   }, //断路器重置事件触发
                                   () => { Console.WriteLine($"服务断路器重置"); }, //断路器半开启事件触发
                                   () => { Console.WriteLine($"服务断路器半开启(一会开,一会关)"); }
                                   )
                           );
    
            }
            //失败重试
            private static void BuildRetryAsync(this IHttpClientBuilder builder, int retryCount)
            {
                if (retryCount > 0)//失败重试
                    builder.AddPolicyHandler(Policy<HttpResponseMessage>.Handle<Exception>().RetryAsync(retryCount));
            }
            //超时
            private static void BuildTimeoutAsync(this IHttpClientBuilder builder, int timeoutTime)
            {
                if (timeoutTime > 0)//超时
                    builder.AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(timeoutTime)));
            }
        }
    }

    3.4 在startup.cs类的ConfigureServices方法中注册,放在所有命名模式的HttpClient客户端注册之后。

    services.AddPollyHttpClient();

    3.5 HttpClient调用远程接口服务的方法不变。

     

    作者:沐雪
    文章均系作者原创或翻译,如有错误不妥之处,欢迎各位批评指正。本文版权归作者和博客园共有,如需转载恳请注明。
    如果您觉得阅读这篇博客让你有所收获,请点击右下方【推荐】
    找一找教程网-随时随地学软件编程 http://www.zyiz.net/

  • 相关阅读:
    关于动态的添加iview admin路由以及刷新侧边栏按钮
    Qt配置,载入html,Echart, 交互
    junit、hamcrest和eclemma安装、使用
    程序中关于浮点数计算的问题
    洛谷P1164->小A点菜
    一道简单的题目
    P1101:单词方阵
    第一个博客
    php生成条形码
    php 上传音频(MP3、MP4等)文件 获取播放时间长度
  • 原文地址:https://www.cnblogs.com/puzi0315/p/15010844.html
Copyright © 2020-2023  润新知