使用 IHttpClientFactory 实现复原 HTTP 请求
Polly 是一个 .NET 库,提供恢复能力和瞬态故障处理功能。 通过应用 Polly 策略(如重试、断路、隔离、超时和回退)即可实现这些功能。
断路:当系统出错的次数超过了指定的阈值,就要中断当前线路,等待一段时间后再继续。
隔离:将可控的操作限制在一个固定大小的资源池中,以隔离有潜在可能相互影响的操作。
HttpClient类实现 IDisposable
,但在 using
语句中声明和实例化它并非首选操作,因为释放 HttpClient
对象时,基础套接字不会立即释放,这可能会导致套接字耗尽问题。 在长期运行的进程中使用 HttpClient
的共享实例时,开发人员会遇到另一个问题。 在将 HttpClient 实例化为单一实例或静态对象的情况下,它无法处理 DNS 更改,问题实际上不是 HttpClient
本身,而是 HttpClient 的默认构造函数,因为它创建了一个新的实际 HttpMessageHandler 实例,该实例具有上面提到的“套接字耗尽”和 DNS 更改问题 。
要解决上述问题并使 HttpClient
实例可管理,.NET Core 2.1 引入了 IHttpClientFactory 接口,该接口可用于在应用中通过依赖关系注入 (DI) 来配置和创建 HttpClient
实例。 它还提供基于 Polly 的中间件的扩展,以利用 HttpClient 中的委托处理程序。
使用 IHttpClientFactory 的好处
同时实现 IHttpMessageHandlerFactory 的 IHttpClientFactory 当前实现具有以下优势:
- 提供一个中心位置,用于命名和配置逻辑
HttpClient
对象。 例如,可以配置预配置的客户端(服务代理)以访问特定微服务。 - 通过后列方式整理出站中间件的概念:在
HttpClient
中委托处理程序并实现基于 Polly 的中间件以利用 Polly 的复原策略。 HttpClient
已经具有委托处理程序的概念,这些委托处理程序可以链接在一起,处理出站 HTTP 请求。 将 HTTP 客户端注册到工厂后,可使用一个 Polly 处理程序将 Polly 策略用于重试、断路器等。- 管理 HttpMessageHandler 的生存期,避免在自行管理
HttpClient
生存期时出现上述问题。
由于关联的 HttpMessageHandler
由工厂管理,因此可安全释放由 DI 注入的 HttpClient
实例。 事实上,注入的 HttpClient
实例是从 DI 角度区分范围的 。
IHttpClientFactory
(DefaultHttpClientFactory
) 实现与 Microsoft.Extensions.DependencyInjection
NuGet 包中的 DI 实现紧密关联。
结合使用类型化客户端和 IHttpClientFactory
什么是“类型化客户端”? 它只是为某些特定用途预配置的 HttpClient
。 此配置可以包括特定值,如基本服务器、HTTP 标头或超时。
安装包含 IServiceCollection 的 AddHttpClient 扩展方法的 Microsoft.Extensions.Http
NuGet 包,在应用程序中添加 IHttpClientFactory。 此扩展方法用于注册内部 DefaultHttpClientFactory
类,后者用作接口 IHttpClientFactory
的单一实例。 它定义 HttpMessageHandlerBuilder 的临时配置。 此消息处理程序(HttpMessageHandler 对象)获取自池,可供从工厂返回的 HttpClient
使用。
池中的 HttpMessageHandler
对象的生存期就是池中的 HttpMessageHandler
实例可重用的时间长度。 默认值为两分钟,但可基于每个类型化客户端重写此值。 要重写该值,请在创建客户端时在返回的 IHttpClientBuilder 上调用 SetHandlerLifetime()
,如以下代码所示:
//Set 5 min as the lifetime for the HttpMessageHandler objects in the pool used for the Catalog Typed Client services.AddHttpClient<ICatalogService, CatalogService>() .SetHandlerLifetime(TimeSpan.FromMinutes(5));
还可以在注册中添加特定于实例的配置(例如,配置基址),并添加一些弹性策略,
services.AddHttpClient<ICatalogService, CatalogService>(client => { client.BaseAddress = new Uri(Configuration["BaseUrl"]); }) .AddPolicyHandler(GetRetryPolicy()) .AddPolicyHandler(GetCircuitBreakerPolicy()); static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy() { return HttpPolicyExtensions .HandleTransientHttpError() .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound) .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); }
GetRetryPolicy 用到了Microsoft.Extensions.Http.Polly 扩展包
IHttpClientFactory允许你用命名的方式来配置和使用 HttpClient。
services.AddHttpClient("GitHub", client => { client.BaseAddress = new Uri("https://api.github.com/"); client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); }) .AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10) })); public class MyController : Controller { private readonly IHttpClientFactory _httpClientFactory; public MyController(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } public Task<IActionResult> SomeAction() { var client = _httpClientFactory.CreateClient("GitHub"); return Ok(await client.GetStringAsync("/someapi")); } }
AddTransientHttpErrorPolicy方法也可以从Polly的一个扩展包Polly.Extensions.Http中得到
Polly还提供了策略注册池,它相当于策略的存储中心,被注册的策略可以让你在应用程序的多个位置重用。AddPolicyHandler的一个重载方法允许您从注册池中选择策略。
var registry = services.AddPolicyRegistry(); registry.Add("defaultretrystrategy", HttpPolicyExtensions.HandleTransientHttpError().WaitAndRetryAsync(/* etc */)); registry.Add("defaultcircuitbreaker", HttpPolicyExtensions.HandleTransientHttpError().CircuitBreakerAsync(/* etc */)); services.AddHttpClient(/* etc */) .AddPolicyHandlerFromRegistry("defaultretrystrategy") .AddPolicyHandlerFromRegistry("defaultcircuitbreaker");
将抖动策略添加到重试策略
在高并发率、高可伸缩性和高争用的情况下,常规重试策略可能会对系统产生影响。 在部分运行中断的情况下,有可能会有许多客户端同时发出相似的重试操作,从而形成操作高峰,为克服这种情况,一个好办法是向重试算法或策略中添加抖动策略。 由于增加了指数退避的随机性,这可能会改进端到端系统的整体性能。 这样在出现问题时可以分散峰值。
Random jitterer = new Random(); var retryWithJitterPolicy = HttpPolicyExtensions .HandleTransientHttpError() .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound) .WaitAndRetryAsync(6, // exponential back-off plus some jitter retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) + TimeSpan.FromMilliseconds(jitterer.Next(0, 100)) );