.NET Core 3.0深入原始碼理解HttpClientFactory之實戰
寫在前面
前面兩篇文章透過原始碼角度,理解了HttpClientFactory的內部實現,當我們在專案中使用時,總會涉及以下幾個問題:
- HttpClient超時處理以及重試機制
- HttpClient熔斷器模式的實現
- HttpClient日誌記錄與追蹤鏈
接下來我們將從使用角度對上述問題作出說明。
詳細介紹
以下程式碼參考了MSDN,因為程式碼裡展示的GitHub介面確實可以調通,省的我再寫一個接口出來測試了。
HttpClient超時處理和重試機制
在此之前,我們需要了解一下Polly這個庫,Polly是一款基於.NET的彈性及瞬間錯誤處理庫, 它允許開發人員以順暢及執行緒安全的方式執行重試(Retry),斷路器(Circuit),超時(Timeout),隔板隔離(Bulkhead Isolation)及後背策略(Fallback)。
以下程式碼描述了在.NET Core 3.0中如何使用超時機制。
1: Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10))
那麼如何將其註冊到對應的HttpClient例項呢,有很多種方式:
- 通過AddPolicyHandler註冊
1: services.AddHttpClient("github", c =>
2: {
3: c.BaseAddress = new Uri("https://api.github.com/");
4:
5: c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
6: c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
7: }).AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10)));
- 宣告Policy註冊物件,並將超時策略物件新增進去
1: var registry = services.AddPolicyRegistry();
2: var timeout = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10));
3: registry.Add("regular", timeout);
呼叫方式
1: services.AddHttpClient("github", c =>
2: {
3: c.BaseAddress = new Uri("https://api.github.com/");
4:
5: c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // GitHub API versioning
6: c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // GitHub requires a user-agent
7: }).AddPolicyHandlerFromRegistry("regular")
Polly重試也很簡單
1: var policyRegistry = services.AddPolicyRegistry();
2:
3: policyRegistry.Add("MyHttpRetry",HttpPolicyExtensions.HandleTransientHttpError().WaitAndRetryAsync(3
,retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)
)));
這裡的重試設定是在第一次呼叫失敗後,還會有三次機會繼續重試,每個請求的時間間隔是指數級延遲。
重試功能除了可以使用Polly實現外,還可以使用DelegatingHandler,DelegatingHandler繼承自HttpMessageHandler,用於”處理請求、響應回覆“,本質上就是一組HttpMessageHandler的有序組合,可以視為是一個“雙向管道”。
此處主要展示DelegatingHandler的使用方式,在實際使用中,仍然建議使用Polly重試。
1: private class RetryHandler : DelegatingHandler
2: {
3: public int RetryCount { get; set; } = 5;
4:
5: protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
6: {
7: for (var i = 0; i < RetryCount; i++)
8: {
9: try
10: {
11: return await base.SendAsync(request, cancellationToken);
12: }
13: catch (HttpRequestException) when (i == RetryCount - 1)
14: {
15: throw;
16: }
17: catch (HttpRequestException)
18: {
19: // 五十毫秒後重試
20: await Task.Delay(TimeSpan.FromMilliseconds(50));
21: }
22: }
23: }
24: }
註冊方式如下:
1: services.AddHttpClient("github", c =>
2: {
3: c.BaseAddress = new Uri("https://api.github.com/");
4:
5: c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // GitHub API versioning
6: c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // GitHub requires a user-agent
7: })
8: .AddHttpMessageHandler(() => new RetryHandler());
HttpClient熔斷器模式的實現
如果非常瞭解Polly庫的使用,那麼熔斷器模式的實現也會非常簡單,
1: var policyRegistry = services.AddPolicyRegistry();
2:
3: policyRegistry.Add("MyCircuitBreaker",HttpPolicyExtensions.HandleTransientHttpError().CircuitBreakerAsync(handledEventsAllowedBeforeBreaking: 10,durationOfBreak: TimeSpan.FromSeconds(30)));
這裡的熔斷器設定規則是在連續10次請求失敗後,會暫停30秒。這個地方可以寫個擴充套件方法註冊到IServiceCollection中。
HttpClient日誌記錄與追蹤鏈
日誌記錄這塊與追蹤鏈,我們一般會通過request.Header實現,而在微服務中,十分關注相關呼叫方的資訊及其獲取,一般的做法是通過增加請求Id的方式來確定請求及其相關日誌資訊。
實現思路是增加一個DelegatingHandler例項,用以記錄相關的日誌以及請求鏈路
1: public class TraceEntryHandler : DelegatingHandler
2: {
3: private TraceEntry TraceEntry { get; set; }
4:
5: public TraceEntryHandler(TraceEntry traceEntry)
6: {
7: this.TraceEntry = traceEntry;
8: }
9:
10: protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
11: {
12: request.Headers.TryAddWithoutValidation("X-TRACE-CID", this.TraceEntry.ClientId);
13: request.Headers.TryAddWithoutValidation("X-TRACE-RID", this.TraceEntry.RequestId);
14: request.Headers.TryAddWithoutValidation("X-TRACE-SID", this.TraceEntry.SessionId);
15: if (this.TraceEntry.SourceIP.IsNullOrEmpty())
16: {
17: request.Headers.TryAddWithoutValidation("X-TRACE-IP", this.TraceEntry.SourceIP);
18: }
19:
20: if (this.TraceEntry.SourceUserAgent.IsNullOrEmpty())
21: {
22: request.Headers.TryAddWithoutValidation("X-TRACE-UA", this.TraceEntry.SourceUserAgent);
23: }
24:
25: if (this.TraceEntry.UserId.IsNullOrEmpty())
26: {
27: request.Headers.TryAddWithoutValidation("X-TRACE-UID", this.TraceEntry.UserId);
28: }
29:
30: return base.SendAsync(request, cancellationToken);
31: }
32: }
我在查詢相關資料的時候,發現有個老外使用CorrelationId元件實現,作為一種實現方式,我決定要展示一下,供大家選擇:
1: public class CorrelationIdDelegatingHandler : DelegatingHandler
2: {
3: private readonly ICorrelationContextAccessor correlationContextAccessor;
4: private readonly IOptions<CorrelationIdOptions> options;
5:
6: public CorrelationIdDelegatingHandler(
7: ICorrelationContextAccessor correlationContextAccessor,
8: IOptions<CorrelationIdOptions> options)
9: {
10: this.correlationContextAccessor = correlationContextAccessor;
11: this.options = options;
12: }
13:
14: protected override Task<HttpResponseMessage> SendAsync(
15: HttpRequestMessage request,
16: CancellationToken cancellationToken)
17: {
18: if (!request.Headers.Contains(this.options.Value.Header))
19: {
20: request.Headers.Add(this.options.Value.Header, correlationContextAccessor.CorrelationContext.CorrelationId);
21: }
22:
23: // Else the header has already been added due to a retry.
24:
25: return base.SendAsync(request, cancellationToken);
26: }
27: }
參考連結:
https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.0
https://rehansaeed.com/optimally-configuring-asp-net-core-httpclientfact