在.Net Core中有一個被.Net基金會承認的庫Polly,它一種彈性和瞬態故障處理庫,能夠用來簡化對服務熔斷降級的處理。
Polly的策略主要由「故障」和「動做」兩個部分組成,「故障」能夠包括異常、超時等狀況,「動做」則包括Fallback(降級)、重試(Retry)、熔斷(Circuit-Breaker)等。策略則用來執行業務代碼,當業務代碼出現了「故障」中的狀況時就開始執行「動做」。
主要包含如下功能:git
故障也能夠說是觸發條件,它使用Handle
來定義,表示在什麼狀況下,纔對其進行處理(熔斷,降級,重試等)。 github
// 單一異常種類 Policy.Handle<HttpRequestException>(); // 帶條件判斷的單一異常 Policy.Handle<SqlException>(ex => ex.Number == 10) // 多種異常 Policy.Handle<HttpRequestException>().Or<OperationCanceledException>() // 多種異常 Policy.Handle<HttpRequestException>().OrResult<HttpResponseMessage>(res => res.StatusCode != HttpStatusCode.OK) // 返回結果異常 Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode != HttpStatusCode.OK)
重試就是指Polly在調用失敗時捕獲咱們指定的異常,並從新發起調用,若是重試成功,那麼對於調用者來講,就像沒有發生過異常同樣。在網絡調用中常常出現瞬時故障,那麼重試機制就很是重要。bash
var client = new HttpClient(); Policy // 處理什麼異常,好比httprequrest異常 .Handle<HttpRequestException>() // 或者處理response的httpstatuscode 不等於200的狀況 .OrResult<HttpResponseMessage>(res => res.StatusCode != HttpStatusCode.OK) // 重試次數 3 .Retry(3, (ex, retryCount,content) => { Console.WriteLine($"請求Api異常,進行第{retryCount}次重試,ErrorCode:{ex.Result.StatusCode}"); }) // 要執行的任務 .Execute(() => { HttpResponseMessage res = client.GetAsync("http://qa.xx.com/Social/policy/1").Result; return res; });
回退也稱服務降級,用來指定發生故障時的備用方案。網絡
var client = new HttpClient(); Policy .Handle<HttpRequestException>() .OrResult<HttpResponseMessage>(res => res.StatusCode != HttpStatusCode.OK) // 出現異常只會回退處理 .Fallback(() => { HttpResponseMessage res = client.GetAsync("http://qa.xx.com/Social/policy/2").Result; Console.WriteLine("Fallback(降級)處理."); return res; }) .Execute(() => { HttpResponseMessage res = client.GetAsync("http://qa.xx.com/Social/policy/1").Result; return res; });
Polly支持兩種超時策略:async
var timeoutPolicy = Policy.TimeoutAsync(1, TimeoutStrategy.Pessimistic, (context, timespan, task) => { Console.WriteLine("請求超時."); return Task.CompletedTask; }); timeoutPolicy.ExecuteAsync(async () => { var client = new HttpClient(); await client.GetAsync("http://localhost:5000/home/delay"); return Task.CompletedTask; });
若是調用某個目標服務出現過多超時、異常等狀況,能夠採起必定時間內熔斷該服務的調用,熔斷期間的請求將再也不繼續調用目標服務,而是直接返回,節約資源,提升服務的穩定性,熔斷週期結束後若是目標服務狀況好轉則恢復調用。函數
熔斷器打開狀態,此時對目標服務的調用都直接返回錯誤,熔斷週期內不會走網絡請求,當熔斷週期結束時進入半開狀態;ui
關閉狀態下正常發生網絡請求,但會記錄符合熔斷條件的連續執行次數,若是錯誤數量達到設定的閾值(若是在沒有達到閾值以前恢復正常,以前的累積次數將會歸零),熔斷狀態進入到打開狀態;spa
半開狀態下容許定量的服務請求,若是調用都成功則認爲恢復了,關閉熔斷器,不然認爲還沒好,又回到熔斷器打開狀態;rest
注意:爲了服務的穩定性,在執行須要屢次 Retry(重試策略)的狀況下,最好組合熔斷策略,預防可能存在的風險。code
var client = new HttpClient(); var ciruitBreaker = Policy.Handle<Exception>() // 熔斷前容許出現3次錯誤,熔斷時間10s,熔斷時觸發, 熔斷恢復時觸發,在熔斷時間到了以後觸發 .CircuitBreaker(3, TimeSpan.FromSeconds(10), (ex, breakDelay) => { //熔斷時觸發 Console.WriteLine("斷路器打開,熔斷觸發."); }, () => { //熔斷恢復時觸發 Console.WriteLine("熔斷器關閉了."); }, () => { //在熔斷時間到了以後觸發 Console.WriteLine("熔斷時間到,進入半開狀態"); } // 模擬屢次調用,觸發熔斷 for (int i = 1; i <= 150; i++) { try { ciruitBreaker.Execute(() => { Console.WriteLine($"第{i}次開始執行."); var res = client.GetAsync("http://localhost:5000/home/delay").Result; Console.WriteLine($"第{i}次執行:正常:" + res.StatusCode); Thread.Sleep(TimeSpan.FromSeconds(1)); return res; }); } catch (Exception e) { Console.WriteLine($"第{i}次執行:異常:" + e.Message); Thread.Sleep(TimeSpan.FromSeconds(1)); } }
根據時間段內總請求數中的異常比例觸發熔斷
var client = new HttpClient(); var advancedCircuitBreaker = Policy.Handle<Exception>() .AdvancedCircuitBreaker(0.5, TimeSpan.FromSeconds(10), 3, TimeSpan.FromSeconds(10), (ex, breakDelay) => { Console.WriteLine("斷路器打開,熔斷觸發."); }, () => { Console.WriteLine("熔斷器關閉了."); }, () => { Console.WriteLine("熔斷時間到,進入半開狀態"); }); // 模擬屢次調用,觸發熔斷 for (int i = 1; i <= 150; i++) { try { advancedCircuitBreaker.Execute(() => { Console.WriteLine($"第{i}次開始執行."); var res = client.GetAsync("http://localhost:5000/home/delay").Result; Console.WriteLine($"第{i}次執行:正常:" + res.StatusCode); Thread.Sleep(TimeSpan.FromSeconds(1)); return res; }); } catch (Exception e) { Console.WriteLine($"第{i}次執行:異常:" + e.Message); Thread.Sleep(TimeSpan.FromSeconds(1)); } }
策略包提供了一種靈活的方式來封裝多個彈性策略(從右往左).
//定義超時 var timeOut = Policy.Timeout(TimeSpan.FromSeconds(10), ((context, timespan, task) => { Console.WriteLine("請求超時."); })); //定義重試 var retry = Policy.Handle<Exception>() .Retry(3, ((exception, retryCount, context) => { Console.WriteLine($"第{retryCount}次重試."); }) // 定義熔斷策略 var circuitBreaker = Policy.Handle<Exception>() // 熔斷前容許出現3次錯誤,熔斷時間10s,熔斷時觸發, 熔斷恢復時觸發,在熔斷時間到了以後觸發 .CircuitBreaker(3, TimeSpan.FromSeconds(10), (ex, breakDelay) => { //熔斷時觸發 Console.WriteLine("斷路器打開,熔斷觸發."); }, () => { //熔斷恢復時觸發 Console.WriteLine("熔斷器關閉了."); }, () => { //在熔斷時間到了以後觸發 Console.WriteLine("熔斷時間到,進入半開狀態"); }); //定義回退策略 var fallback = Policy.Handle<Exception>() .Fallback(() => { Console.WriteLine("正在降級處理."); } fallback.Wrap(Policy.Wrap(circuitBreaker,retry, timeOut)).Execute(() => { Console.WriteLine("start."); });
Install Microsoft.Extensions.Http
若是有多個能夠同時使用
// StartUp->ConfigureServices services.AddHttpClient("local",options => { options.BaseAddress = new Uri("http://localhost:5000"); } services.AddHttpClient("fanyou",options => { options.BaseAddress = new Uri("http://qa.fanyouvip.com"); }); //使用 [Route("client")] public class ClientController : ControllerBase { private readonly IHttpClientFactory _clientFactory; public ClientController(IHttpClientFactory clientFactory) { _clientFactory = clientFactory; } [HttpGet("Local")] public async Task<IActionResult> Local() { var client = _clientFactory.CreateClient("local"); var res = await client.GetAsync("/home/delay"); return Ok(res); } [HttpGet("Fanyou")] public async Task<IActionResult> Fanyou() { var client = _clientFactory.CreateClient("fanyou"); var res = await client.GetAsync("/social"); return Ok(res); } }
Install Microsoft.Extensions.Http.Polly
// 第一種方式 services.AddHttpClient("local", options => { options.BaseAddress = new Uri("http://localhost:5000"); }) .AddTransientHttpErrorPolicy(p => { var handlers = p.OrResult(result => result.StatusCode != HttpStatusCode.OK) .RetryAsync(3, (ex, retryCount, context) => { Console.WriteLine($"第{retryCount}次重試.異常:{ex.Exception.Message}"); }); return handlers; }).AddTransientHttpErrorPolicy(p => { var breaker = p.CircuitBreakerAsync(3, TimeSpan.FromSeconds(10)); return breaker; }); //第二種方式 services.AddHttpClient("Test", options => { options.BaseAddress = new Uri("http://localhost:5003"); }) .AddPolicyHandler(RetryPolicy()) .AddPolicyHandler(CircuiBreakerPolicy()); /// <summary> /// 重試策略 /// </summary> /// <returns>IAsyncPolicy<HttpResponseMessage></returns> private IAsyncPolicy<HttpResponseMessage> RetryPolicy() { return HttpPolicyExtensions .HandleTransientHttpError() .OrResult(res => res.StatusCode != HttpStatusCode.OK) .WaitAndRetryAsync(3, retryCount => TimeSpan.FromSeconds(Math.Pow(2, retryCount))); /// <summary> /// 熔斷策略 /// </summary> /// <returns>IAsyncPolicy<HttpResponseMessage></returns> private IAsyncPolicy<HttpResponseMessage> CircuiBreakerPolicy() { return HttpPolicyExtensions .HandleTransientHttpError() .CircuitBreakerAsync(5, TimeSpan.FromMinutes(1)); }