×

.NET 8 弹性管道:让系统“扛得住”的秘密

独孤求败 独孤求败 发表于2026-03-11 14:48:42 浏览39 评论0

抢沙发发表评论

在很多关于站点“弹性(Resilience)”伸缩的文章里,内容往往停留在“加个重试、配个熔断器”这个层面。但真正跑在生产环境中的系统,就没有这么简单了。

真实环境里的故障通常是相互叠加的:网络抖动引发超时,超时又触发重试,重试进一步放大下游压力,最终把本来还能撑住的服务直接拖垮。更糟的是,一个配置不当的全局熔断器,甚至可能成为新的单点故障。

.NET 8 引入的 Resilience Pipelines(弹性管道),并不是对 Polly 的简单“换壳升级”,而是一种全新的容错执行模型。它的目标不是“多试几次”,而是让 ASP.NET Core 应用在面对复杂失败模式时,具备可组合、可控制、可观测的系统级防御能力。


弹性管道的执行模型

从本质上看,弹性管道是一条分层包裹的执行链。每一层都在“真正的业务调用”之外增加了一层保护,而这些层的顺序,远比参数本身更重要。

一个官方推荐、也更符合生产实践的顺序如下:

请求
 ↓
对冲(Hedging)
 ↓
超时(Timeout)
 ↓
重试(Retry)
 ↓
熔断器(Circuit Breaker)
 ↓
实际操作

顺序不对,效果可能天差地别。举个很常见的例子:如果你把重试放在最外层、超时放在里面,那么一次请求在最坏情况下,可能会把每次重试都“完整跑完”,总耗时轻松飙到好几秒。反过来,如果超时在外层,整体执行时间被牢牢控制住,多余的重试会被直接掐掉。

合理的顺序,背后其实是明确的设计意图:对冲要尽早启动,用来对付延迟尖刺;超时用来约束整体执行窗口;重试只处理瞬时失败;而熔断器则基于最终结果,决定是否要暂时“拉闸保护”。


构建受控的弹性管道

下面这段代码,是一个可以直接用在生产环境里的配置示例

builder.Services.AddResiliencePipeline<HttpResponseMessage>(
    "advanced-pipeline",
    pipeline =>
    {
        pipeline
            .AddHedging(new HedgingStrategyOptions<HttpResponseMessage>
            {
                MaxHedgedAttempts = 2,
                Delay = TimeSpan.FromMilliseconds(150)
            })
            .AddTimeout(TimeSpan.FromSeconds(2))
            .AddRetry(new RetryStrategyOptions<HttpResponseMessage>
            {
                MaxRetryAttempts = 2,
                ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
                    .Handle<HttpRequestException>()
                    .HandleResult(r => !r.IsSuccessStatusCode)
            })
            .AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
            {
                FailureRatio = 0.4,
                MinimumThroughput = 20,
                SamplingDuration = TimeSpan.FromSeconds(30),
                BreakDuration = TimeSpan.FromSeconds(10)
            });
    });

这条管道在运行时会做几件非常关键的事情:当响应时间突然拉长时,自动并行发起备用请求;无论发生什么情况,总执行时间都被严格限制;遇到网络异常或 5xx 响应会自动重试;而一旦失败率持续升高,熔断器会果断介入,保护下游服务不被打垮。


关键进阶特性

按键隔离(Keyed Pipelines):真正的租户级容错

很多系统在早期都会犯一个错误:使用全局熔断器。结果就是,一个租户把服务打挂,所有租户一起陪葬。

.NET 8 提供的按键管道,让你可以按“租户、用户、业务维度”维护完全独立的弹性状态

builder.Services.AddResiliencePipeline<string, HttpResponseMessage>(
    "tenant-pipeline",
    (context, pipeline) =>
    {
        var tenantId = context;
        pipeline.AddCircuitBreaker(new CircuitBreakerStrategyOptions
        {
            FailureRatio = tenantId == "premium" ? 0.7 : 0.3,
            BreakDuration = TimeSpan.FromSeconds(10)
        });
    });

// 使用
await resiliencePipeline.ExecuteAsync(tenantId, async token => 
    await httpClient.GetAsync(url, token));

这样一来,高级租户可以享受更高的失败容忍度,普通租户也不会被“误伤”。而且这一切都发生在服务内部,不依赖 API 网关,架构上非常干净。


每请求动态策略:让弹性具备“业务感知”

并不是所有请求都适合同一套策略。POST 请求通常不应该重试;管理员操作可能需要禁用对冲;健康检查也不该触发熔断。

通过在 ResilienceContext 中传递元数据,可以做到按请求调整行为

var context = new ResilienceContext();
context.Properties["AllowRetry"] = false;

await pipeline.ExecuteAsync(context, async token =>
    await httpClient.PostAsync(url, content, token));

在策略中读取这些信息:

ShouldHandle = args =>
{
    if (args.Context.Properties.TryGetValue("AllowRetry"out var allow) && 
        allow is false)
        return ValueTask.FromResult(false);
    return ValueTask.FromResult(true);
};

这一步的意义非常大:弹性策略不再是“无脑重试”,而是开始理解业务语义。


自定义弹性策略:不再受限于内置能力

当内置策略无法覆盖你的需求时,弹性管道允许你扩展到框架级能力

public sealedclassDeploymentBlockStrategy : ResilienceStrategy
{
    protectedoverrideasync ValueTask<TResult> ExecuteCore<TResult>(
        Func<ResilienceContext, ValueTask<TResult>> callback,
        ResilienceContext context)
    {
        if (DeploymentState.IsInProgress)
            thrownew InvalidOperationException("部署进行中,禁止流量");
        returnawait callback(context);
    }
}

// 注册
pipeline.AddStrategy(new DeploymentBlockStrategy());

这种策略非常适合和发布系统、安全审计、灰度开关结合,而不是在中间件里东拼西凑。


可靠的故障测试

弹性代码最大的痛点之一就是难以测试。好消息是,.NET 8 提供了可预测、可验证的执行模型:

var attempts = 0;
var pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new RetryStrategyOptions { MaxRetryAttempts = 3 })
    .Build();

await Assert.ThrowsAsync<HttpRequestException>(() =>
    pipeline.ExecuteAsync(async _ =>
    {
        attempts++;
        throw new HttpRequestException();
    }));

Assert.Equal(4, attempts);

这种测试方式,可以非常精确地验证重试次数、熔断状态变化,以及超时是否按预期触发。


有效的可观测性

在生产环境里,仅仅打印“发生了一次重试”几乎没有价值。真正值得关注的,是这些指标:重试放大因子、熔断持续时间、对冲请求的浪费率,以及超时和主动取消的比例。

通过事件钩子,你可以把这些数据完整地打出来:

pipeline.OnRetry(args =>
{
    logger.LogWarning(
        "第 {Attempt} 次重试,原因:{Reason}",
        args.AttemptNumber,
        args.Outcome.Exception?.Message);
});

这类数据,才是真正用来判断系统健康度的依据。


常见反模式(真实生产教训)

在实际项目中,下面这些坑几乎都会遇到:对非幂等请求重试,导致重复下单;重试间隔设置得比用户耐心还长;一个全局熔断器拖垮所有租户;对写操作启用对冲,引发数据不一致;以及超时配置比 SLA 还长,形同虚设。


弹性管道如何改变架构决策

当弹性策略真正落到服务内部之后,你会发现很多架构决策都发生了变化。API 网关可以更轻量;后台队列不再是“救命稻草”;部分故障下的恢复速度明显提升;SLO 和尾延迟也更容易被控制。

这已经不只是一个库的升级,而是分布式系统设计思路的进化


结语

.NET 8 的 Resilience Pipelines 最大的价值,在于三点:可组合、上下文感知、可扩展。大多数人只用了它最表层的能力,但真正稳定、抗压的系统,往往构建在那被忽视的 90% 之上。

当你开始认真使用这些特性时,你做的已经不只是“写接口”,而是在打造一个具备自我修复能力的弹性系统。


群贤毕至

访客