簡體   English   中英

Polly CircuitBreaker 在電路斷開時更改 HttpClient 基地址以繼續執行請求

[英]Polly CircuitBreaker change HttpClient baseaddress while circuit broken to continue execution of requests

我現在有什么?

目前,我有一個配置了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並將這兩個策略鏈接在一起。 我正在尋找一種封裝的配置,並且在客戶端級別而不是在使用該客戶端的 class 上處理故障。

場景解釋

讓我們假設有一個斷路器策略包裝在具有單個客戶端的重試策略中。

斷路器配置為在主基地址上的瞬態錯誤嘗試失敗 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 代碼

如本文所述,有一個解決方案是使用包裝在回退中的斷路器,但正如您所見,默認和回退的邏輯是在class中實現的,而不是在客戶端級別。

我想

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");
    }
}

我讀了什么?

我嘗試了什么?

我嘗試了幾種不同的場景來鏈接斷路器策略並將其與重試策略相結合,以在啟動文件中的客戶端杠桿上實現預期目標。 最后的 state 如下。 這些策略按照重試能夠捕獲BrokenCircuitException的順序進行包裝,但情況並非如此。 在消費者 class 上拋出異常,這不是預期的結果。 雖然觸發了RetryPolicy ,但是仍然拋出了消費者 class 上的異常。

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 Retries on break circuit 異常拋出:System.Private.CoreLib.dll中的'System.AggregateException'

更新 2:使用配置了策略的客戶端向 class 添加了參考 URL

更新 3:該項目已更新,以便WeatherService2.Get的實現以所需的方式工作:當主要服務不可用時,電路中斷,使用 falover 服務直到電路關閉。 這將是這個問題的答案,但是我想探索一個解決方案,使用WeatherService.GetStartup上使用適當的策略和客戶端設置來實現相同的結果。

使用客戶端參考class 參考使用 class 的項目

在上面的日志中可以看到Exception thrown: 'System.AggregateException' in System.Private.CoreLib.dll由斷路器拋出 - 這是不期望的,因為有重試包裝斷路器。

我已經下載了你的項目並使用它,所以這是我的觀察:

阻塞與非阻塞

  • 因為您的代碼使用阻塞異步調用( .Result ),所以您會看到AggregateException
public IEnumerable<WeatherForecast> Get()
{
    HttpResponseMessage response = null;
    try
    {
        response = _client.GetAsync(string.Empty).Result; //AggregateException  
    }
    catch (Exception e)
    {
        Debug.WriteLine($"{e.Message}");
    }
    ...
}
  • 為了解開AggregateExceptionInnerException ,您需要使用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}");
    }
    ...
}

升級

每當您將策略包裝到另一個策略中時,都可能會發生升級。 這意味着如果內部無法處理問題,那么它會將相同的問題傳播到外部,外部可能會也可能無法處理。 如果最外層沒有處理問題,那么(大部分時間)原始異常將被拋出給彈性策略的消費者(這是策略的組合)。

在這里您可以找到有關升級的更多詳細信息。

讓我們在您的案例中回顧一下這個概念:

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. 即使重試正在處理狀態 500,它也不會觸發,因為它達到了重試計數 (1)
  9. httpClient 返回帶有InternalServerError StatusCode 的HttpResponseMessage

現在,讓我們將連續失敗計數從 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將拋出該異常。

我希望這個練習可以幫助您更好地理解如何創建彈性策略,通過升級問題來組合多個策略。

我已經查看了您的替代解決方案,該解決方案與我在上一篇文章中討論的設計問題相同。

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 將 statusCode 為 500 的HttpResponseMessage傳播到外部策略
  4. 回退不會觸發,因為它是為異常設置的Handle<Exception>()
  5. 策略返回帶有 statusCode 500 的HttpResponseMessage
  6. 您的代碼手動檢查響應,然后手動調用回退。

如果您將政策更改為:

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

那么就不需要手動回退了。

  1. HttpClient 收到 statusCode 500 的響應
  2. 斷路器斷開
  3. CB 將 statusCode 為 500 的HttpResponseMessage傳播到外部策略
  4. 回退觸發,因為它也是為不成功的狀態代碼設置的
  5. HttpClient 收到 statusCode 200 的響應
  6. 策略返回帶有 statusCode 500 的HttpResponseMessage

您還需要了解一件更重要的事情。 前面的代碼之所以有效,因為您在沒有斷路器策略的情況下注冊了 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);

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM