关于 resilience
面向 .NET 开发者的弹性处理指南,基于 Polly v8 与 .NET 内置限流功能,讲解 HTTP 和非 HTTP 调用的策略配置。重点包括超时、重试、熔断、遥测及避免不安全重试。
主要功能
- 01Polly v8 弹性管道
- 02HTTP 客户端标准与自定义处理器
- 03重试、熔断、超时和回退
- 04对延迟敏感读取的并行请求
- 05遥测与内置限流
适用场景
- 01为外部 HTTP API 配置弹性处理
- 02为数据库或消息队列注册命名管道
- 03为延迟敏感读取设置并行请求
- 04监控熔断器并限制 API 请求速率
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill resilience安装后在 Claude Code 中输入/resilience确认已加载。
详细介绍
弹性
核心原则
- 使用 Polly v8 弹性管道,而非 v7 策略 — Polly v8 用
ResiliencePipeline替代了Policy。切勿使用PolicyBuilder、Policy.Handle<>()或ISyncPolicy。新 API 可组合、类型安全,并与IHttpClientFactory原生集成。 - 通过
AddResilienceHandler配置,而非手动包装 — 对于 HTTP 调用,请使用Microsoft.Extensions.Http.Resilience,它会通过 DI 将管道直接添加到HttpClient。无需手动使用ExecuteAsync包装。 - 组合策略,而非嵌套策略 — 单个
ResiliencePipeline可以串联重试、断路器和超时策略。策略按由外向内的顺序执行(最先添加的位于最外层)。无需嵌套try/catch或手动编排。 - 始终设置超时 — 每次外部调用都需要设置超时。使用 Polly 的
AddTimeout()作为最内层策略,使其按尝试次数生效;也可以选择添加外层超时,以限制总耗时。 - 全面埋点 — Polly v8 会发出
Metering事件,并支持通过TelemetryOptions集成 OpenTelemetry。利用这些功能监控重试率、断路器状态和超时频率。
模式
HTTP 客户端弹性(推荐默认配置)
// Program.cs — Standard resilience handler covers 90% of use cases
builder.Services.AddHttpClient<IPaymentGateway, PaymentGatewayClient>(client =>
{
client.BaseAddress = new Uri("https://api.payments.example.com");
})
.AddStandardResilienceHandler(); // Retry + circuit breaker + timeout out of the box
// That's it. The standard handler configures:
// - Retry: 3 attempts, exponential backoff, jitter
// - Circuit breaker: 10% failure ratio over 30s sampling, 30s break
// - Attempt timeout: 10s per attempt
// - Total request timeout: 30s
原因:Microsoft.Extensions.Http.Resilience 中的 AddStandardResilienceHandler() 会应用适用于生产环境的默认配置。只有在需要不同阈值时才进行覆盖。
自定义 HTTP 弹性配置
builder.Services.AddHttpClient<ICatalogService, CatalogServiceClient>(client =>
{
client.BaseAddress = new Uri("https://api.catalog.example.com");
})
.AddResilienceHandler("catalog", builder =>
{
// Total timeout — outermost, caps total elapsed time
builder.AddTimeout(TimeSpan.FromSeconds(15));
// Retry — exponential backoff with jitter
builder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
Delay = TimeSpan.FromMilliseconds(500),
ShouldHandle = static args => ValueTask.FromResult(
args.Outcome.Result?.StatusCode is HttpStatusCode.RequestTimeout
or HttpStatusCode.TooManyRequests
or HttpStatusCode.ServiceUnavailable
|| args.Outcome.Exception is HttpRequestException)
});
// Circuit breaker — prevent cascading failures
builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(10),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(30)
});
// Per-attempt timeout — innermost
builder.AddTimeout(TimeSpan.FromSeconds(5));
});
原因:命名弹性处理程序允许你针对每项服务进行调整。顺序很重要:总超时 > 重试 > 断路器 > 单次尝试超时。
非 HTTP 弹性管道
// For database calls, message queues, or any non-HTTP operation
builder.Services.AddResiliencePipeline("database", builder =>
{
builder
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
Delay = TimeSpan.FromMilliseconds(200),
ShouldHandle = new PredicateBuilder()
.Handle<TimeoutException>()
.Handle<InvalidOperationException>(ex =>
ex.Message.Contains("deadlock", StringComparison.OrdinalIgnoreCase))
})
.AddTimeout(TimeSpan.FromSeconds(10));
});
// Inject and use
public sealed class OrderRepository(
AppDbContext db,
[FromKeyedServices("database")] ResiliencePipeline pipeline)
{
public async Task<Order?> GetByIdAsync(Guid id, CancellationToken ct)
{
return await pipeline.ExecuteAsync(
async token => await db.Orders.FindAsync([id], token),
ct);
}
}
原因:AddResiliencePipeline 会在 DI 中注册命名管道。通过 [FromKeyedServices] 注入,可让代码更整洁且易于测试。
类型化弹性管道
// When the operation returns a specific type, use ResiliencePipeline<T>
builder.Services.AddResiliencePipeline<string, HttpResponseMessage>("external-api", builder =>
{
builder
.AddFallback(new FallbackStrategyOptions<HttpResponseMessage>
{
FallbackAction = static args =>
{
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("{\"status\":\"degraded\",\"data\":[]}")
};
return Outcome.FromResultAsValueTask(response);
},
ShouldHandle = static args => ValueTask.FromResult(
args.Outcome.Exception is not null
|| args.Outcome.Result?.IsSuccessStatusCode == false)
})
.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
MaxRetryAttempts = 2,
Delay = TimeSpan.FromMilliseconds(500)
})
.AddTimeout(TimeSpan.FromSeconds(5));
});
原因:类型化管道允许你添加回退策略,在所有重试耗尽后返回默认值,这对实现平稳降级至关重要。
对冲(并行请求)
builder.Services.AddHttpClient<ISearchService, SearchServiceClient>()
.AddResilienceHandler("search-hedging", builder =>
{
builder.AddHedging(new HttpHedgingStrategyOptions
{
MaxHedgedAttempts = 2,
Delay = TimeSpan.FromMilliseconds(500) // Send parallel request after 500ms
});
builder.AddTimeout(TimeSpan.FromSeconds(3));
});
原因:如果第一个请求在延迟时间内仍未响应,对冲会发送并行请求。适用于对延迟敏感且可以容忍重复工作的读取操作。
遥测集成
builder.Services.AddResiliencePipeline("monitored", (builder, context) =>
{
// Polly v8 emits metrics via System.Diagnostics.Metrics automatically.
// ConfigureTelemetry wires structured logging for strategy events.
builder
.ConfigureTelemetry(new TelemetryOptions
{
LoggerFactory = context.ServiceProvider.GetRequiredService<ILoggerFactory>()
})
.AddRetry(new RetryStrategyOptions { MaxRetryAttempts = 3 })
.AddCircuitBreaker(new CircuitBreakerStrategyOptions())
.AddTimeout(TimeSpan.FromSeconds(10));
});
// In Program.cs — wire up OpenTelemetry to capture Polly metrics
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics.AddMeter("Polly"));
速率限制(.NET 内置)
.NET 通过 AddRateLimiter() 提供内置速率限制中间件,无需外部包。算法包括:AddFixedWindowLimiter、AddSlidingWindowLimiter、AddTokenBucketLimiter、AddConcurrencyLimiter。
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromSeconds(60);
opt.QueueLimit = 0;
});
// Always return ProblemDetails with Retry-After on 429
options.OnRejected = async (context, ct) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
context.HttpContext.Response.Headers.RetryAfter =
((int)retryAfter.TotalSeconds).ToString();
await context.HttpContext.Response.WriteAsJsonAsync(
new ProblemDetails { Title = "Too many requests", Status = 429 }, ct);
};
});
app.UseRateLimiter();
app.MapGet("/api/orders", ListOrders).RequireRateLimiting("fixed");
反模式
错误:使用 Polly v7 API
// BAD — v7 policy syntax, do not use
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));
var response = await retryPolicy.ExecuteAsync(() => httpClient.GetAsync("/api/data"));
正确:使用 Polly v8 弹性管道
// GOOD — v8 pipeline via DI
builder.Services.AddHttpClient<IDataService, DataServiceClient>()
.AddStandardResilienceHandler();
错误:手动包装每次调用
// BAD — manual resilience per call site
public async Task<Order> GetOrderAsync(Guid id)
{
try
{
return await _pipeline.ExecuteAsync(async ct =>
await _httpClient.GetFromJsonAsync<Order>($"/orders/{id}", ct));
}
catch (TimeoutRejectedException)
{
return Order.Empty;
}
catch (BrokenCircuitException)
{
return Order.Empty;
}
}
正确:通过 HttpClient DI 让管道处理一切
// GOOD — resilience is configured at the HttpClient level
public async Task<Order?> GetOrderAsync(Guid id, CancellationToken ct)
{
var response = await _httpClient.GetAsync($"/orders/{id}", ct);
if (!response.IsSuccessStatusCode) return null;
return await response.Content.ReadFromJsonAsync<Order>(ct);
}
错误:对非幂等操作进行重试
// BAD — retrying a POST that creates a resource risks duplicates
builder.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 5 // This will create 5 orders on transient failures!
});
正确:仅重试幂等操作,或使用幂等键
// GOOD — use idempotency key header for non-idempotent operations
builder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
ShouldHandle = static args => ValueTask.FromResult(
args.Outcome.Result?.StatusCode is HttpStatusCode.RequestTimeout
or HttpStatusCode.ServiceUnavailable)
});
// Pair with idempotency key in the request
httpClient.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());
错误:未监控的断路器
// BAD — circuit breaker with no visibility into state changes
builder.AddCircuitBreaker(new CircuitBreakerStrategyOptions());
// How do you know when it trips? You don't.
正确:带遥测的断路器
// GOOD — Polly v8 metrics captured via OpenTelemetry
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics.AddMeter("Polly"));
// Dashboard alerts on: polly.circuit_breaker.state = Open
决策指南
| 场景 | 策略 | 配置 |
|---|---|---|
| 调用外部 API 的 HTTP 请求 | AddStandardResilienceHandler() | 使用默认值,仅覆盖特定阈值 |
| 使用自定义阈值的 HTTP 请求 | AddResilienceHandler("name", ...) | 使用命名处理程序,按服务调整 |
| 数据库 / EF Core 调用 | AddResiliencePipeline("db", ...) | 遇到死锁 / 超时时重试,不使用断路器 |
| 发布消息队列 | AddResiliencePipeline("mq", ...) | 使用指数退避重试,并设置超时 |
| 对延迟敏感的读取操作 | AddHedging(...) | 超过延迟阈值后发起并行请求 |
| 优雅降级 | AddFallback(...) | 全部失败时返回缓存值 / 默认值 |
| 单次尝试的时间限制 | 将 AddTimeout(...) 放在最内层 | 根据操作设置为 2-10 秒 |
| 整个操作的时间限制 | 将 AddTimeout(...) 放在最外层 | 所有重试时长之和 + 缓冲时间 |
| 非幂等写入 | 使用幂等键重试 | 或不重试,快速失败 |
| 读取密集型微服务 | 标准处理程序 + 对冲 | 通过冗余降低延迟 |
| API 速率限制 | AddRateLimiter() + RequireRateLimiting() | 为每个端点设置固定、滑动或令牌桶限制 |
评论
支持 Markdown