【问题标题】:Polly CircuitBreaker change HttpClient baseaddress while circuit broken to continue execution of requestsPolly CircuitBreaker 在电路断开时更改 HttpClient 基地址以继续执行请求
【发布时间】:2021-03-20 13:48:53
【问题描述】:

我现在有什么?

目前,我有一个配置了RetryAsync 策略的客户端,该策略使用主地址并在发生故障时切换到故障转移地址。连接详细信息从机密管理器中读取。

services
    .AddHttpClient ("MyClient", client => client.BaseAddress = PlaceholderUri)
    .ConfigureHttpMessageHandlerBuilder (builder => {

        // loads settings from secret manager
        var settings = configLoader.LoadSettings().Result;

        builder.PrimaryHandler = new HttpClientHandler {
            Credentials = new NetworkCredential (settings.Username, settings.Password),
            AutomaticDecompression = DecompressionMethods.GZip
        };

        var primaryBaseAddress = new Uri (settings.Host);
        var failoverBaseAddress = new Uri (settings.DrHost);

        builder.AdditionalHandlers.Add (new PolicyHttpMessageHandler (requestMessage => {
            var relativeAddress = PlaceholderUri.MakeRelativeUri (requestMessage.RequestUri);
            requestMessage.RequestUri = new Uri (primaryBaseAddress, relativeAddress);

            return HttpPolicyExtensions.HandleTransientHttpError ()
                .RetryAsync ((result, retryCount) =>
                    requestMessage.RequestUri = new Uri (failoverBaseAddress, relativeAddress));
        }));
    });

我想达到什么目标?

一般

我的客户可以使用主服务或故障转移服务。当主服务器关闭时,使用故障转移直到主服务器备份。当两者都关闭时,我们会收到警报,并且可以通过秘密管理器动态更改服务地址。

在代码中

现在我还想介绍一个CircuitBreakerPolicy 并将这两个策略链接在一起。我正在寻找一种封装的配置,并且在客户端级别而不是在使用该客户端的类上处理错误。

场景解释

让我们假设有一个断路器策略包装在具有单个客户端的重试策略中。

断路器配置为在 暂时错误 3 次尝试失败 后将电路断开 60 秒在主基地址上。 OnBreak - 地址从主地址更改为故障转移。

重试策略配置为处理BrokenCircuitException,并重试一次,地址从主地址更改为故障转移以继续。

  1. 请求主地址 - 500 代码
  2. 请求主地址 - 500 代码
  3. 主地址请求 - 500 代码(连续 3 次失败)
  4. 电路中断 60 秒
  5. 对主地址的请求 - BrokenCircuitException 被重试策略捕获,调用故障转移
  6. 对主地址的请求 - BrokenCircuitException 被重试策略捕获,调用故障转移
  7. 对主地址的请求 - BrokenCircuitException 被重试策略捕获,调用故障转移
  8. 对主地址的请求 - BrokenCircuitException 被重试策略捕获,调用故障转移
  9. (60 秒后)电路半开 - (此处可以再断开 60 秒或打开 - 假设打开)
  10. 请求主地址 - 200 代码

正如articles 中所述,有一个解决方案是使用包装在回退中的断路器,但正如您所见,默认和回退的逻辑是在类中实现的,而不是在客户端级别。

我愿意

public class OpenExchangeRatesClient
{
    private readonly HttpClient _client;
    private readonly Policy _policy;
    public OpenExchangeRatesClient(string apiUrl)
    {
        _client = new HttpClient
        {
            BaseAddress = new Uri(apiUrl),
        };

        var circuitBreaker = Policy
            .Handle<Exception>()
            .CircuitBreakerAsync(
                exceptionsAllowedBeforeBreaking: 2,
                durationOfBreak: TimeSpan.FromMinutes(1)
            );

        _policy = Policy
            .Handle<Exception>()
            .FallbackAsync(() => GetFallbackRates())
            .Wrap(circuitBreaker);
    }

    public Task<ExchangeRates> GetLatestRates()
    {
        return _policy
            .ExecuteAsync(() => CallRatesApi());
    }

    public Task<ExchangeRates> CallRatesApi()
    {
        //call the API, parse the results
    }

    public Task<ExchangeRates> GetFallbackRates()
    {
        // load the rates from the embedded file and parse them
    }
}

改写为

public class OpenExchangeRatesClient 
{
    private readonly HttpClient _client;
    public OpenExchangeRatesClient (IHttpClientFactory clientFactory) {
        _client = clientFactory.CreateClient ("MyClient");
    }

    public Task<ExchangeRates> GetLatestRates () {
        return _client.GetAsync ("/rates-gbp-usd");
    }
}

我读了些什么?

我尝试了什么?

我尝试了几种不同的方案,将断路器策略与重试策略链接并结合起来,以在启动文件中的客户端杠杆上实现预期目标。最后一个状态如下。这些策略按照重试能够捕获BrokenCircuitException 的顺序进行包装,但情况并非如此。在消费者类上抛出异常,这不是预期的结果。虽然触发了RetryPolicy,但是还是抛出了消费者类的异常。

var retryPolicy = GetRetryPolicy();
var circuitBreaker = GetCircuitBreakerPolicy();

var policyWraper = Policy.WrapAsync(retryPolicy, circuitBreaker);

services
    .AddHttpClient("TestClient", client => client.BaseAddress = GetPrimaryUri())
    .AddPolicyHandler(policyWraper);

static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .CircuitBreakerAsync(
            3,
            TimeSpan.FromSeconds(45),
            OnBreak,
            OnReset, 
            OnHalfOpen);
}

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return Policy<HttpResponseMessage>
        .Handle<Exception>()
        .RetryAsync(1, (response, retryCount) =>
        {
            Debug.WriteLine("Retries on broken circuit");
        });
}

我省略了 OnBreakOnResetOnHalfOpen 方法,因为它们只是打印一些消息。

更新:从控制台添加了日志。

Circuit broken (after 3 attempts)
Retries on broken
Exception thrown: 'System.AggregateException' in System.Private.CoreLib.dll 
Retries on broken circuit
Exception thrown: 'System.AggregateException' in System.Private.CoreLib.dll

'CircuitBreakerPolicy.exe' (CoreCLR: clrhost): 加载'C:\Program 断路重试 抛出异常:System.Private.CoreLib.dll 中的“System.AggregateException”

更新 2:向使用配置了策略的客户端的类添加了参考 URL

更新 3:project 已更新,以便WeatherService2.Get 的实现以所需的方式工作:当主要服务不可用时,电路断开,使用故障转移服务直到电路关闭。这将是这个问题的答案,但是我想探索一个解决方案,其中使用 WeatherService.Get 并在 Startup 上使用适当的策略和客户端设置来实现相同的结果。

使用客户端引用class。 使用类引用project

在上面的日志中可以看到Exception thrown: 'System.AggregateException' in System.Private.CoreLib.dll是由断路器抛出的——这是不期望的,因为有重试包裹断路器。

【问题讨论】:

  • 您的 GetRetryPolicy 将 retryCount 设置为 1。这意味着将有 2 次尝试(初始和重试)。因此,您的断路器不会中断,因为没有连续 3 次失败的尝试。
  • CircuitBreaker 在 3 次尝试后断开,然后由于抛出 BrokenCircutException 而触发重试。问题是消费类也抛出了异常,我希望它由包装断路器策略的重试策略处理。修改帖子以添加日志。
  • 你能和我们分享一下消费方面吗?你也可以和我们分享一下AggregateException里面是什么(InnerException是什么)?
  • 已将项目上传到公共仓库,并使用客户端添加了对类的引用以及对整个项目的引用。我还通过启用中断所有异常进行调试,AggregateException 具有内部异常Polly.CircuitBreaker.BrokenCircuitException&lt;System.Net.Http.HttpResponseMessage&gt;
  • 我还没有机会看你的第二个帖子,会尽快做和回答。

标签: c# asp.net-core polly circuit-breaker resiliency


【解决方案1】:

我已经下载了你的项目并开始使用它,以下是我的观察:

阻塞与非阻塞

  • 因为您的代码使用阻塞异步调用 (.Result),这就是您看到 AggregateException 的原因
public IEnumerable<WeatherForecast> Get()
{
    HttpResponseMessage response = null;
    try
    {
        response = _client.GetAsync(string.Empty).Result; //AggregateException  
    }
    catch (Exception e)
    {
        Debug.WriteLine($"{e.Message}");
    }
    ...
}
  • 为了解开InnerExceptionInnerException,您需要使用await
public async Task<IEnumerable<WeatherForecast>> Get()
{
    HttpResponseMessage response = null;
    try
    {
        response = await _client.GetAsync(string.Empty); //BrokenCircuitException
    }
    catch (Exception e)
    {
        Debug.WriteLine($"{e.Message}");
    }
    ...
}

升级

每当您将策略包装到另一个策略时,就可能会发生升级。这意味着如果内部无法处理问题,那么它会将相同的问题传播到外部,外部可能会也可能无法处理。如果最外层没有处理问题,那么(大部分时间)原始异常将被抛出给弹性策略的消费者(这是策略的组合)。

您可以在此处找到有关escalation 的更多详细信息。

让我们在您的案例中回顾一下这个概念:

var policyWrapper = Policy.WrapAsync(retryPolicy, circuitBreaker);

static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .CircuitBreakerAsync(3, TimeSpan.FromSeconds(45), ...);
}

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return Policy<HttpResponseMessage>
        .Handle<Exception>()
        .RetryAsync(1, ...);
}
  1. 针对https://httpstat.us/500 发出初始请求(1. 尝试)
  2. 它返回 500,这会将连续瞬态故障从 0 增加到 1
  3. CB 升级问题以重试
  4. 重试未处理状态 500,因此未触发重试
  5. httpClient 返回带有InternalServerError 状态码的HttpResponseMessage

让我们修改重试策略来处理短暂的 http 错误:

static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .CircuitBreakerAsync(3, TimeSpan.FromSeconds(45), ...);
}

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .Or<Exception>()
        .RetryAsync(1, ...);
}
  1. 针对https://httpstat.us/500 发出初始请求(1.尝试)
  2. 它返回 500,这会将连续瞬态故障从 0 增加到 1
  3. CB 升级问题以重试
  4. Retry 正在处理状态 500,因此 retry 会立即发出另一次尝试
  5. 针对https://httpstat.us/500 发出第一个重试请求(2.尝试)
  6. 它返回 500,这会将连续瞬态故障从 1 增加到 2
  7. CB 升级问题以重试
  8. 即使 Retry 正在处理状态 500,它也不会触发,因为它已达到其重试次数 (1)
  9. httpClient 返回一个HttpResponseMessageInternalServerError StatusCode。

现在,让我们将连续失败计数从 3 降低到 1 并明确处理 BrokenCircuitException

static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .CircuitBreakerAsync(1, TimeSpan.FromSeconds(45), ...);
}

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .Or<BrokenCircuitException>()
        .RetryAsync(1, ...);
}
  1. 针对https://httpstat.us/500 发出初始请求(1. 尝试)
  2. 它返回 500,这会将连续瞬态故障从 0 增加到 1
  3. 断路器打开,因为它达到了预定义的阈值
  4. CB 升级问题以重试
  5. Retry 正在处理状态 500,因此 retry 会立即发出另一次尝试
  6. 针对https://httpstat.us/500 发出第一次重试请求(2.尝试)
  7. CB 阻止此调用,因为它已损坏
  8. CB 抛出 BrokenCircuitException
  9. 即使 Retry 正在处理 BrokenCircuitException,它也不会触发,因为它已达到其重试次数 (1)
  10. 重试会抛出原​​始异常 (BrokenCircuitException),因此 httpClient 的 GetAsync 会抛出该异常。

最后让我们将 retryCount 从 1 增加到 2:

static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .CircuitBreakerAsync(1, TimeSpan.FromSeconds(45), ...);
}

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .Or<BrokenCircuitException>()
        .RetryAsync(2, ...);
}
  1. 针对https://httpstat.us/500 发出初始请求(1. 尝试)
  2. 它返回 500,这会将连续瞬态故障从 0 增加到 1
  3. 断路器打开,因为它达到了预定义的阈值
  4. CB 升级问题以重试
  5. Retry 正在处理状态 500,因此 retry 会立即发出另一次尝试
  6. 针对https://httpstat.us/500 发出第一次重试请求(2.尝试)
  7. CB 阻止此调用,因为它已损坏
  8. CB 抛出 BrokenCircuitException
  9. Retry 正在处理 BrokenCircuitException,它没有超过它的 retryCount,因此它立即发出另一次尝试
  10. 针对https://httpstat.us/500 发出第二次重试请求(3. 尝试)
  11. CB 阻止此调用,因为它已损坏
  12. CB 抛出 BrokenCircuitException
  13. 即使 Retry 正在处理 BrokenCircuitException,它也不会触发,因为它已达到其重试次数 (2)
  14. 重试将抛出原始异常 (BrokenCircuitException),因此 httpClient 的 GetAsync 将抛出该异常。

我希望这个练习可以帮助您更好地了解如何制定弹性策略,通过升级问题来组合多个策略。

【讨论】:

  • @VergilC。我的解释够清楚还是需要进一步解释?
  • 抱歉回复延迟,直到今天才有机会看到您的回复。是的,很清楚现在发生了什么,非常感谢您花时间调查并提供如此详细的答案。我将根据您的示例调整代码以使用它,理解并进入下一个难题,探索是否可以调整客户端的处理程序以尝试不同的 URL,但是是的,您的答案已经清除起雾。
  • @VergilC。如果您认为我的帖子有帮助,请考虑通过投票来支持我的工作。如果您还有其他问题,请随时提问。
  • 在你的最后一个例子中,5. Retry is handling status 500, so retry issues another attempt immediately - 有没有办法在重试请求 uri 更改之前配置它?我现在尝试实现我的初始目标 - “我的客户端可以使用主服务器或故障转移服务。当主服务器关闭时,使用故障转移直到主服务器备份。当两者都关闭时,我们会收到警报并且可以动态更改服务地址通过秘密管理器。”我曾尝试使用 builder.AdditionalHandlers.Add() 为客户端注册 2 个策略,但这不起作用。
  • @VergilC。 Here 我已经详细说明了如何更改每次重试尝试的 url。适合你吗?
【解决方案2】:

我已经查看了您的替代解决方案,该解决方案与我在上一篇文章中讨论的设计问题相同。

public WeatherService2(IHttpClientFactory clientFactory, IEnumerable<IAsyncPolicy<HttpResponseMessage>> policies)
{
    _primaryClient = clientFactory.CreateClient("PrimaryClient");
    _failoverClient = clientFactory.CreateClient("FailoverClient");
    _circuitBreaker = policies.First(p => p.PolicyKey == "CircuitBreaker");

    _policy = Policy<HttpResponseMessage>
        .Handle<Exception>()
        .FallbackAsync(_ => CallFallbackForecastApi())
        .WrapAsync(_circuitBreaker);
}

public async Task<string> Get()
{
    var response = await _policy.ExecuteAsync(async () => await CallForecastApi());

    if (response.IsSuccessStatusCode) 
        return response.StatusCode.ToString();

    response = await CallFallbackForecastApi();
    return response.StatusCode.ToString();
}

永远不会触发您的后备策略。

  1. HttpClient 收到 statusCode 500 的响应
  2. 断路器断路
  3. CB 将状态码为 500 的 HttpResponseMessage 传播到外部策略
  4. 回退不会触发,因为它是为异常设置的 Handle&lt;Exception&gt;()
  5. 策略返回 HttpResponseMessage,statusCode 为 500
  6. 您的代码手动检查响应,然后手动调用回退。

如果您将政策更改为:

_policy = Policy
    .HandleResult<HttpResponseMessage>(response => response != null && !response.IsSuccessStatusCode)
    .Or<Exception>()
    .FallbackAsync(_ => CallFallbackForecastApi())
    .WrapAsync(_circuitBreaker);

那么就不需要手动后备了。

  1. HttpClient 收到 statusCode 500 的响应
  2. 断路器断路
  3. CB 将状态码为 500 的 HttpResponseMessage 传播到外部策略
  4. 后备触发器,因为它也被设置为不成功的状态代码
  5. HttpClient 收到 statusCode 200 的响应
  6. 策略返回 HttpResponseMessage,statusCode 为 500

您还需要了解一件更重要的事情。之前的代码有效,因为您注册了 HttpClients没有断路器策略。

这意味着 CB 未附加到 HttpClient。因此,如果您像这样更改代码:

public async Task<HttpResponseMessage> CallForecastApi()
    => await _primaryClient.GetAsync("https://httpstat.us/500/");

public async Task<HttpResponseMessage> CallFallbackForecastApi()
    => await _primaryClient.GetAsync("https://httpstat.us/200/");

那么即使 CircuitBreaker 在第一次尝试后打开,CallFallbackForecastApi 也不会抛出 BrokenCircuitException

但是,如果您像这样将 CB 附加到 HttpClient:

services
    .AddHttpClient("PrimaryClient", client => client.BaseAddress = PlaceholderUri)
    ...
    .AddPolicyHandler(GetCircuitBreakerPolicy());

然后你像这样简化WeatherService2

private readonly HttpClient _primaryClient;
private readonly IAsyncPolicy<HttpResponseMessage> _policy;

public WeatherService2(IHttpClientFactory clientFactory)
{
    _primaryClient = clientFactory.CreateClient("PrimaryClient");
    _policy = Policy
        .HandleResult<HttpResponseMessage>(response => response != null && !response.IsSuccessStatusCode)
        .Or<Exception>()
        .FallbackAsync(_ => CallFallbackForecastApi());
}

那么它将以BrokenCircuitException 惨遭失败。


如果您的WeatherService2 看起来像这样:

public class WeatherService2 : IWeatherService2
{
    private readonly HttpClient _primaryClient;
    private readonly HttpClient _secondaryClient;
    private readonly IAsyncPolicy<HttpResponseMessage> _policy;
    public WeatherService2(IHttpClientFactory clientFactory)
    {
        _primaryClient = clientFactory.CreateClient("PrimaryClient");
        _secondaryClient = clientFactory.CreateClient("FailoverClient");

        _policy = Policy
            .HandleResult<HttpResponseMessage>(response => response != null && !response.IsSuccessStatusCode)
            .Or<Exception>()
            .FallbackAsync(_ => CallFallbackForecastApi());
    }

    public async Task<string> Get()
    {
        var response = await _policy.ExecuteAsync(async () => await CallForecastApi());
        return response.StatusCode.ToString();
    }

    public async Task<HttpResponseMessage> CallForecastApi()
        => await _primaryClient.GetAsync("https://httpstat.us/500/");

    public async Task<HttpResponseMessage> CallFallbackForecastApi()
        => await _secondaryClient.GetAsync("https://httpstat.us/200/");
}

那么它可以正常工作只有PrimaryClientFailoverClient不同的断路器

services
    .AddHttpClient("PrimaryClient", client => client.BaseAddress = PlaceholderUri)
    ...
    .AddPolicyHandler(GetCircuitBreakerPolicy());

services
    .AddHttpClient("FailoverClient", client => client.BaseAddress = PlaceholderUri)
    ...
    .AddPolicyHandler(GetCircuitBreakerPolicy());

如果他们共享同一个断路器,那么第二次调用将再次失败,并返回 BrokenCircuitException

var cbPolicy = GetCircuitBreakerPolicy();

services
    .AddHttpClient("PrimaryClient", client => client.BaseAddress = PlaceholderUri)
    ...
    .AddPolicyHandler(cbPolicy);

services
    .AddHttpClient("FailoverClient", client => client.BaseAddress = PlaceholderUri)
    ...
    .AddPolicyHandler(cbPolicy);

【讨论】:

    猜你喜欢
    • 2021-12-03
    • 1970-01-01
    • 2015-02-23
    • 1970-01-01
    • 1970-01-01
    • 2021-08-08
    • 2018-07-04
    • 2018-03-29
    • 1970-01-01
    相关资源
    最近更新 更多