HttpClient 與 Polly 進行重試與斷路策略 - Microsoft.Extensions.Http.Resilience

在開發WebAPI串接第三方服務或是其他服務,通常會在HttpClient連線部分進行設定 Polly,以前會使用 Microsoft.Extensions.Http.Polly套件,但是最近翻文件發現 Polly 更新版本提供了 Pipeline 使用方式。
使用 HttpClientFactory 微軟官方也有新增 Microsoft.Extensions.Http.Resilience 來支援這方式。比起 Policy 方式宣告 Pipeline 在效能上更優化。而且提供斷路器可以進行監控狀態及手動控制等功能,讓斷路器實作上更方便。下面範例使用 .Net 8 進行示範。

Polly 官方文件

https://www.pollydocs.org/advanced/performance.html

微軟官方文件

https://learn.microsoft.com/en-us/dotnet/core/resilience/?tabs=dotnet-cli

安裝方式

1
dotnet add package Microsoft.Extensions.Http.Resilience

Circuit Breaker 斷路器實作與設定

比起之前版本新版提供 CircuitBreakerStateProviderCircuitBreakerManualControl類別。但是需要依照各斷路器設定不同的狀態提供者及控制器。

CircuitBreakerManualControl.cs 介紹

提供兩個方法 CloseAsyncIsolateAsync進行手動控制斷路器。

  • IsolateAsync(隔離)
    IsolateAsync 方法將斷路器手動設置為 Isolated 狀態。當呼叫此方法後,斷路器將進入隔離狀態,阻止所有請求的執行,無論系統是否正常。此狀態會保持,直到手動解除隔離。

  • CloseAsync(關閉)
    CloseAsync 方法將斷路器手動恢復到 Closed 狀態,允許所有請求正常通過。

CircuitBreakerStateProvider.cs 介紹

CircuitBreakerStateProvider.cs 有提供目前狀態 CircuitState ,並裡面有四種狀態以下是這四種狀態介紹。

  • Closed(關閉狀態):
    在 Closed 狀態下,斷路器允許執行所有操作。這表示電路正常運作,所有請求都會通過。通常當操作成功率高且錯誤次數未達到設定的閾值時,斷路器會保持在 Closed 狀態。

  • Open(開啟狀態):
    Open 狀態通常由控制器觸發,表示斷路器已被打開。這是因為請求失敗次數超過了設定閥值。在 Open 狀態下,所有操作的執行會被阻止,並拋出BrokenCircuitException

  • Half-open(半開狀態):
    Half-open 狀態表示斷路器正在從 Open 狀態恢復。當斷路器經過設定的 BreakDuration 時間後進入此狀態,表示正在嘗試恢復正常的請求處理。在此狀態下,斷路器會允許部分請求通過並監控其結果。如果這些請求成功,斷路器改到 Closed 狀態;如果請求再次失敗,斷路器回到 Open 狀態。

  • Isolated(隔離狀態):
    Isolated 狀態表示斷路器被手動隔離進入 Open 狀態。這是透過呼叫 CircuitBreakerManualControl.IsolateAsync 方法。在此狀態下,所有操作的執行會被阻止,並拋出 IsolatedCircuitException ,直到手動呼叫CircuitBreakerManualControl.CloseAsync方法將斷路器改為 Closed 狀態。

設定

這邊在設定上說明一下每欄位

Polly 官方文件

https://www.pollydocs.org/strategies/circuit-breaker.html

  • FailureRatio 為失敗率必須是0~1。如果是0.1的話,表示發送請求中達到10%的失敗就進行斷路。

  • SamplingDuration 計算失敗-成功比率的時間段。

  • MinimumThroughput 在指定的採樣持續時間內必須發生的最小執行次數。

  • BreakDuration 斷路時間長度,等到時間結束並不會馬上恢復,而是進行少量的請求。確定了服務正常才進行打開。

  • StateProvider 如果提供,則可以透過物件檢索電路的當前狀態。

  • ManualControl 如果提供,則可以透過物件手動控制電路的狀態。

  • Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

builder.Services.AddKeyedSingleton<CircuitBreakerStateProvider>("CircuitBreakerSamplePipeline");
builder.Services.AddKeyedSingleton<CircuitBreakerManualControl>("CircuitBreakerSamplePipeline");

builder.Services.AddHttpClient("CircuitBreakerSample")
.AddResilienceHandler
(
"CircuitBreakerSamplePipeline",
(pipelineBuilder,context)=>
{
// CircuitBreaker
var loggerFactory = context.ServiceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("SamplePipeline-CircuitBreaker");

var stateProvider = context.ServiceProvider.GetRequiredKeyedService<CircuitBreakerStateProvider>(CircuitBreakerPipelineNames.Sample);
var manualControl = context.ServiceProvider.GetRequiredKeyedService<CircuitBreakerManualControl>(CircuitBreakerPipelineNames.Sample);

var circuitBreakerStrategyOptions = new CircuitBreakerStrategyOptions<HttpResponseMessage>
{
FailureRatio = 0.01,
SamplingDuration = TimeSpan.FromMinutes(1),
MinimumThroughput = 2,
BreakDuration = TimeSpan.FromSeconds(30),
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult
(
response => response.StatusCode is HttpStatusCode.OK
),
StateProvider = stateProvider,
ManualControl = manualControl,
OnOpened = arg =>
{
logger.LogInformation("Circuit opened for {Duration} seconds due to: {StatusCode}", arg.BreakDuration.TotalSeconds, arg.Outcome.Result?.StatusCode);
return ValueTask.CompletedTask;
},
OnClosed = arg =>
{
logger.LogInformation("Circuit closed. Resuming normal operation.");
return ValueTask.CompletedTask;
}
};
pipelineBuilder.AddCircuitBreaker(circuitBreakerStrategyOptions);
}
);

使用方式

只需要與之前使用HttpClientFactory方式一樣即可

1
2
3
4
5
6
7
8
9
10
11

[HttpGet]
public async Task<IActionResult> CircuitBreaker()
{
var httpClient = _httpClientFactory.CreateClient("CircuitBreakerSample");

var response = await httpClient.GetAsync("https://ian26875.github.io/");

return Content(response.StatusCode.ToString());
}

演示範例

這邊也提供範例程式碼,為了測試方便範例程式碼只需要1分鐘內執行兩次回應為Ok(ShouldHandle 設定為 response.StatusCode 200 )以上就會觸發斷路器的 Closed,做一個簡單畫面進行觀察。

  • 原本正常不啟動斷路

CircuitBreaker_Demo

  • 進行觸發斷路系統會拋出 BrokenCircuitException

CircuitBreaker_Demo

  • 查詢斷路狀態為 Open

CircuitBreaker_Open

  • 恢復成手動點擊關閉斷路或是等待BreakDuration時間過去

CircuitBreaker_Demo

  • 手動點擊斷路器,則狀態變更為 Isolated

CircuitBreaker_Close

  • 在狀態為 Isolated,發送請求系統會拋出 IsolatedCircuitException

CircuitBreaker_IsolatedCircuitException

範例程式碼

關於上面的程式碼也提供在個人Github上面有完整的範例程式碼