mcpField
R
resilience

resilience

github.com/codewithmukesh开发7913.0k 次安装12 周前更新
代码仓库

关于 resilience

面向 .NET 开发者的弹性处理指南,基于 Polly v8 与 .NET 内置限流功能,讲解 HTTP 和非 HTTP 调用的策略配置。重点包括超时、重试、熔断、遥测及避免不安全重试。

主要功能

  1. 01Polly v8 弹性管道
  2. 02HTTP 客户端标准与自定义处理器
  3. 03重试、熔断、超时和回退
  4. 04对延迟敏感读取的并行请求
  5. 05遥测与内置限流

适用场景

  1. 01为外部 HTTP API 配置弹性处理
  2. 02为数据库或消息队列注册命名管道
  3. 03为延迟敏感读取设置并行请求
  4. 04监控熔断器并限制 API 请求速率

安装

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

$npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill resilience

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

详细介绍

弹性

核心原则

  1. 使用 Polly v8 弹性管道,而非 v7 策略 — Polly v8 用 ResiliencePipeline 替代了 Policy。切勿使用 PolicyBuilder、Policy.Handle<>() 或 ISyncPolicy。新 API 可组合、类型安全,并与 IHttpClientFactory 原生集成。
  2. 通过 AddResilienceHandler 配置,而非手动包装 — 对于 HTTP 调用,请使用 Microsoft.Extensions.Http.Resilience,它会通过 DI 将管道直接添加到 HttpClient。无需手动使用 ExecuteAsync 包装。
  3. 组合策略,而非嵌套策略 — 单个 ResiliencePipeline 可以串联重试、断路器和超时策略。策略按由外向内的顺序执行(最先添加的位于最外层)。无需嵌套 try/catch 或手动编排。
  4. 始终设置超时 — 每次外部调用都需要设置超时。使用 Polly 的 AddTimeout() 作为最内层策略,使其按尝试次数生效;也可以选择添加外层超时,以限制总耗时。
  5. 全面埋点 — 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