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 = "当前远程服务不可用,请稍后重试"
});
边界与限制
不要给所有远程调用统一套一层重试
不同接口的幂等性、时效性和副作用不同。越是"统一一刀切",越容易把问题隐藏掉。
超时和重试通常要一起考虑
如果没有超时保护,重试前的单次等待就可能已经拖垮调用链。如果只有超时没有重试,很多短暂抖动又会直接暴露给业务。
熔断阈值要偏保守
熔断不是越敏感越好。阈值太小容易误伤正常流量,阈值太大又起不到保护作用。
常见问题
为什么加了重试,接口还是经常失败
优先检查:
- 失败是不是持续性故障,而不是偶发抖动
- 当前接口是否根本不适合重试
- 重试次数、间隔和超时时间是否过小
为什么加了重试后,问题反而更严重
最常见原因是:
- 非幂等接口被重复调用
- 没有限流和熔断,所有实例同时重试
- 对端已经很慢,又被重试进一步放大压力
为什么明明 HTTP 是 200,策略还是应该触发
因为很多系统把业务失败包在正常 HTTP 响应里。这种情况下应该用 OrResult(...) 判断业务结果,而不是只看状态码。