如何利用HttpClientFactory、Polly和幂等边界优化ASP.NET Core外部依赖调用?

摘要:订单服务最容易出现的稳定性问题,不是业务代码写错,而是下游支付、库存、短信网关一抖,整个接口成功率跟着雪崩。看起来只是一次超时,实际上会引发重试风暴、线程池占满、数据库回写积压。 今天我们讨论一个问题:如何把外部依赖调用链路收敛到可控、可观
订单服务最容易出现的稳定性问题,不是业务代码写错,而是下游支付、库存、短信网关一抖,整个接口成功率跟着雪崩。看起来只是一次超时,实际上会引发重试风暴、线程池占满、数据库回写积压。 今天我们讨论一个问题:如何把外部依赖调用链路收敛到可控、可观测、可恢复的状态。 1. 问题背景:服务没挂,为什么成功率先掉 线上经常出现这种现象: API 进程还活着,CPU 占用也不高,可能只有40%(举个例子,非真实数据)。 请求延迟从 200ms 拉到 6s。 失败率增多。 这类故障通常不是单点问题,大概就是遇到下面这些因素了: HttpClient 使用方式不当。 超时和重试没有预算控制,单次请求被放大成多次慢调用。 对写操作直接重试却没有幂等约束,最终引发重复扣款或重复下单。 对于有多年经验的开发者来说,这不是 API 会不会调的问题,而是如何在高并发场景下把失败控制在局部,不让故障扩散到整条链路。 2. 原理解析:连接、超时、重试和幂等为什么必须一起设计 2.1 连接管理决定了你能扛多久 每次请求都新建 HttpClient,会导致连接池无法稳定复用,遇到峰值时容易把端口和连接资源打爆。更隐蔽的问题是连接存活太久导致 DNS 变更不生效,流量继续打到旧节点。 IHttpClientFactory 的价值不是语法糖,而是把连接池生命周期交给 SocketsHttpHandler 管理。常见配置里至少要有: PooledConnectionLifetime:定期轮换连接,避免长期粘住旧地址。 MaxConnectionsPerServer:控制单机并发上限,避免瞬时过载。 2.2 超时预算必须先于重试策略 很多团队会先配“重试 3 次”,但没配总预算。结果是每次重试都等满超时,最后一个请求占用十几秒。 更稳妥的做法是分两层: 每次尝试超时(per-try timeout):避免单次卡死。 整体调用超时(overall timeout):限制整次业务调用的总耗时。 在 Resilience Pipeline 里,这两层要通过策略顺序明确表达: outer timeout 放在 retry 外层,控制整次调用预算。 inner timeout 放在 retry 内层,控制单次 attempt。 先定预算,再谈重试次数,否则重试是放大器,不是保护器。 2.3 遵循行业共识,重试只能处理瞬时故障,不能处理业务冲突 可重试的典型对象是:网络抖动、连接中断、429、部分 5xx。不可重试的是:参数错误、鉴权失败、业务规则冲突。 如果把所有非 200 都重试,会把本来可快速失败的请求拖成长尾,最终压垮线程池和连接池。 2.4 写操作重试前必须定义幂等边界 对 POST/PUT 这类有副作用的请求,重试不是默认安全动作。支付创建、库存扣减、优惠券核销这类写操作,必须先定义幂等键和幂等存储,再启用自动重试。 简单说: 没有幂等键,重试可能制造重复业务。 有幂等键但没有唯一约束,仍然可能并发写穿。
阅读全文