Skip to content
Go back

用 Microsoft.Extensions.Resilience 构建弹性 ASP.NET Core API

生产环境里,每一个你调用的外部接口迟早都会出问题——数据库超时、第三方服务返回 503、网络抖动 200ms、下游微服务正在部署……这些不是极端情况,而是常态。

区别在于:应用是直接崩,还是优雅地恢复。

过去在 .NET 里实现弹性,通常要手动集成 Polly。它能用,但配置繁琐。现在 .NET 官方提供了第一方库:Microsoft.Extensions.Resilience,基于 Polly v8 构建,与 HttpClientFactory、依赖注入、结构化日志和 OpenTelemetry 原生集成。

这篇文章逐一介绍六种弹性策略,并给出生产环境推荐的管道组合。

安装依赖

在 .NET 10 项目中运行:

dotnet add package Microsoft.Extensions.Http.Resilience
dotnet add package Microsoft.Extensions.Resilience

弹性管道是什么

核心概念:把多个弹性策略串成一条管道,每个策略负责一种故障场景,按你定义的顺序依次执行。

ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new RetryStrategyOptions
    {
        ShouldHandle = new PredicateBuilder().Handle<ConflictException>(),
        Delay = TimeSpan.FromSeconds(2),
        MaxRetryAttempts = 3,
        BackoffType = DelayBackoffType.Exponential,
        UseJitter = true
    })
    .AddTimeout(new TimeoutStrategyOptions
    {
        Timeout = TimeSpan.FromSeconds(5)
    })
    .Build();

await pipeline.ExecuteAsync(
    async ct => await httpClient.GetAsync("/api/weather", ct),
    cancellationToken);

如果 HTTP 调用抛出 ConflictException,重试策略最多重试 3 次,使用指数退避加抖动。如果整个操作超过 5 秒,超时策略直接终止。

顺序很重要:外层策略先执行,内层策略在外层保护下运行。

重试策略

分布式系统里瞬时故障很常见——网络抖动、服务短暂过载。重试策略在失败后等一段时间再尝试,给系统恢复的机会。

builder.Services.AddResiliencePipeline("retry-pipeline", builder =>
{
    builder.AddRetry(new RetryStrategyOptions
    {
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromMilliseconds(300),
        BackoffType = DelayBackoffType.Exponential,
        ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>()
    });
});

这个配置最多重试 3 次,延迟依次是约 300ms、600ms、1200ms。

为什么用指数退避:如果服务过载,每 300ms 发一次请求只会让它更堵。退避给它喘息空间。

为什么用抖动(UseJitter = true:如果 50 个客户端都在完全相同的时间间隔重试,会形成”重试风暴”。加入随机偏移让请求分散开来。

超时策略

没有超时保护,一个永远不响应的外部服务会让线程一直挂着,消耗连接池,最终拖垮整个应用。

builder.Services.AddResiliencePipeline("timeout-pipeline", builder =>
{
    builder.AddTimeout(TimeSpan.FromSeconds(2));
});

外部服务 2 秒没响应,操作以 TimeoutRejectedException 失败。

重要的顺序规则:超时和重试组合时,超时应该是内层策略(后加入)。这样每次尝试都有独立的超时,超时后重试策略可以继续下一次尝试。

熔断策略

熔断器是我在生产中用得最顺手的策略之一。没有熔断器,应用会持续向已经崩溃的服务发请求,白白浪费资源,甚至级联失败。

builder.Services.AddResiliencePipeline("cb-pipeline", builder =>
{
    builder.AddCircuitBreaker(new CircuitBreakerStrategyOptions
    {
        FailureRatio = 0.5,
        MinimumThroughput = 10,
        SamplingDuration = TimeSpan.FromSeconds(30),
        BreakDuration = TimeSpan.FromSeconds(15)
    });
});

工作方式:

任何调用外部 API 的服务都应该加这个。

对冲策略

对冲(Hedging)用于对延迟敏感的场景。与其等一个慢响应,不如并行发出多个请求,谁先返回用谁。

builder.Services.AddResiliencePipeline<string, string>("gh-hedging", builder =>
{
    builder.AddHedging(new HedgingStrategyOptions<string>
    {
        MaxHedgedAttempts = 3,
        DelayGenerator = args =>
        {
            var delay = args.AttemptNumber switch
            {
                0 or 1 => TimeSpan.Zero,        // 并行模式
                _ => TimeSpan.FromSeconds(-1)    // 降级模式
            };
            return new ValueTask<TimeSpan>(delay);
        }
    });
});

这个配置最多发 4 个请求(1 个主请求 + 3 个对冲),前两个并行,后续的作为降级备用。第一个成功的结果胜出,其余请求取消。

在有多个服务副本的场景下效果特别好——可以把对冲请求路由到不同实例。

降级策略

降级是最后一道防线。与其返回 500 错误让用户看到崩溃页面,不如返回缓存数据或默认值。

builder.Services.AddResiliencePipeline<string, string?>("gh-fallback", builder =>
{
    builder.AddFallback(new FallbackStrategyOptions<string?>
    {
        FallbackAction = _ =>
            Outcome.FromResultAsValueTask<string?>(string.Empty)
    });
});

实际项目里,这里通常返回缓存数据而不是空字符串。核心意图是:用户得到一个降级结果,而不是报错。

限流策略

调用有速率限制的第三方 API 时,最好在本地先做限流,避免被对方返回 429。

builder.Services.AddResiliencePipeline("ratelimiter-pipeline", builder =>
{
    builder.AddRateLimiter(new SlidingWindowRateLimiter(
        new SlidingWindowRateLimiterOptions
        {
            PermitLimit = 100,
            SegmentsPerWindow = 4,
            Window = TimeSpan.FromMinutes(1)
        }
    ));
});

每分钟最多 100 个请求,窗口分为 4 段(每段 15 秒),流量分布更平滑。

在端点里使用管道

正式项目里不需要每次手动构建管道——注册一次,通过 ResiliencePipelineProvider 按名称取用。

app.MapGet("/subscribers", async (
    HttpClient httpClient,
    ResiliencePipelineProvider<string> pipelineProvider,
    CancellationToken cancellationToken) =>
{
    var pipeline = pipelineProvider.GetPipeline<Subscriber?>("gh-fallback");

    return await pipeline.ExecuteAsync(
        async token =>
            await httpClient.GetFromJsonAsync<Subscriber>("api/subscribers", token),
        cancellationToken);
});

AddResiliencePipeline("gh-fallback", ...) 注册,用 GetPipeline<T>() 获取,干净、可测试、整个应用可复用。

生产推荐管道

跑过多个项目后,这是我对每个外部 HTTP 调用的最低配置:

builder.Services.AddResiliencePipeline("production-pipeline", builder =>
{
    builder
        .AddRetry(new RetryStrategyOptions
        {
            MaxRetryAttempts = 3,
            Delay = TimeSpan.FromMilliseconds(500),
            BackoffType = DelayBackoffType.Exponential,
            UseJitter = true,
            ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>()
        })
        .AddCircuitBreaker(new CircuitBreakerStrategyOptions
        {
            FailureRatio = 0.5,
            MinimumThroughput = 10,
            SamplingDuration = TimeSpan.FromSeconds(30),
            BreakDuration = TimeSpan.FromSeconds(15)
        })
        .AddTimeout(TimeSpan.FromSeconds(5));
});

重试 → 熔断 → 超时,这是每个外部调用的最低标准:

在此基础上,按需加入对冲和降级。

小结

Microsoft.Extensions.Resilience 把生产级弹性能力直接集成进了 .NET 的依赖注入和 HTTP 基础设施,不需要从头搭建。六种策略各司其职,可以自由组合。

从重试 + 熔断 + 超时开始,根据具体服务的特性调整阈值配置,然后上线。

参考


Tags


Previous

在 C# 中构建 VS Code 风格的扩展系统

Next

C# 插件架构测试策略:让可扩展系统经得起考验