使用 Polly 揭秘 HTTP 超时并重试

Ham*_*med 1 c# timeout dotnet-httpclient .net-core polly

注册服务:

var host = new HostBuilder().ConfigureServices(services =>
{
    services.AddHttpClient<Downloader>(client =>
    {
        client.Timeout = TimeSpan.FromSeconds(1); // -- T1
    })
    .AddPolicyHandler(HttpPolicyExtensions
        .HandleTransientHttpError()
        .Or<HttpRequestException>()
        .WaitAndRetryAsync(Backoff.DecorrelatedJitterBackoffV2(
            TimeSpan.FromSeconds(5), // -- T2
            retryCount: 3)))
    .AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(10)) // -- T3
    .AddPolicyHandler(HttpPolicyExtensions
        .HandleTransientHttpError()
        .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30))); // -- T4

    services.AddTransient<Downloader>();

}).Build();
Run Code Online (Sandbox Code Playgroud)

实施Downloader

class Downloader
{
    private HttpClient _client;
    public Downloader(IHttpClientFactory factory)
    {
        _client = factory.CreateClient();
    }

    public void Download(List<Uri> links)
    {
        await Parallel.ForEachAsync(
            links, 
            async (link, _cancelationToken) =>
            {
                await _client.GetStreamAsync(uri, _cancellationToken);
            });
    }
}
Run Code Online (Sandbox Code Playgroud)

在此伪代码中,我对超时之间的相关性以及如何/何时重新提交 HTTP 请求感到困惑。具体来说:

  • T1T2T3、 和是如何T4“精心策划”的?我假设如果端点在 中没有响应T1await _client.GetStreamAsync抛出超时异常,那么在与 相关的时间间隔内T2,HTTP 请求将被提交最大3次数,或者如果断路器计时器达到T4。那么 的作用是什么呢T3

  • 所有配置是否都与客户端相关HttpMessageHandler,并且我仍然需要将调用包装GetStreamAsync为如下?!

Policy
    .Handle<Exception>()
    .RetryAsync(3)
    .ExecuteAsync(
        async () => await _client.GetStreamAsync(uri, _cancellationToken));
Run Code Online (Sandbox Code Playgroud)

Pet*_*ala 7

首先让我提供一些建议,然后讨论您的问题。

优于Wrap多个AddPolicyHandlers

寄存器AddPolicyHandleraPolicyHttpMessageHandler是 a DelegatingHandler。在您的情况下,您有 3DelegatingHandler秒,因此异常传播是由 ASP.NET Core 而不是 Polly 完成的。

如果您愿意Policy.WrapAsync,您可以以 Polly 方式链接策略(升级)。

var T2 = HttpPolicyExtensions
        .HandleTransientHttpError()
        .Or<HttpRequestException>()
        .WaitAndRetryAsync(Backoff.DecorrelatedJitterBackoffV2(
            TimeSpan.FromSeconds(5),
            retryCount: 3));

var T3 = Policy.TimeoutAsync<HttpResponseMessage>(10);

var T4 = HttpPolicyExtensions
        .HandleTransientHttpError()
        .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
Run Code Online (Sandbox Code Playgroud)
var resilienceStrategy = Policy.WrapAsync<HttpResponseMessage>(T2, T3, T4);
var host = new HostBuilder().ConfigureServices(services =>
{
    services
      .AddHttpClient<Downloader>(client =>
        client.Timeout = TimeSpan.FromSeconds(1))
      .AddPolicyHandler(resilienceStrategy);

    services.AddTransient<Downloader>();

}).Build();
Run Code Online (Sandbox Code Playgroud)

进一步建议

交换超时和断路器

顺便说一下,交换超时和断路器策略可能是有意义的。如果超时是最内部的,并且您将调整断路器策略以了解超时问题 ( .Or<TimeoutRejectedException>()),那么它也可能会因此而中断。

var T3 = Policy.TimeoutAsync<HttpResponseMessage>(10);

var T4 = HttpPolicyExtensions
        .HandleTransientHttpError()
        .Or<TimeoutRejectedException>()
        .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));

var resilienceStrategy = Policy.WrapAsync<HttpResponseMessage>(T2, T4, T3);
Run Code Online (Sandbox Code Playgroud)

更多情况下重试

在超时或断路的情况下执行重试也可能有意义

var T2 = HttpPolicyExtensions
        .HandleTransientHttpError()
        .Or<HttpRequestException>()
        .Or<TimeoutRejectedException>()
        .Or<BrokenCircuitException>()
        .WaitAndRetryAsync(Backoff.DecorrelatedJitterBackoffV2(
            TimeSpan.FromSeconds(5),
            retryCount: 3));
Run Code Online (Sandbox Code Playgroud)

回复问题#2

请允许我从你的第二个问题开始。

所有配置都与客户端和 HttpMessageHandler 相关吗,我仍然需要将调用包装GetStreamAsync为如下?!

不,你不需要这样做。由于您已经HttpClient用弹性策略装饰了您的策略,因此您不需要对每个HttpClient方法调用执行相同的操作。

回复问题#1

请允许我将其分成多个问题

T1、T2、T3、T4是如何“编排”的?

  • T1( HttpClients Timeout) 充当全局超时。这意味着如果您需要执行多次重试,那么它们将在 1 秒后被取消。因此,这是所有尝试的总体时间限制。
  • T2(重试退避medianFirstRetryDelay)以带有抖动的指数退避方式提供重试之间的睡眠持续时间序列。换句话说,每次重试之间不是总是等待相同的时间,而是每次等待的时间越来越长。
  • T3(Timeout)充当本地超时。换句话说,每次重试尝试都会休息并强制执行此超时
    • HttpClient这是全局Timeout超时的对比
    • 如果您将超时策略定义为最外层策略( 的最左边参数Policy.WrapAsync),那么它也充当全局策略
  • T4(断路器breakDuration)充当看门人。如果 CB 损坏(在您的情况下连续 5 次失败后),那么它会将自身转换为“打开”状态。
    • BrokenCircuitException当 CB 处于打开状态时,任何请求都会终止
    • 经过breakDuration一段时间后,它会转换为HalfOpen状态并允许单个探测。如果成功则 CB 返回Closed状态,否则返回Open

我假设如果端点在 T1 内没有响应,await _client.GetStreamAsync 会抛出超时异常,然后在与 T2 相关的时间间隔内,HTTP 请求将最多提交 3 次,或者如果断路器计时器达到 T4。那么T3的作用是什么?

通过上一点,我想我已经解决了这个问题:)。因为您已将全局超时设置为 1 秒,所以您的本地超时(10 秒)永远不会触发。

如果您为全局超时设置更高的值,则可能会触发每个请求(基于重试尝试)超时。


如果上述任何一点不清楚,请告诉我,我将链接我之前的一些帖子,其中详细讨论了这一点。


更新#1

您能详细说明一下 T1 与 T3 吗?

在以下主题中,我尝试阐明全局超时和本地超时之间的区别:

最后,这是一个 SO 主题,其中介绍了如何拥有比 HttpClient 的 Timeout 更长的 Timeout

我应该实现捕获 CB 的 Open、HalfOpen 和 Closed 状态并缓冲和保留状态请求的逻辑,还是 CB 在适当的时候在内部缓冲并重新提交请求?

断路器不是这样工作的。断路器不维护请求队列之类的东西。它只是一个代理,如果下游系统被视为暂时不可用,它可以缩短请求的执行。CB 本身不执行任何重试逻辑。

速率限制器策略也以同样的方式工作。在有足够的吞吐量之前,它不会保留请求。

您可以做的是创建 CB 感知重试逻辑并将它们组合起来