.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調用遠程接口服務的方法不變。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM