Polly策略应按重试→熔断→降级顺序组合,即Policy.WrapAsync(fallback, circuitBreaker, retry),确保重试先执行、熔断监控重试结果、降级兜底最终失败;需统一异常处理谓词、启用日志回调并单独标注Context。

熔断器在高并发下频繁触发,CircuitState 变成 Open 后请求全被拒绝
高并发时,下游服务响应变慢或超时增多,Polly 的熔断策略会快速累积失败计数,一旦达到 FailureThreshold 就跳闸。此时所有新请求都会立即抛出 BrokenCircuitException,连重试机会都没有。
关键点在于:熔断器默认不区分异常类型,HttpRequestException 和 TimeoutException 都算失败;但像 404、401 这类业务错误不该触发熔断。
- 用
HandleResult+() HttpStatusCode判断显式排除非致命 HTTP 状态码 - 把
SamplingDuration设得稍长(比如 30 秒),避免短时间毛刺导致误熔断 -
MinimumThroughput建议设为 20+,防止低流量下因偶然失败就开闸 - 启用
AutomaticTransition,让熔断器在HalfOpen状态自动试探,而不是靠定时器硬切
var circuitBreaker = Policy.HandleResult( r => !r.IsSuccessStatusCode && r.StatusCode is not (HttpStatusCode.NotFound or HttpStatusCode.Unauthorized)) .CircuitBreakerAsync( handledEventsAllowedBeforeBreaking: 10, durationOfBreak: TimeSpan.FromMinutes(1), samplingDuration: TimeSpan.FromSeconds(30), minimumThroughput: 20, automaticTransition: true);
重试策略在并发激增时引发雪崩,下游压力反而更大
多个线程/请求同时失败,若都按相同间隔重试(尤其是固定延迟),容易形成“重试风暴”,把本已吃紧的下游彻底压垮。
Polly 默认的 WaitAndRetryAsync 如果没加退避和抖动,就是典型风险点。比如 5 次重试全卡在 100ms,第 2 轮所有请求几乎同时砸过去。
- 必须用
WaitAndRetryAsync的指数退避重载,例如Backoff.DecorrelatedJitterBackoffV2 - 设置
maxRetryCount≤ 3,高并发场景下重试次数宁少勿多 - 对
TimeoutException和连接级异常优先重试,对 500 错误可考虑降级而非重试 - 结合
Context传递请求 ID,在日志里标记是否为重试请求,方便定位放大效应
var retryPolicy = Policy
.Handle()
.Or()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: (retryAttempt, context) =>
Backoff.DecorrelatedJitterBackoffV2(
medianFirstRetryDelay: TimeSpan.FromMilliseconds(100),
retryCount: 3)[retryAttempt]);
降级逻辑写在 FallbackAsync 里,但实际没生效
常见误区是把降级当成“兜底打印日志”或返回空对象,结果上游调用方没做 null 检查直接 NRE;或者降级函数本身也抛异常,导致 fallback 链路中断。
更隐蔽的问题是:fallback 执行时仍处于原始请求的 CancellationToken 生命周期内,如果原请求已超时,fallback 可能被取消——而你根本没意识到它没跑完。
- fallback 函数体内必须用
try/catch包住所有逻辑,尤其涉及 IO 或外部调用 - 不要在 fallback 里复用原请求的
CancellationToken,改用CancellationToken.None或独立超时控制 - 降级返回值需与主逻辑类型严格一致,避免隐式转换失败;必要时用
ResultSelector统一包装 - 对核心接口,降级建议返回缓存快照(如 Redis 中的
GetAsync("fallback:user:123")),而非硬编码默认值
var fallbackPolicy = Policy.Handle () .FallbackAsync( fallbackAction: async (ct) => { try { // 注意:这里用 CancellationToken.None,避免被上游超时干扰 return await _cache.GetStringAsync("fallback:config", CancellationToken.None) ?? "default_config"; } catch { return "fallback_failed"; // 真正的保底 } }, onFallbackAsync: (ex, ct) => Log.Warning(ex.Exception, "Fallback triggered"));
三种策略组合后执行顺序混乱,熔断器没等重试就提前介入
Polly 策略组合不是简单叠加,而是按 WrapAsync 的嵌套顺序执行:最外层策略最先拦截,最内层最后生效。如果把熔断器包在重试外面,那只要第一次失败就进熔断,重试根本不会发生。
正确顺序永远是:重试 → 熔断 → 降级。即重试策略要最靠近业务调用,熔断器监控重试后的整体成败,降级则兜住整个链路的最终失败。
- 用
Policy.WrapAsync(retry, circuitBreaker, fallback)是错的;必须是Policy.WrapAsync(fallback, circuitBreaker, retry) - 所有策略的异常/结果处理谓词必须对齐,比如重试只捕获网络异常,熔断器却统计所有异常,会导致状态不一致
- 调试时开启
onRetry/onBreak/onFallback回调,打日志确认各策略触发时机和上下文 - 高并发下建议给每个策略单独配
Context标签(如"retry-v1"),避免日志混在一起无法归因
真正容易被忽略的是:当重试策略内部抛出未被捕获的异常(比如自定义策略里忘了 await),整个 wrapper 会直接崩溃,熔断和降级全部失效——这种问题在线上只会在 CPU 突增时偶然暴露。










