Polly Retry - 传递所有执行直到重试成功

Polly Retry - Pass all execution until a retry is successful

目前,Polly 重试策略独立地淘汰所有失败的请求。因此,如果有 10 个请求失败并且我设置了永远重试策略,那么每次重试发生时它都会再发送 10 个请求,服务器将永远无法恢复。

如何异步传递所有失败的请求并只重试一个请求并在重试成功后恢复正常流程?

我不能(不想)使用 Circuit Breaker,因为我的服务是后台工作者服务,而 Circuit Breaker 会破坏整个后台服务逻辑。

// Current code with only retry policy
var retry = HttpPolicyExtensions.HandleTransientHttpError().WaitAndRetryForeverAsync(retryNo => new TimeSpan(0, retryNo > 3 ? 10 : (retryNo * 2), 0));
builder.Services.AddHttpClient<TestClient>().AddPolicyHandler(retry);

用例:我编写了一个后台服务并不断抓取一个包含 30000 多个页面的网站。为了防止站点过载,我使用 SemaphoreSlim(或 Bulkhead)来限制在某个时间点发送到服务器的请求数。

不过,服务器有可能拒绝我的请求。那时,我需要重试 只有一个 失败的请求单元,服务器开始再次接受我的请求。由于我同时发送多个请求,Polly 正在重试所有失败的请求,这让服务器不高兴。

期望:

10 次请求失败 -> 重试 1 次请求(单元成功) -> 如果成功则重新发送剩余的 9 次请求。

问题

据我了解,您有一个 HttpClient,用于针对同一下游系统发出 N 个限速并发请求。

您想处理以下故障情况:

  • 如果出现暂时性网络问题,您想重试单个请求
  • 如果下游系统过载(因此大多数并发请求失败),那么您希望退后一步并仅使用单个请求来探测其健康状况

选项 A - 合并 CB 并重试

断路器策略作为代理。它跟踪传出的通信,如果连续失败太多,那么它会阻止进一步的请求。它通过抛出 BrokenCircuitException.

来缩短请求

一段时间后,CB 将允许单个请求向下游系统发出,如果成功,则允许所有传出通信,但如果失败,则将它们短路。 我已经详细说明了 CB 是如何工作的。

您可以调整重试策略以了解此异常。这意味着您的重试请求仍将发出,但不会离开您的应用程序域。幸运的是,在 Polly 中,您可以为一个策略定义多个触发器:

HttpPolicyExtensions
   .HandleTransientHttpError()
   .Or<BrokenCircuitException>()
   .WaitAndRetryForeverAsync(retryNo => new TimeSpan(0, retryNo > 3 ? 10 : (retryNo * 2), 0));

因此,它会触发 HttpRequestExceptionBrokenCircuitException。如果 HttpStatusCode 是 408 或 5xx,它也会触发。

现在剩下的就是将重试和熔断策略组合成一个弹性策略。您可以使用以下方法之一来做到这一点:

.AddPolicyHandler(retryPolicy.Wrap(cbPolicy))
//OR
.AddPolicyHandler(Policy.Wrap(retryPolicy, cbPolicy))

请注意顺序。重要的是将 cb 注册为内部策略并将重试注册为外部策略以便能够依赖 escalation. Here 我已经详细说明了这个确切的场景。

注意:如果您愿意,可以在断路器打开时使用不同的延迟。我知道 如何使用 Context 对象来做到这一点。


选项 B - 使用队列

如果应用程序没有崩溃,上述解决方案可以正常工作。如果确实如此,那么您必须从头开始整个处理过程。

如果您需要避免这种情况,那么您需要将您的工作项(待处理的 url)存储在某个地方。

我建议采用以下架构:

  • 您的主要工作人员不会向下游系统发出 http 请求,而是创建工作/工作项
    • 它可以将工作项存储在数据库或持久队列中
  • 有另一个工作人员从数据库或队列中获取作业并尝试执行请求
    • 如果请求成功,则会从持久存储中删除工作项
    • 如果请求失败,则它不会删除该项目,而是获取一个新项目
      • 根据您的要求,您可能需要删除该项目并将其推到队列的末尾<<重新排队
  • 获取逻辑可以知道断路器状态
    • 如果 CB 已关闭,则它会获取 N 个作业
    • 如果它是开放的,那么它只获取一个

使用此架构,您不需要明确的重试策略,因为您的 queue/database 会保留那些未成功的项目。因此,您的提取逻辑将检索相同的作业,直到它最终完成。

您可以通过创建一个 dead letter queue 来进一步扩展这个概念,您可以在其中存储那些失败 N 次的工作项。这样您的队列就不会被“永久”工作项污染。