跳到主要内容
版本:3.0.0

Polly 容错扩展

解决什么问题

Aegis.Extensions.Polly 是调用侧按需引入的容错扩展包,用来辅助处理远程调用中的超时、重试、熔断和降级。它不需要 Component.deps.json,不需要服务注册,在调用代码或 HttpClient 注册处显式挂策略即可。

如何引入

NuGet 包Aegis.Extensions.Polly

注册方式:不需要 Component.deps.json,也不需要 Services / Middlewares。直接在调用代码中使用。

常用命名空间:

using System.Net;
using Polly;
using Polly.CircuitBreaker;
using Polly.Timeout;

先做一个判断:不是所有接口都应该重试

在加策略前,先判断调用是否具备幂等性。

适合重试:

  • 查询类接口
  • 幂等更新
  • 带唯一业务单号的提交

不适合盲目重试:

  • 重复提交会产生副作用的创建接口
  • 第三方没有幂等保障的扣费、发药、库存扣减接口

常用策略怎么选

策略解决什么问题最常见用法
重试偶发异常、短暂网络抖动查询、幂等写入
超时对端太慢、连接迟迟不返回外部 HTTP 或第三方接口
熔断连续失败后快速失败,避免雪崩不稳定依赖服务
降级全部策略都失败后返回替代结果非核心结果、兜底提示

使用示例

只做重试

var retryPolicy = Policy
.Handle<HttpRequestException>()
.Or<TimeoutRejectedException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

先超时,再重试

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(5);

var retryPolicy = Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.Or<TimeoutRejectedException>()
.OrResult(response => !response.IsSuccessStatusCode)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

var policyWrap = Policy.WrapAsync(retryPolicy, timeoutPolicy);

超时 + 重试 + 熔断 + 降级

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(5);

var retryPolicy = Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.Or<TimeoutRejectedException>()
.OrResult(response => !response.IsSuccessStatusCode)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

var circuitBreakerPolicy = Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.Or<TimeoutRejectedException>()
.OrResult(response => !response.IsSuccessStatusCode)
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 3,
durationOfBreak: TimeSpan.FromSeconds(10));

var fallbackPolicy = Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.Or<TimeoutRejectedException>()
.Or<BrokenCircuitException>()
.OrResult(response => !response.IsSuccessStatusCode)
.FallbackAsync(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)
{
Content = new StringContent("{\"message\":\"fallback\"}")
});

var policyWrap = Policy.WrapAsync(
fallbackPolicy,
retryPolicy,
timeoutPolicy,
circuitBreakerPolicy);

和 HttpClient 一起用

通过 HttpClientFactory 把策略直接挂到对应客户端:

services.AddHttpClient("RemoteApi", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddPolicyHandler(Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(response => !response.IsSuccessStatusCode)
.WaitAndRetryAsync(
3,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))));

这种方式最适合一个下游系统对应一个固定客户端,策略跟客户端配置放在一起管理。

按业务结果触发策略

很多时候接口返回 200 但业务结果失败,可以用 OrResult(...) 把业务失败也纳入触发条件:

var retryPolicy = Policy<Result>
.Handle<HttpRequestException>()
.OrResult(result => result.Status != 200)
.WaitAndRetryAsync(
3,
retryAttempt => TimeSpan.FromSeconds(retryAttempt * 2));

使用降级时要明确的事

降级不是"吞掉错误",而是"明确给上游一个兜底结果":

  • 返回一个可识别的失败对象
  • 返回缓存或默认数据
  • 记录告警后转人工补偿
var fallbackPolicy = Policy<Result>
.Handle<Exception>()
.OrResult(result => result.Status != 200)
.FallbackAsync(new Result
{
Status = 503,
Message = "当前远程服务不可用,请稍后重试"
});

边界与限制

不要给所有远程调用统一套一层重试

不同接口的幂等性、时效性和副作用不同。越是"统一一刀切",越容易把问题隐藏掉。

超时和重试通常要一起考虑

如果没有超时保护,重试前的单次等待就可能已经拖垮调用链。如果只有超时没有重试,很多短暂抖动又会直接暴露给业务。

熔断阈值要偏保守

熔断不是越敏感越好。阈值太小容易误伤正常流量,阈值太大又起不到保护作用。

常见问题

为什么加了重试,接口还是经常失败

优先检查:

  1. 失败是不是持续性故障,而不是偶发抖动
  2. 当前接口是否根本不适合重试
  3. 重试次数、间隔和超时时间是否过小

为什么加了重试后,问题反而更严重

最常见原因是:

  • 非幂等接口被重复调用
  • 没有限流和熔断,所有实例同时重试
  • 对端已经很慢,又被重试进一步放大压力

为什么明明 HTTP 是 200,策略还是应该触发

因为很多系统把业务失败包在正常 HTTP 响应里。这种情况下应该用 OrResult(...) 判断业务结果,而不是只看状态码。

配合阅读