You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Polly v8与RestSharp构建弹性管道:解决Retry-After及断路器问题

基于RestSharp + Polly实现带Retry-After与全局断路器的高可用API客户端

针对你的需求,下面给出完整的实现方案,解决断路器无日志、Retry-After解析、全局熔断等问题,同时覆盖所有次要目标:

核心策略链设计

采用重试策略(带Retry-After解析+抖动)→ 断路器策略 → 日志与异常处理的顺序,确保:

  1. 重试时优先遵循服务端指定的Retry-After延迟
  2. 断路器状态全局共享,所有API调用共用同一熔断逻辑
  3. 日志与异常处理统一由策略接管

1. 统一日志与异常处理逻辑

将原LogAndRaiseErrors的逻辑整合到Polly策略中,自动将非2xx响应转为异常,同时记录全链路请求日志:

private static void LogAndValidateResponse<T>(ILogger logger, RestRequest request, RestResponse<T>? response)
{
    // 记录请求详情
    logger.LogInformation("发送请求:{Method} {Resource}, 参数: {@Parameters}", 
        request.Method, request.Resource, request.Parameters);

    if (response == null)
    {
        logger.LogWarning("请求无响应");
        throw new HttpRequestException("请求未收到响应");
    }

    // 记录响应详情
    logger.LogInformation("收到响应:状态码 {StatusCode}, 响应内容: {@Content}", 
        response.StatusCode, response.Content);

    // 非成功响应转为异常,供重试/断路器策略捕获
    if (!response.IsSuccessful)
    {
        throw new HttpRequestException($"请求失败,状态码:{response.StatusCode}", null, response.StatusCode);
    }
}

2. 实现带Retry-After与抖动的重试策略

优先解析响应头Retry-After的值,结合指数退避+抖动生成重试延迟,同时指定需要重试的异常类型:

var retryOptions = new RetryStrategyOptions
{
    // 触发重试的异常与响应规则
    ShouldHandle = new PredicateBuilder()
        .Handle<HttpRequestException>(ex => 
            ex.StatusCode is HttpStatusCode.TooManyRequests or HttpStatusCode.ServiceUnavailable or HttpStatusCode.GatewayTimeout)
        .Handle<TaskCanceledException>() // 超时异常
        .Handle<SocketException>(), // 网络故障
    // 生成重试延迟逻辑
    WaitDurationGenerator = args =>
    {
        var logger = args.Context.Properties.GetValue<ILogger>("Logger");
        TimeSpan delay;

        // 尝试读取Retry-After头
        if (args.Outcome.Result is RestResponse response)
        {
            if (response.Headers.TryGetFirst("Retry-After", out var retryAfterVal))
            {
                // 处理秒数格式的Retry-After
                if (int.TryParse(retryAfterVal.ToString(), out var seconds))
                {
                    delay = TimeSpan.FromSeconds(seconds);
                    logger?.LogInformation("服务端指定重试延迟:{Seconds}秒", seconds);
                }
                // 处理日期格式的Retry-After
                else if (DateTimeOffset.TryParse(retryAfterVal.ToString(), out var retryTime))
                {
                    delay = retryTime - DateTimeOffset.UtcNow;
                    logger?.LogInformation("服务端指定重试时间:{Time}, 延迟:{Delay:F2}秒", 
                        retryTime, delay.TotalSeconds);
                }
                // 解析失败时使用默认指数退避+抖动
                else
                {
                    delay = GetExponentialBackoffWithJitter(args.AttemptNumber);
                    logger?.LogWarning("无法解析Retry-After,使用默认延迟:{Delay:F2}秒", delay.TotalSeconds);
                }
            }
            else
            {
                delay = GetExponentialBackoffWithJitter(args.AttemptNumber);
                logger?.LogInformation("未找到Retry-After,使用指数退避延迟:{Delay:F2}秒", delay.TotalSeconds);
            }
        }
        else
        {
            delay = GetExponentialBackoffWithJitter(args.AttemptNumber);
            logger?.LogInformation("无响应,使用指数退避延迟:{Delay:F2}秒", delay.TotalSeconds);
        }

        return ValueTask.FromResult(delay);
    },
    MaxRetryAttempts = 5,
    // 记录重试事件
    OnRetry = args =>
    {
        var logger = args.Context.Properties.GetValue<ILogger>("Logger");
        logger?.LogWarning("第{Attempt}次重试,原因:{Reason}", 
            args.AttemptNumber + 1, args.Outcome.Exception?.Message ?? "非成功响应");
        return ValueTask.CompletedTask;
    }
};

// 带抖动的指数退避辅助方法
private static TimeSpan GetExponentialBackoffWithJitter(int attemptNumber)
{
    var baseDelay = TimeSpan.FromSeconds(Math.Pow(2, attemptNumber));
    var jitter = new Random().NextDouble() * baseDelay.TotalSeconds;
    return TimeSpan.FromSeconds(baseDelay.TotalSeconds + jitter);
}

3. 全局生效的断路器策略(解决无日志问题)

使用共享策略注册表确保断路器状态全局复用,同时订阅所有状态变化事件输出日志:

var circuitBreakerOptions = new CircuitBreakerStrategyOptions
{
    // 触发熔断条件:连续3次失败
    FailureRatio = 1.0,
    MinimumThroughput = 3,
    // 熔断时长(可根据业务调整)
    BreakDuration = TimeSpan.FromMinutes(1),
    // 触发断路器的异常规则
    ShouldHandle = new PredicateBuilder()
        .Handle<HttpRequestException>(ex => 
            ex.StatusCode is HttpStatusCode.TooManyRequests or HttpStatusCode.ServiceUnavailable or HttpStatusCode.GatewayTimeout)
        .Handle<TaskCanceledException>()
        .Handle<SocketException>(),
    // 断路器状态变化日志
    OnCircuitBreak = args =>
    {
        var logger = args.Context.Properties.GetValue<ILogger>("Logger");
        logger?.LogError("断路器已打开,{Seconds}秒后恢复", args.BreakDuration.TotalSeconds);
        return ValueTask.CompletedTask;
    },
    OnCircuitClosed = args =>
    {
        var logger = args.Context.Properties.GetValue<ILogger>("Logger");
        logger?.LogInformation("断路器已关闭,恢复正常请求");
        return ValueTask.CompletedTask;
    },
    OnCircuitHalfOpen = args =>
    {
        var logger = args.Context.Properties.GetValue<ILogger>("Logger");
        logger?.LogInformation("断路器进入半开状态,将尝试测试请求");
        return ValueTask.CompletedTask;
    }
};

4. 整合策略链并注册全局实例

// 全局策略注册表,确保断路器状态共享
var pipelineRegistry = new ResiliencePipelineRegistry();

// 注册组合策略:重试 → 断路器 → 日志处理
pipelineRegistry.TryAdd("ApiClientPipeline", builder =>
{
    builder
        .AddRetry(retryOptions)
        .AddCircuitBreaker(circuitBreakerOptions)
        .AddStrategy(context => new LoggingStrategy(context), "LoggingHandler");
});

// 自定义日志处理策略
public class LoggingStrategy : ResilienceStrategy
{
    private readonly ILogger _logger;

    public LoggingStrategy(ResilienceStrategyContext context)
    {
        _logger = context.Properties.GetValue<ILogger>("Logger") ?? throw new ArgumentNullException(nameof(ILogger));
    }

    protected override async ValueTask<Outcome<T>> ExecuteCore<T>(
        Func<ResilienceContext, ValueTask<Outcome<T>>> callback,
        ResilienceContext context,
        CancellationToken cancellationToken)
    {
        var outcome = await callback(context);

        // 处理RestSharp响应的日志与异常转换
        if (outcome.Result is RestResponse<T> response)
        {
            try
            {
                LogAndValidateResponse(_logger, response.Request, response);
                return outcome;
            }
            catch (Exception ex)
            {
                return Outcome.FromException<T>(ex);
            }
        }

        return outcome;
    }
}

5. 在RestSharp客户端中使用策略

public class ApiClient
{
    private readonly RestClient _client;
    private readonly ResiliencePipeline _pipeline;
    private readonly ILogger<ApiClient> _logger;

    public ApiClient(RestClient client, ResiliencePipelineRegistry pipelineRegistry, ILogger<ApiClient> logger)
    {
        _client = client;
        _pipeline = pipelineRegistry.GetPipeline("ApiClientPipeline");
        _logger = logger;
    }

    public async Task<TResponse> PostAsync<TRequest, TResponse>(string resource, TRequest requestBody)
    {
        var restRequest = new RestRequest(resource, Method.Post)
            .AddJsonBody(requestBody);

        // 传入日志实例到策略上下文
        var context = ResilienceContextPool.Shared.Get();
        try
        {
            context.Properties.Set("Logger", _logger);
            var response = await _pipeline.ExecuteAsync(async ctx =>
            {
                return await _client.ExecuteAsync<TResponse>(restRequest, ctx.CancellationToken);
            }, context);

            return response.Data!;
        }
        finally
        {
            ResilienceContextPool.Shared.Return(context);
        }
    }
}

断路器无日志问题的根源与解决

之前断路器无日志输出,通常是以下原因:

  1. 策略顺序错误:如果断路器放在重试策略外层,重试的失败不会被断路器统计,导致永远无法触发熔断。正确顺序是重试包裹断路器。
  2. 未订阅状态事件:没有配置OnCircuitBreak/OnCircuitClosed等回调,状态变化时无日志输出。上述代码已完整订阅所有事件。
  3. 触发条件未达标:比如失败次数未达到MinimumThroughput阈值,或失败比例未满足FailureRatio,可根据业务调整配置。

内容的提问来源于stack exchange,提问作者Myster

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 04:14:52