关于 dotnet-resilience
dotnet-resilience 是面向 .NET 开发者的 Polly v8 与现代弹性模式技能,介绍 HTTP 和非 HTTP 调用的弹性管道配置。内容涵盖依赖注入、遥测以及从旧版 HTTP Polly 包迁移。
主要功能
- 01HTTP 标准弹性管道
- 02重试、超时与断路器策略
- 03自定义 Polly v8 管道
- 04通过依赖注入注册管道
- 05弹性指标与日志遥测
- 06迁移旧版 HTTP Polly
适用场景
- 01为 HTTP 客户端配置重试与超时
- 02为数据库调用构建弹性管道
- 03调整断路器及重试参数
- 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);
});
迁移步骤
- 将
Microsoft.Extensions.Http.PollyNuGet 引用替换为Microsoft.Extensions.Http.Resilience - 将
AddTransientHttpErrorPolicy调用替换为AddStandardResilienceHandler或自定义管道 - 将 Polly v7 策略配置转换为 v8 策略选项
- 移除显式的
IAsyncPolicy<HttpResponseMessage>类型引用 - 确认重试/断路器行为与之前的配置一致
核心原则
- HTTP 使用标准管道 --
AddStandardResilienceHandler()以经过实战检验的默认值覆盖最常见的情况 - 通过选项自定义,而非编写自定义代码 -- 标准处理程序选项涵盖重试次数、退避、断路器阈值和超时
- 非 HTTP 场景使用
AddResiliencePipeline-- 数据库调用、消息队列、文件 I/O - 始终为重试添加抖动 -- 避免多个客户端同时重试时发生惊群效应
- 通过 appsettings.json 配置 -- 不同环境需要不同的阈值;避免在生产环境中硬编码值
- 不要抑制
TimeoutRejectedException-- 让它沿管道传播,以便外层策略(重试、断路器)作出响应 - 新项目不要使用
Microsoft.Extensions.Http.Polly-- 它封装的是 Polly v7,现已被取代
Agent 注意事项
- 不要混用 Polly v7 和 v8 API -- v8 使用
ResiliencePipeline和策略选项;v7 使用IAsyncPolicy。两者不能互换。 - 不要重复添加
AddStandardResilienceHandler-- 它会组合完整的管道;添加两次会使每个策略层都重复执行。 - 不要将单次尝试超时设得高于总超时 -- 单次尝试超时必须短于总超时,否则不会生效。
- 不要重试非幂等操作 -- 只重试可安全重复的操作(GET 请求、带幂等键的幂等写入)。
- 生产环境不要使用硬编码的重试延迟 -- 始终设置
UseJitter = true,并通过 appsettings 配置延迟。