1. 程式人生 > >.NET Core 3.0深入原始碼理解HttpClientFactory之實戰

.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