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

Polly重试策略与Ocelot网关断路器结合失效问题求助

问题描述

在Ocelot API网关中尝试通过DelegatingHandler整合Polly的重试策略与断路器功能,目前断路器可正常工作(调用API3次后触发打开),但重试策略完全不生效,调用时直接抛出异常,无任何重试行为。

相关代码片段

"ExceptionsAllowedBeforeBreaking": 3,
.CircuitBreakerAsync(route.QosOptions.ExceptionsAllowedBeforeBreaking,

下游API接口代码:

[HttpGet("RaiseException")]
public async Task<int> RaiseException()
{
    await Task.Delay(1);
    throw new Exception("Mock Exception");
}

自定义DelegatingHandler

public class PollyWithInternalServerErrorCircuitBreakingDelegatingHandler : DelegatingHandler
{
    private readonly IOcelotLogger _logger;
    private readonly Polly.Wrap.AsyncPolicyWrap<HttpResponseMessage> _circuitBreakerPolicies;
    public PollyWithInternalServerErrorCircuitBreakingDelegatingHandler(DownstreamRoute route, IOcelotLoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<PollyWithInternalServerErrorCircuitBreakingDelegatingHandler>();

        var pollyQosProvider = new PollyQoSProvider(route, loggerFactory);

        var retryPolicy = HttpPolicyExtensions.HandleTransientHttpError()
                            .OrResult(r => r.StatusCode == HttpStatusCode.NotFound)
                            .WaitAndRetryAsync(2, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

        var responsePolicy = Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.InternalServerError)
            .CircuitBreakerAsync(route.QosOptions.ExceptionsAllowedBeforeBreaking,
                TimeSpan.FromMilliseconds(route.QosOptions.DurationOfBreak));
        _circuitBreakerPolicies = Policy.WrapAsync(pollyQosProvider.CircuitBreaker.Policies)
            .WrapAsync(retryPolicy).WrapAsync(responsePolicy);
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        try
        {
            return await _circuitBreakerPolicies.ExecuteAsync(() => base.SendAsync(request, cancellationToken));
        }
        catch (BrokenCircuitException ex)
        {
            _logger.LogError($"Reached to allowed number of exceptions. Circuit is open", ex);
            throw;
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError($"Error in CircuitBreakingDelegatingHandler.SendAsync", ex);
            throw;
        }
    }
}

Ocelot构建器扩展

public static class OcelotBuilderExtensions
{
    public static IOcelotBuilder AddPollyWithInternalServerErrorHandling(this IOcelotBuilder builder)
    {
        var errorMapping = new Dictionary<Type, Func<Exception, Error>>
        {
            {typeof(TaskCanceledException), e => new RequestTimedOutError(e)},
            {typeof(TimeoutRejectedException), e => new RequestTimedOutError(e)},
            {typeof(BrokenCircuitException), e => new RequestTimedOutError(e)}
        };

        builder.Services.AddSingleton(errorMapping);

        DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute route, IOcelotLoggerFactory logger)
        {
            return new PollyWithInternalServerErrorCircuitBreakingDelegatingHandler(route, logger);
        }

        builder.Services.AddSingleton((QosDelegatingHandlerDelegate)QosDelegatingHandlerDelegate);

        return builder;
    }
}

Program.cs配置

var builder = WebApplication.CreateBuilder(args);

//Ocelot加载配置文件
builder.Configuration.AddJsonFile($"ocelot.config.{builder.Environment.EnvironmentName}.json", optional: false, reloadOnChange: true);
builder.Services.AddOcelot(builder.Configuration)
    .AddPollyWithInternalServerErrorHandling();

Ocelot路由配置

"UpstreamHttpMethod": [ "GET" ],
"QoSOptions": {
    //触发断路器前允许的异常次数
    "ExceptionsAllowedBeforeBreaking": 3,
    //断路器打开后的持续时间(毫秒)
    "DurationOfBreak": 5000,
    //请求超时阈值(毫秒)
    "TimeoutValue": 100000
}

问题分析与修复方案

核心问题1:重试策略未覆盖当前异常场景

下游API直接抛出Exception,但当前重试策略仅处理:

  • HandleTransientHttpError():涵盖HTTP 5xx状态码和HttpRequestException
  • OrResult(r => r.StatusCode == HttpStatusCode.NotFound):仅处理404响应

下游抛出的未捕获异常无法被当前重试策略识别,导致重试逻辑不触发。

核心问题2:Policy包装顺序错误

Polly的WrapAsync执行顺序是从外到内,你当前的包装顺序让断路器逻辑先于重试执行,一旦断路器触发,重试策略根本没有执行机会。正确顺序应该是重试在外,断路器在内,确保重试失败后再触发断路器。

修复步骤

1. 扩展重试策略的异常覆盖范围

修改重试策略,捕获下游抛出的异常类型:

var retryPolicy = HttpPolicyExtensions.HandleTransientHttpError()
                    .OrResult(r => r.StatusCode == HttpStatusCode.NotFound)
                    .Or<Exception>() // 可替换为更精确的异常类型,比如HttpRequestException
                    .WaitAndRetryAsync(2, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

2. 修正Policy包装顺序

将重试策略放在最外层,确保重试逻辑优先执行:

// 正确顺序:重试 → 自定义断路器 → Ocelot默认断路器
_circuitBreakerPolicies = Policy.WrapAsync(retryPolicy)
    .WrapAsync(responsePolicy)
    .WrapAsync(pollyQosProvider.CircuitBreaker.Policies);

3. 移除重复断路器(可选)

如果不需要Ocelot默认的QoS断路器配置,可以直接移除pollyQosProvider.CircuitBreaker.Policies,仅保留自定义的重试+断路器组合,避免逻辑冲突。

4. 验证异常传递

在SendAsync中添加日志,确认下游返回的响应状态或异常类型,确保重试策略的触发条件匹配实际场景:

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
    try
    {
        var response = await base.SendAsync(request, cancellationToken);
        _logger.LogInformation($"下游响应状态码:{response.StatusCode}");
        return response;
    }
    catch (Exception ex)
    {
        _logger.LogError($"下游抛出异常:{ex.Message}");
        throw;
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 22:30:32