Поделиться через


Создание устойчивых HTTP-приложений: ключевые шаблоны разработки

Создание надежных HTTP-приложений, которые могут восстановиться после временных ошибок сбоя, является общим требованием. В этой статье предполагается, что вы уже прочитали общие сведения о устойчивой разработке приложений, так как в этой статье представлены основные понятия. Чтобы помочь в создании устойчивых HTTP-приложений, пакет NuGet Microsoft.Extensions.Http.Resilience предоставляет механизмы устойчивости специально для этого HttpClient. Этот пакет NuGet использует библиотеку Microsoft.Extensions.Resilience и Polly, который является популярным проектом с открытым исходным кодом. Дополнительные сведения см. в разделе Polly.

Начало работы

Чтобы использовать шаблоны устойчивости в HTTP-приложениях, установите пакет NuGet Microsoft.Extensions.Http.Resilience .

dotnet add package Microsoft.Extensions.Http.Resilience --version 8.0.0

Дополнительные сведения см. в разделе dotnet package add или Управление зависимостями пакетов в приложениях .NET.

Добавление устойчивости к HTTP-клиенту

To add resilience to an HttpClient, you chain a call on the IHttpClientBuilder type that is returned from calling any of the available AddHttpClient methods. For more information, see IHttpClientFactory with .NET.

Доступно несколько расширений, ориентированных на устойчивость. Некоторые из них являются стандартными, поэтому используют различные отраслевые рекомендации, а другие — более настраиваемыми. При добавлении устойчивости следует добавить только один обработчик устойчивости и избегать наложения обработчиков. Если необходимо добавить несколько обработчиков устойчивости, следует рассмотреть возможность использования AddResilienceHandler метода расширения, что позволяет настроить стратегии устойчивости.

Внимание

Все примеры в этой статье зависят от API AddHttpClient из библиотеки Microsoft.Extensions.Http, которая возвращает экземпляр IHttpClientBuilder. Экземпляр IHttpClientBuilder используется для настройки HttpClient и добавления обработчика устойчивости.

Добавьте стандартный обработчик устойчивости

Стандартный обработчик устойчивости использует несколько сложенных друг на друга стратегий устойчивости, с параметрами по умолчанию для отправки запросов и обработки временных ошибок. The standard resilience handler is added by calling the AddStandardResilienceHandler extension method on an IHttpClientBuilder instance.

var services = new ServiceCollection();

var httpClientBuilder = services.AddHttpClient<ExampleClient>(
    configureClient: static client =>
    {
        client.BaseAddress = new("https://jsonplaceholder.typicode.com");
    });

Предыдущий код:

  • Creates a ServiceCollection instance.
  • Adds an HttpClient for the ExampleClient type to the service container.
  • Настраивает HttpClient для использования "https://jsonplaceholder.typicode.com" в качестве базового адреса.
  • Создает этот объект httpClientBuilder , используемый в других примерах в этой статье.

A more real-world example would rely on hosting, such as that described in the .NET Generic Host article. Используя пакет NuGet Microsoft.Extensions.Hosting, рассмотрим следующий обновленный пример:

using Http.Resilience.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

IHttpClientBuilder httpClientBuilder = builder.Services.AddHttpClient<ExampleClient>(
    configureClient: static client =>
    {
        client.BaseAddress = new("https://jsonplaceholder.typicode.com");
    });

Предыдущий код аналогичен ручному подходу к созданию ServiceCollection, но вместо этого используется Host.CreateApplicationBuilder() для построения хоста, который предоставляет службы.

Определяется ExampleClient следующим образом:

using System.Net.Http.Json;

namespace Http.Resilience.Example;

/// <summary>
/// An example client service, that relies on the <see cref="HttpClient"/> instance.
/// </summary>
/// <param name="client">The given <see cref="HttpClient"/> instance.</param>
internal sealed class ExampleClient(HttpClient client)
{
    /// <summary>
    /// Returns an <see cref="IAsyncEnumerable{T}"/> of <see cref="Comment"/>s.
    /// </summary>
    public IAsyncEnumerable<Comment?> GetCommentsAsync()
    {
        return client.GetFromJsonAsAsyncEnumerable<Comment>("/comments");
    }
}

Предыдущий код:

  • Определяет ExampleClient тип, имеющий конструктор, который принимает объект HttpClient.
  • Предоставляет метод GetCommentsAsync, который отправляет GET запрос в конечную точку /comments и возвращает ответ.

Тип Comment определяется следующим образом:

namespace Http.Resilience.Example;

public record class Comment(
    int PostId, int Id, string Name, string Email, string Body);

Учитывая, что вы создали IHttpClientBuilder (httpClientBuilder) и теперь понимаете реализацию ExampleClient и соответствующую Comment модель, рассмотрим следующий пример:

httpClientBuilder.AddStandardResilienceHandler();

The preceding code adds the standard resilience handler to the HttpClient. Как и большинство API устойчивости, предусмотрены перегрузки, позволяющие настраивать параметры по умолчанию и стратегии устойчивости.

Удалите стандартные обработчики устойчивости

Существует метод RemoveAllResilienceHandlers, который удаляет все ранее зарегистрированные обработчики устойчивости. Это полезно, если необходимо очистить существующие обработчики отказоустойчивости и добавить ваши собственные. В следующем примере показано, как сконфигурировать пользовательский HttpClient, используя метод AddHttpClient, удалить все заранее определённые стратегии устойчивости и заменить их новыми обработчиками. Этот подход позволяет очистить существующие конфигурации и определить новые в соответствии с вашими требованиями.

// By default, we want all HttpClient instances to include the StandardResilienceHandler.
services.ConfigureHttpClientDefaults(builder => builder.AddStandardResilienceHandler());
// For a named HttpClient "custom" we want to remove the StandardResilienceHandler and add the StandardHedgingHandler instead.
services.AddHttpClient("custom")
    .RemoveAllResilienceHandlers()
    .AddStandardHedgingHandler();

Предыдущий код:

  • Creates a ServiceCollection instance.
  • Adds the standard resilience handler to all HttpClient instances.
  • For the "custom" HttpClient:
    • Removes all predefined resilience handlers that were previously registered. This is useful when you want to start with a clean state to add your own custom strategies.
    • Добавляет StandardHedgingHandler в HttpClient. Вы можете заменить AddStandardHedgingHandler() любой стратегией, которая соответствует потребностям приложения, таким как механизмы повторных попыток, выключатели цепи или другие методы устойчивости.

Стандартные обработчики отказоустойчивости по умолчанию

Конфигурация по умолчанию объединяет пять стратегий устойчивости в следующем порядке (от самого внешнего к самому внутреннему):

заказ Стратегия Описание Defaults
1 Ограничение скорости Механизм ограничения запросов ограничивает максимальное количество одновременных запросов, отправляемых зависимому компоненту. Queue: 0
Разрешение: 1_000
2 Общий тайм-аут Общая система ограничения времени ожидания запроса применяет общее время ожидания к процессу выполнения, гарантируя, что запрос, включая повторные попытки, не превышает настроенное ограничение. Общее время ожидания: 30s
3 Повторить попытку Конвейер повторных попыток повторяет запрос в случае замедления зависимости или возвращает временную ошибку. Максимальное число повторных попыток: 3
Backoff: Exponential
Use jitter: true
Задержка:2s
4 Circuit breaker Ломатель цепи блокирует выполнение, если обнаружено слишком много непосредственных отказов или таймаутов. Соотношение сбоев: 10 %
Минимальная пропускная способность: 100
Длительность выборки: 30s
Длительность перерыва: 5s
5 Attempt timeout The attempt timeout pipeline limits each request attempt duration and throws if it's exceeded. Attempt timeout: 10s

Retries and circuit breakers

Стратегии повторных попыток и останова цепи обрабатывают набор определенных кодов состояния HTTP и исключений. Рассмотрим следующие коды состояния HTTP:

  • HTTP 500 и более поздних версий (ошибки сервера)
  • HTTP 408 (истекло время ожидания запроса)
  • HTTP 429 (слишком много запросов)

Кроме того, эти стратегии обрабатывают следующие исключения:

  • HttpRequestException
  • TimeoutRejectedException

Отключение повторных попыток для заданного списка методов HTTP

По умолчанию стандартный обработчик устойчивости настроен для повторных попыток для всех методов HTTP. Для некоторых приложений такое поведение может быть нежелательным или даже вредным. Например, если запрос POST вставляет новую запись в базу данных, то повторные попытки для такого запроса могут привести к дублированию данных. Если необходимо отключить повторные попытки для заданного списка методов HTTP, можно использовать метод DisableFor(HttpRetryStrategyOptions, HttpMethod[]):

httpClientBuilder.AddStandardResilienceHandler(options =>
{
    options.Retry.DisableFor(HttpMethod.Post, HttpMethod.Delete);
});

Кроме того, можно использовать метод DisableForUnsafeHttpMethods(HttpRetryStrategyOptions), который отключает повторные попытки для запросов POST, PATCH, PUT, DELETEи CONNECT запросов. Согласно RFC, эти методы считаются небезопасными; означает, что их семантика не доступна только для чтения:

httpClientBuilder.AddStandardResilienceHandler(options =>
{
    options.Retry.DisableForUnsafeHttpMethods();
});

Добавить стандартный обработчик хеджирования

Стандартный обработчик хеджирования оборачивает выполнение запроса стандартным механизмом хеджирования. Hedging retries slow requests in parallel.

Чтобы использовать стандартный обработчик хэджинга, вызовите AddStandardHedgingHandler метод расширения. В следующем примере ExampleClient настраивается для использования стандартного обработчика хеджирования.

httpClientBuilder.AddStandardHedgingHandler();

The preceding code adds the standard hedging handler to the HttpClient.

Standard hedging handler defaults

The standard hedging uses a pool of circuit breakers to ensure that unhealthy endpoints aren't hedged against. By default, the selection from the pool is based on the URL authority (scheme + host + port).

Tip

Рекомендуется настроить способ выбора стратегий путем вызова StandardHedgingHandlerBuilderExtensions.SelectPipelineByAuthority или StandardHedgingHandlerBuilderExtensions.SelectPipelineBy для более сложных сценариев.

The preceding code adds the standard hedging handler to the IHttpClientBuilder. Конфигурация по умолчанию объединяет пять стратегий устойчивости в следующем порядке (от самого внешнего к самому внутреннему):

заказ Стратегия Описание Defaults
1 Общее время ожидания запроса Общая система ограничения времени ожидания запроса применяет общий тайм-аут на выполнение задачи, гарантируя, что запрос, включая попытки хеджирования, не превышает установленное ограничение. Общее время ожидания: 30s
2 Хеджирование Стратегия хеджирования выполняет запросы к нескольким конечным точкам в случае, если зависимость медленно работает или возвращает временную ошибку. Маршрутизация — это параметры, по умолчанию он просто хеджирует URL-адрес, предоставленный исходным HttpRequestMessage. Минимальное количество попыток: 1
Максимальное количество попыток: 10
Задержка: 2 с
3 Ограничение скорости (на конечную точку) Механизм ограничения запросов ограничивает максимальное количество одновременных запросов, отправляемых зависимому компоненту. Queue: 0
Разрешение: 1_000
4 Разделитель цепи (на каждом конце) Ломатель цепи блокирует выполнение, если обнаружено слишком много непосредственных отказов или таймаутов. Соотношение сбоев: 10 %
Минимальная пропускная способность: 100
Длительность выборки: 30s
Длительность перерыва: 5s
5 Attempt timeout (per endpoint) The attempt timeout pipeline limits each request attempt duration and throws if it's exceeded. Время ожидания: 10s

Настройка выбора маршрута обработчика хеджирования

При использовании стандартного обработчика хеджирования можно настроить способ выбора конечных точек запроса путем вызова различных расширений в типе IRoutingStrategyBuilder . Это может быть полезно для таких сценариев, как тестирование A/B, где требуется направлять процент запросов в другую конечную точку:

httpClientBuilder.AddStandardHedgingHandler(static (IRoutingStrategyBuilder builder) =>
{
    // Hedging allows sending multiple concurrent requests
    builder.ConfigureOrderedGroups(static options =>
    {
        options.Groups.Add(new UriEndpointGroup()
        {
            Endpoints =
            {
                // Imagine a scenario where 3% of the requests are 
                // sent to the experimental endpoint.
                new() { Uri = new("https://example.net/api/experimental"), Weight = 3 },
                new() { Uri = new("https://example.net/api/stable"), Weight = 97 }
            }
        });
    });
});

Предыдущий код:

  • Adds the hedging handler to the IHttpClientBuilder.
  • Configures the IRoutingStrategyBuilder to use the ConfigureOrderedGroups method to configure the ordered groups.
  • Adds an EndpointGroup to the orderedGroup that routes 3% of the requests to the https://example.net/api/experimental endpoint and 97% of the requests to the https://example.net/api/stable endpoint.
  • Configures the IRoutingStrategyBuilder to use the ConfigureWeightedGroups method to configure the

Чтобы настроить весовую группу, вызовите метод ConfigureWeightedGroups на типе IRoutingStrategyBuilder. Следующий пример настраивает IRoutingStrategyBuilder на использование метода ConfigureWeightedGroups для настройки взвешенных групп.

httpClientBuilder.AddStandardHedgingHandler(static (IRoutingStrategyBuilder builder) =>
{
    // Hedging allows sending multiple concurrent requests
    builder.ConfigureWeightedGroups(static options =>
    {
        options.SelectionMode = WeightedGroupSelectionMode.EveryAttempt;

        options.Groups.Add(new WeightedUriEndpointGroup()
        {
            Endpoints =
            {
                // Imagine A/B testing
                new() { Uri = new("https://example.net/api/a"), Weight = 33 },
                new() { Uri = new("https://example.net/api/b"), Weight = 33 },
                new() { Uri = new("https://example.net/api/c"), Weight = 33 }
            }
        });
    });
});

Предыдущий код:

  • Adds the hedging handler to the IHttpClientBuilder.
  • Configures the IRoutingStrategyBuilder to use the ConfigureWeightedGroups method to configure the weighted groups.
  • Устанавливает SelectionMode на WeightedGroupSelectionMode.EveryAttempt.
  • Добавляет WeightedEndpointGroup в weightedGroup, который распределяет 33% запросов на конечную точку https://example.net/api/a, 33% запросов на конечную точку https://example.net/api/b и 33% запросов на конечную точку https://example.net/api/c.

Tip

Максимальное количество попыток хеджирования напрямую сопоставляется с числом настроенных групп. Например, если у вас есть две группы, максимальное количество попыток составляет два.

Дополнительные сведения см. в документации Polly: стратегия повышения устойчивости.

Обычно можно настроить упорядоченную группу или взвешенную группу, но возможно настроить обе. Использование упорядоченных и взвешированных групп полезно в сценариях, когда вы хотите отправить процент запросов в другую конечную точку, например, при тестировании A/B.

Add custom resilience handlers

Чтобы получить дополнительные возможности управления, можно настроить обработчики устойчивости с помощью AddResilienceHandler API. Этот метод принимает делегата, который конфигурирует экземпляр ResiliencePipelineBuilder<HttpResponseMessage>, используемый для создания стратегий повышения устойчивости.

Чтобы настроить именованный обработчик устойчивости, вызовите AddResilienceHandler метод расширения с именем обработчика. В следующем примере настраивается именованный обработчик стабильности "CustomPipeline".

httpClientBuilder.AddResilienceHandler(
    "CustomPipeline",
    static builder =>
{
    // See: https://www.pollydocs.org/strategies/retry.html
    builder.AddRetry(new HttpRetryStrategyOptions
    {
        // Customize and configure the retry logic.
        BackoffType = DelayBackoffType.Exponential,
        MaxRetryAttempts = 5,
        UseJitter = true
    });

    // See: https://www.pollydocs.org/strategies/circuit-breaker.html
    builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
    {
        // Customize and configure the circuit breaker logic.
        SamplingDuration = TimeSpan.FromSeconds(10),
        FailureRatio = 0.2,
        MinimumThroughput = 3,
        ShouldHandle = static args =>
        {
            return ValueTask.FromResult(args is
            {
                Outcome.Result.StatusCode:
                    HttpStatusCode.RequestTimeout or
                        HttpStatusCode.TooManyRequests
            });
        }
    });

    // See: https://www.pollydocs.org/strategies/timeout.html
    builder.AddTimeout(TimeSpan.FromSeconds(5));
});

Предыдущий код:

  • Adds a resilience handler with the name "CustomPipeline" as the pipelineName to the service container.
  • Adds a retry strategy with exponential backoff, five retries, and jitter preference to the resilience builder.
  • Adds a circuit breaker strategy with a sampling duration of 10 seconds, a failure ratio of 0.2 (20%), a minimum throughput of three, and a predicate that handles RequestTimeout and TooManyRequests HTTP status codes to the resilience builder.
  • Adds a timeout strategy with a timeout of five seconds to the resilience builder.

Существует множество вариантов для каждой стратегии устойчивости. Дополнительные сведения см. в документации Полли : Стратегии. For more information about configuring ShouldHandle delegates, see Polly docs: Fault handling in reactive strategies.

Динамическая перезагрузка

Polly поддерживает динамическую перезагрузку настроенных стратегий устойчивости. Это означает, что можно изменить конфигурацию стратегий устойчивости во время выполнения. To enable dynamic reload, use the appropriate AddResilienceHandler overload that exposes the ResilienceHandlerContext. Given the context, call EnableReloads of the corresponding resilience strategy options:

httpClientBuilder.AddResilienceHandler(
    "AdvancedPipeline",
    static (ResiliencePipelineBuilder<HttpResponseMessage> builder,
        ResilienceHandlerContext context) =>
    {
        // Enable reloads whenever the named options change
        context.EnableReloads<HttpRetryStrategyOptions>("RetryOptions");

        // Retrieve the named options
        var retryOptions =
            context.GetOptions<HttpRetryStrategyOptions>("RetryOptions");

        // Add retries using the resolved options
        builder.AddRetry(retryOptions);
    });

Предыдущий код:

  • Adds a resilience handler with the name "AdvancedPipeline" as the pipelineName to the service container.
  • Enables the reloads of the "AdvancedPipeline" pipeline whenever the named RetryStrategyOptions options change.
  • Retrieves the named options from the IOptionsMonitor<TOptions> service.
  • Добавляет стратегию повторных попыток с полученными параметрами в модуль устойчивости.

Для получения дополнительных сведений см. документацию Полли: расширенное внедрение зависимостей.

В этом примере используется раздел параметров, который может изменяться, например файл appsettings.json . Рассмотрите следующий файл appsettings.json:

{
    "RetryOptions": {
        "Retry": {
            "BackoffType": "Linear",
            "UseJitter": false,
            "MaxRetryAttempts": 7
        }
    }
}

Теперь представьте, что эти параметры привязаны к конфигурации приложения, привязав его HttpRetryStrategyOptions к разделу "RetryOptions" :

var section = builder.Configuration.GetSection("RetryOptions");

builder.Services.Configure<HttpStandardResilienceOptions>(section);

Для получения дополнительной информации см. шаблон параметров в .NET.

Пример использования

Your app relies on dependency injection to resolve the ExampleClient and its corresponding HttpClient. The code builds the IServiceProvider and resolves the ExampleClient from it.

IHost host = builder.Build();

ExampleClient client = host.Services.GetRequiredService<ExampleClient>();

await foreach (Comment? comment in client.GetCommentsAsync())
{
    Console.WriteLine(comment);
}

Предыдущий код:

  • Создает IServiceProvider из ServiceCollection.
  • Resolves the ExampleClient from the IServiceProvider.
  • Вызывает метод GetCommentsAsync на ExampleClient, чтобы получить комментарии.
  • Записывает каждый комментарий в консоль.

Представьте себе ситуацию, когда сеть исчезнет или сервер не отвечает. На следующей схеме показано, как стратегии устойчивости будут обрабатывать ситуацию, учитывая ExampleClient и метод GetCommentsAsync:

Пример рабочего потока HTTP GET с конвейером устойчивости.

На приведенной выше схеме показано следующее:

  • Запрос ExampleClient HTTP GET отправляется в конечную точку /comments .
  • The HttpResponseMessage is evaluated:
    • Если ответ выполнен успешно (HTTP 200), возвращается ответ.
    • If the response is unsuccessful (HTTP non-200), the resilience pipeline employs the configured resilience strategies.

Хотя это простой пример, он демонстрирует, как стратегии устойчивости можно использовать для обработки временных ошибок. Дополнительные сведения смотрите в документации Полли: Стратегии.

Известные проблемы

В следующих разделах подробно описаны различные известные проблемы.

Совместимость с пакетом Grpc.Net.ClientFactory

Если вы используете Grpc.Net.ClientFactory версию 2.63.0 или более раннюю, то включение стандартных обработчиков устойчивости или резервирования для клиента gRPC может вызвать исключение во время выполнения. В частности, рассмотрим следующий пример кода:

services
    .AddGrpcClient<Greeter.GreeterClient>()
    .AddStandardResilienceHandler();

Предыдущий код приводит к следующему исключению:

System.InvalidOperationException: The ConfigureHttpClient method is not supported when creating gRPC clients. Unable to create client with name 'GreeterClient'.

Чтобы устранить эту проблему, рекомендуется обновить до Grpc.Net.ClientFactory версии 2.64.0 или более поздней.

Существует проверка во время сборки, которая проверяет, используете ли вы версию Grpc.Net.ClientFactory или более раннюю 2.63.0, и если да, создаётся предупреждение о компиляции. Предупреждение можно отключить, задав в файле проекта следующее свойство:

<PropertyGroup>
  <SuppressCheckGrpcNetClientFactoryVersion>true</SuppressCheckGrpcNetClientFactoryVersion>
</PropertyGroup>

Совместимость с .NET Application Insights

Если вы используете .NET Application Insights версии 2.22.0 или более поздней, то включение функций устойчивости в приложении может привести к тому, что все данные телеметрии Application Insights отсутствуют. Проблема возникает при регистрации функций устойчивости перед службами Application Insights. Рассмотрим следующий пример, вызывающий проблему:

// At first, we register resilience functionality.
services.AddHttpClient().AddStandardResilienceHandler();

// And then we register Application Insights. As a result, Application Insights doesn't work.
services.AddApplicationInsightsTelemetry();

Эту проблему можно устранить, обновив .NET Application Insights до версии 2.23.0 или более поздней. Если вы не можете выполнить обновление, зарегистрируйте службы Application Insights перед функциями устойчивости, как показано ниже. Это исправит проблему.

// We register Application Insights first, and now it will be working correctly.
services.AddApplicationInsightsTelemetry();
services.AddHttpClient().AddStandardResilienceHandler();