上一章節將了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調用遠程接口服務的方法不變。