mcpField
D
dotnet-resilience

dotnet-resilience

github.com/wshaddix开发8683 次安装35 周前更新
代码仓库

关于 dotnet-resilience

dotnet-resilience 是面向 .NET 开发者的 Polly v8 与现代弹性模式技能,介绍 HTTP 和非 HTTP 调用的弹性管道配置。内容涵盖依赖注入、遥测以及从旧版 HTTP Polly 包迁移。

主要功能

  1. 01HTTP 标准弹性管道
  2. 02重试、超时与断路器策略
  3. 03自定义 Polly v8 管道
  4. 04通过依赖注入注册管道
  5. 05弹性指标与日志遥测
  6. 06迁移旧版 HTTP Polly

适用场景

  1. 01为 HTTP 客户端配置重试与超时
  2. 02为数据库调用构建弹性管道
  3. 03调整断路器及重试参数
  4. 04将旧项目迁移至现代弹性包

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-resilience

安装后在 Claude Code 中输入/dotnet-resilience确认已加载。

详细介绍

dotnet-resilience

使用 Polly v8 和 Microsoft.Extensions.Http.Resilience 为 .NET 应用实现现代弹性模式。涵盖标准弹性管道(速率限制器、总超时、重试、断路器、尝试超时)、自定义管道配置,以及与 .NET 依赖注入系统的集成。

已被取代的包: Microsoft.Extensions.Http.Polly 已被 Microsoft.Extensions.Http.Resilience 取代。新项目请勿使用 Microsoft.Extensions.Http.Polly。有关升级现有代码,请参阅迁移指南。

不在本文范围内: DI 容器机制和服务生命周期——请参阅 [skill:dotnet-csharp-dependency-injection]。Async/await 模式和取消令牌传播——请参阅 [skill:dotnet-csharp-async-patterns]。HTTP 客户端工厂模式(类型化客户端、命名客户端、DelegatingHandlers)详见 [skill:dotnet-http-client]。有关弹性策略测试,请参阅 [skill:dotnet-integration-testing],了解如何使用 WebApplicationFactory 进行测试;以及 [skill:dotnet-xunit],了解如何对弹性处理程序进行单元测试。

交叉引用:[skill:dotnet-csharp-dependency-injection] 介绍服务注册,[skill:dotnet-csharp-async-patterns] 介绍取消令牌传播,[skill:dotnet-http-client] 介绍如何将弹性应用于 HTTP 客户端。


包概览

包状态用途
Polly (v8+)当前核心弹性库——策略、管道、遥测
Microsoft.Extensions.Resilience当前为非 HTTP 弹性管道提供 DI 集成
Microsoft.Extensions.Http.Resilience当前为 IHttpClientFactory 弹性管道提供 DI 集成
Microsoft.Extensions.Http.Polly已被取代旧版 HTTP 弹性方案——迁移到 Microsoft.Extensions.Http.Resilience
Polly (v7 and earlier)旧版较旧的 API——迁移到 v8

安装现代技术栈:

<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.*" />

仅用于非 HTTP 场景:

<PackageReference Include="Microsoft.Extensions.Resilience" Version="9.*" />

标准弹性管道

Microsoft.Extensions.Http.Resilience 提供遵循推荐顺序的标准弹性管道。管道层从最外层到最内层执行:

Request
  --> Rate Limiter        (1. shed excess load)
    --> Total Timeout      (2. cap total wall-clock time)
      --> Retry             (3. retry transient failures)
        --> Circuit Breaker  (4. stop calling failing services)
          --> Attempt Timeout (5. cap individual attempt time)
            --> HTTP call
为什么顺序很重要
  • 速率限制器位于最前:防止重试风暴使下游服务不堪重负
  • 总超时包裹重试:确保整个操作(包括所有重试)都有截止时间
  • 重试包裹断路器:断路器复位后可再次尝试;断路状态会被视为可重试的故障
  • 断路器包裹尝试超时:超时的尝试会计入断路器的故障阈值
  • 尝试超时位于最内层:每次独立的 HTTP 调用都有自己的截止时间
使用默认值的标准管道
builder.Services
    .AddHttpClient("catalog-api", client =>
    {
        client.BaseAddress = new Uri("https://catalog.internal");
    })
    .AddStandardResilienceHandler();

此配置会应用采用合理默认值的标准管道:

  • 速率限制器:1000 个并发请求
  • 总超时:30 秒
  • 重试:3 次尝试,指数退避(基数为 2 秒),加入抖动
  • 断路器:10% 故障率,100 个样本,5 秒断路时长
  • 尝试超时:10 秒
使用自定义选项的标准管道
builder.Services
    .AddHttpClient("catalog-api", client =>
    {
        client.BaseAddress = new Uri("https://catalog.internal");
    })
    .AddStandardResilienceHandler(options =>
    {
        // Total timeout for the entire operation including retries
        options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(60);

        // Retry strategy
        options.Retry.MaxRetryAttempts = 5;
        options.Retry.Delay = TimeSpan.FromSeconds(1);
        options.Retry.BackoffType = DelayBackoffType.Exponential;
        options.Retry.UseJitter = true;
        options.Retry.ShouldHandle = args => ValueTask.FromResult(
            args.Outcome.Result?.StatusCode is
                HttpStatusCode.RequestTimeout or
                HttpStatusCode.TooManyRequests or
                >= HttpStatusCode.InternalServerError);

        // Circuit breaker
        options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
        options.CircuitBreaker.FailureRatio = 0.1;
        options.CircuitBreaker.MinimumThroughput = 20;
        options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(10);

        // Per-attempt timeout
        options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);
    });
通过 appsettings.json 配置

从配置中绑定弹性选项,以便针对不同环境进行调整:

builder.Services
    .AddHttpClient("catalog-api", client =>
    {
        client.BaseAddress = new Uri("https://catalog.internal");
    })
    .AddStandardResilienceHandler(options =>
    {
        builder.Configuration
            .GetSection("Resilience:CatalogApi")
            .Bind(options);
    });
{
  "Resilience": {
    "CatalogApi": {
      "Retry": {
        "MaxRetryAttempts": 5,
        "Delay": "00:00:02",
        "BackoffType": "Exponential"
      },
      "CircuitBreaker": {
        "BreakDuration": "00:00:15"
      },
      "TotalRequestTimeout": {
        "Timeout": "00:01:00"
      }
    }
  }
}

自定义弹性管道

如果标准管道不适用,可直接使用 Polly v8 构建自定义管道。

重试策略
builder.Services.AddResiliencePipeline("db-retry", pipelineBuilder =>
{
    pipelineBuilder.AddRetry(new RetryStrategyOptions
    {
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromMilliseconds(500),
        BackoffType = DelayBackoffType.Exponential,
        UseJitter = true,
        ShouldHandle = new PredicateBuilder()
            .Handle<DbUpdateConcurrencyException>()
            .Handle<TimeoutException>(),
        OnRetry = args =>
        {
            // Structured logging of retry attempts
            var logger = args.Context.Properties
                .GetValue(new ResiliencePropertyKey<ILogger>("logger"), null!);
            logger?.LogWarning(
                args.Outcome.Exception,
                "Retry attempt {AttemptNumber} after {Delay}ms",
                args.AttemptNumber,
                args.RetryDelay.TotalMilliseconds);
            return ValueTask.CompletedTask;
        }
    });
});

// Inject and use
public sealed class OrderRepository(
    [FromKeyedServices("db-retry")] ResiliencePipeline pipeline,
    AppDbContext db)
{
    public async Task<Order> UpdateAsync(Order order, CancellationToken ct)
    {
        return await pipeline.ExecuteAsync(async token =>
        {
            db.Orders.Update(order);
            await db.SaveChangesAsync(token);
            return order;
        }, ct);
    }
}
断路器策略
builder.Services.AddResiliencePipeline("payment-gateway", pipelineBuilder =>
{
    pipelineBuilder.AddCircuitBreaker(new CircuitBreakerStrategyOptions
    {
        SamplingDuration = TimeSpan.FromSeconds(30),
        FailureRatio = 0.25,           // Open after 25% failure rate
        MinimumThroughput = 10,        // Need at least 10 calls to evaluate
        BreakDuration = TimeSpan.FromSeconds(15),
        ShouldHandle = new PredicateBuilder()
            .Handle<HttpRequestException>()
            .Handle<TimeoutException>()
    });
});
超时策略
builder.Services.AddResiliencePipeline("external-api", pipelineBuilder =>
{
    // Total timeout for the entire pipeline execution
    pipelineBuilder.AddTimeout(new TimeoutStrategyOptions
    {
        Timeout = TimeSpan.FromSeconds(30),
        OnTimeout = args =>
        {
            // Log timeout details for diagnostics
            return ValueTask.CompletedTask;
        }
    });
});
组合多个策略

通过链式添加策略构建复合管道。顺序很重要——最外层的策略最先添加:

builder.Services.AddResiliencePipeline("composed", pipelineBuilder =>
{
    // 1. Total timeout (outermost -- caps entire operation)
    pipelineBuilder.AddTimeout(new TimeoutStrategyOptions
    {
        Timeout = TimeSpan.FromSeconds(45)
    });

    // 2. Retry (retries on transient failures)
    pipelineBuilder.AddRetry(new RetryStrategyOptions
    {
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromSeconds(1),
        BackoffType = DelayBackoffType.Exponential,
        UseJitter = true,
        ShouldHandle = new PredicateBuilder()
            .Handle<HttpRequestException>()
            .Handle<TimeoutException>()
    });

    // 3. Circuit breaker (stops calling failing services)
    pipelineBuilder.AddCircuitBreaker(new CircuitBreakerStrategyOptions
    {
        FailureRatio = 0.1,
        MinimumThroughput = 20,
        SamplingDuration = TimeSpan.FromSeconds(30),
        BreakDuration = TimeSpan.FromSeconds(10),
        ShouldHandle = new PredicateBuilder()
            .Handle<HttpRequestException>()
            .Handle<TimeoutException>()
    });

    // 4. Attempt timeout (innermost -- caps single attempt)
    pipelineBuilder.AddTimeout(new TimeoutStrategyOptions
    {
        Timeout = TimeSpan.FromSeconds(10)
    });
});

类型化弹性管道

对于会返回结果的操作,请使用 ResiliencePipeline<T>:

builder.Services.AddResiliencePipeline<string, HttpResponseMessage>(
    "typed-http",
    pipelineBuilder =>
    {
        pipelineBuilder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
        {
            MaxRetryAttempts = 3,
            Delay = TimeSpan.FromSeconds(1),
            BackoffType = DelayBackoffType.Exponential,
            ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
                .Handle<HttpRequestException>()
                .HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError)
        });
    });

对冲策略

并行发送请求以降低尾部延迟。对冲策略会在初始请求耗时较长时,分派额外的尝试:

builder.Services
    .AddHttpClient("search-api")
    .AddStandardHedgingHandler(options =>
    {
        options.Hedging.MaxHedgedAttempts = 2;
        options.Hedging.Delay = TimeSpan.FromMilliseconds(500);
        // Hedging sends a parallel request if the first hasn't
        // responded within 500ms
    });

适合使用对冲的情况:

  • 操作是幂等的(GET 请求、只读查询)
  • 降低尾部延迟比增加负载更重要

不适合使用对冲的情况:

  • 操作会产生副作用(POST、PUT、DELETE)
  • 下游服务无法承受增加的负载

遥测与可观测性

使用 DI 集成时,Polly v8 开箱即用地通过 System.Diagnostics 发出指标和跟踪信息。

内置指标

Microsoft.Extensions.Resilience 包会自动报告:

指标描述
polly.strategy.attempt.duration每次尝试的耗时
polly.strategy.pipeline.duration整个管道执行的耗时
polly.strategy.attempt.count尝试次数(包括重试)

在应用程序中配置 OpenTelemetry SDK 后,这些指标会自动与 OpenTelemetry 集成——收集器设置请参阅 [skill:dotnet-observability]。

启用遥测

使用基于 DI 的注册(AddResiliencePipeline、AddStandardResilienceHandler)时,会自动启用弹性遥测。Microsoft.Extensions.Resilience 包会注册 MeteringEnricher 和 LoggingEnricher,通过 DI 提供的标准 ILoggerFactory 和 IMeterFactory 发出结构化日志和指标:

// Telemetry is automatic -- no extra configuration needed.
// Structured logs appear via ILogger; metrics via IMeter.
builder.Services
    .AddHttpClient("catalog-api")
    .AddStandardResilienceHandler();

// To see resilience logs, set the Polly category to Information:
// appsettings.json
// {
//   "Logging": {
//     "LogLevel": {
//       "Polly": "Information"
//     }
//   }
// }

从 Microsoft.Extensions.Http.Polly 迁移

如果从已被取代的 Microsoft.Extensions.Http.Polly 包升级:

之前(旧版)
// Using Microsoft.Extensions.Http.Polly (superseded)
builder.Services
    .AddHttpClient("catalog-api")
    .AddTransientHttpErrorPolicy(p =>
        p.WaitAndRetryAsync(3, attempt =>
            TimeSpan.FromSeconds(Math.Pow(2, attempt))))
    .AddTransientHttpErrorPolicy(p =>
        p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
之后(新版)
// Using Microsoft.Extensions.Http.Resilience (current)
builder.Services
    .AddHttpClient("catalog-api")
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.MaxRetryAttempts = 3;
        options.Retry.Delay = TimeSpan.FromSeconds(2);
        options.Retry.BackoffType = DelayBackoffType.Exponential;

        options.CircuitBreaker.MinimumThroughput = 5;
        options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(30);
    });
迁移步骤
  1. 将 Microsoft.Extensions.Http.Polly NuGet 引用替换为 Microsoft.Extensions.Http.Resilience
  2. 将 AddTransientHttpErrorPolicy 调用替换为 AddStandardResilienceHandler 或自定义管道
  3. 将 Polly v7 策略配置转换为 v8 策略选项
  4. 移除显式的 IAsyncPolicy<HttpResponseMessage> 类型引用
  5. 确认重试/断路器行为与之前的配置一致

核心原则

  • HTTP 使用标准管道 -- AddStandardResilienceHandler() 以经过实战检验的默认值覆盖最常见的情况
  • 通过选项自定义,而非编写自定义代码 -- 标准处理程序选项涵盖重试次数、退避、断路器阈值和超时
  • 非 HTTP 场景使用 AddResiliencePipeline -- 数据库调用、消息队列、文件 I/O
  • 始终为重试添加抖动 -- 避免多个客户端同时重试时发生惊群效应
  • 通过 appsettings.json 配置 -- 不同环境需要不同的阈值;避免在生产环境中硬编码值
  • 不要抑制 TimeoutRejectedException -- 让它沿管道传播,以便外层策略(重试、断路器)作出响应
  • 新项目不要使用 Microsoft.Extensions.Http.Polly -- 它封装的是 Polly v7,现已被取代

Agent 注意事项

  1. 不要混用 Polly v7 和 v8 API -- v8 使用 ResiliencePipeline 和策略选项;v7 使用 IAsyncPolicy。两者不能互换。
  2. 不要重复添加 AddStandardResilienceHandler -- 它会组合完整的管道;添加两次会使每个策略层都重复执行。
  3. 不要将单次尝试超时设得高于总超时 -- 单次尝试超时必须短于总超时,否则不会生效。
  4. 不要重试非幂等操作 -- 只重试可安全重复的操作(GET 请求、带幂等键的幂等写入)。
  5. 生产环境不要使用硬编码的重试延迟 -- 始终设置 UseJitter = true,并通过 appsettings 配置延迟。

References

评论

支持 Markdown