基于Polly v8与RestSharp构建弹性管道:解决Retry-After及断路器问题
基于RestSharp + Polly实现带Retry-After与全局断路器的高可用API客户端
针对你的需求,下面给出完整的实现方案,解决断路器无日志、Retry-After解析、全局熔断等问题,同时覆盖所有次要目标:
核心策略链设计
采用重试策略(带Retry-After解析+抖动)→ 断路器策略 → 日志与异常处理的顺序,确保:
- 重试时优先遵循服务端指定的Retry-After延迟
- 断路器状态全局共享,所有API调用共用同一熔断逻辑
- 日志与异常处理统一由策略接管
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); } } }
断路器无日志问题的根源与解决
之前断路器无日志输出,通常是以下原因:
- 策略顺序错误:如果断路器放在重试策略外层,重试的失败不会被断路器统计,导致永远无法触发熔断。正确顺序是重试包裹断路器。
- 未订阅状态事件:没有配置
OnCircuitBreak/OnCircuitClosed等回调,状态变化时无日志输出。上述代码已完整订阅所有事件。 - 触发条件未达标:比如失败次数未达到
MinimumThroughput阈值,或失败比例未满足FailureRatio,可根据业务调整配置。
内容的提问来源于stack exchange,提问作者Myster
相关产品推荐
相关产品推荐

