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


Создание устойчивых 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-клиенту

Чтобы добавить устойчивость к HttpClient объекту, необходимо вызвать метод IHttpClientBuilder, который возвращается при вызове любого из доступных AddHttpClient методов. Дополнительные сведения см. статью IHttpClientFactory с .NET.

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

Внимание

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

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

Стандартный обработчик устойчивости использует несколько сложенных друг на друга стратегий устойчивости, с параметрами по умолчанию для отправки запросов и обработки временных ошибок. Стандартный обработчик устойчивости добавляется через вызов AddStandardResilienceHandler метода расширения на экземпляре IHttpClientBuilder.

var services = new ServiceCollection();

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

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

  • Создает экземпляр ServiceCollection.
  • Добавляет элемент HttpClient для типа ExampleClient в контейнер службы.
  • Настраивает HttpClient для использования "https://jsonplaceholder.typicode.com" в качестве базового адреса.
  • Создает этот объект httpClientBuilder , используемый в других примерах в этой статье.

Более реальный пример будет зависеть от размещения, такого как описано в статье .NET Generic Host. Используя пакет 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();

В приведенный выше код добавляется стандартный обработчик устойчивости 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();

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

  • Создает экземпляр ServiceCollection.
  • Добавляет стандартный обработчик устойчивости ко всем экземплярам HttpClient.
  • Для HttpClient"custom" :
    • Удаляет все предопределенные обработчики устойчивости, которые были зарегистрированы ранее. Это полезно, если вы хотите начать с чистого листа, чтобы добавить собственные пользовательские стратегии.
    • Добавляет StandardHedgingHandler в HttpClient. Вы можете заменить AddStandardHedgingHandler() любой стратегией, которая соответствует потребностям приложения, таким как механизмы повторных попыток, выключатели цепи или другие методы устойчивости.

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

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

заказ Стратегия Описание Параметры по умолчанию
1 Ограничение скорости Механизм ограничения запросов ограничивает максимальное количество одновременных запросов, отправляемых зависимому компоненту. Очередь: 0
Разрешение: 1_000
2 Общий тайм-аут Общая система ограничения времени ожидания запроса применяет общее время ожидания к процессу выполнения, гарантируя, что запрос, включая повторные попытки, не превышает настроенное ограничение. Общее время ожидания: 30s
3 Повторить попытку Конвейер повторных попыток повторяет запрос в случае замедления зависимости или возвращает временную ошибку. Максимальное число повторных попыток: 3
Откат: Exponential
Используйте джиттер: true
Задержка:2s
4 Средство разбиения цепи Ломатель цепи блокирует выполнение, если обнаружено слишком много непосредственных отказов или таймаутов. Соотношение сбоев: 10 %
Минимальная пропускная способность: 100
Длительность выборки: 30s
Длительность перерыва: 5s
5 Тайм-аут попытки Конвейер с ограничением времени ожидания ограничивает длительность каждой попытки запроса и выдаёт исключение, если оно превышено. Тайм-аут ожидания попытки: 10 сек.

Повторные попытки и прерыватели цепи

Стратегии повторных попыток и останова цепи обрабатывают набор определенных кодов состояния 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();
});

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

Стандартный обработчик хеджирования оборачивает выполнение запроса стандартным механизмом хеджирования. Параллельное хеджирование замедляет повторные попытки выполнения запросов.

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

httpClientBuilder.AddStandardHedgingHandler();

В предыдущий код добавляется стандартный обработчик хеджирования в HttpClient.

Стандартные обработчики хеджирования по умолчанию

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

Подсказка

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

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

заказ Стратегия Описание Параметры по умолчанию
1 Общее время ожидания запроса Общая система ограничения времени ожидания запроса применяет общий тайм-аут на выполнение задачи, гарантируя, что запрос, включая попытки хеджирования, не превышает установленное ограничение. Общее время ожидания: 30s
2 Хеджирование Стратегия хеджирования выполняет запросы к нескольким конечным точкам в случае, если зависимость медленно работает или возвращает временную ошибку. Маршрутизация — это параметры, по умолчанию он просто хеджирует URL-адрес, предоставленный исходным HttpRequestMessage. Минимальное количество попыток: 1
Максимальное количество попыток: 10
Задержка: 2 с
3 Ограничение скорости (на конечную точку) Механизм ограничения запросов ограничивает максимальное количество одновременных запросов, отправляемых зависимому компоненту. Очередь: 0
Разрешение: 1_000
4 Разделитель цепи (на каждом конце) Ломатель цепи блокирует выполнение, если обнаружено слишком много непосредственных отказов или таймаутов. Соотношение сбоев: 10 %
Минимальная пропускная способность: 100
Длительность выборки: 30s
Длительность перерыва: 5s
5 Время ожидания истечения попытки (для каждой конечной точки) Конвейер с ограничением времени ожидания ограничивает длительность каждой попытки запроса и выдаёт исключение, если оно превышено. Время ожидания: 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 }
            }
        });
    });
});

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

  • Добавляет обработчик хеджирования в IHttpClientBuilder.
  • Настраивает IRoutingStrategyBuilder для использования метода ConfigureOrderedGroups при настройке упорядоченных групп.
  • Добавляет в EndpointGrouporderedGroup правило маршрутизации, которое направляет 3% запросов к конечной точке https://example.net/api/experimental и 97% запросов к конечной точке https://example.net/api/stable.
  • Настраивает IRoutingStrategyBuilder, чтобы использовать метод ConfigureWeightedGroups для настройки.

Чтобы настроить весовую группу, вызовите метод 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 }
            }
        });
    });
});

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

  • Добавляет обработчик хеджирования в IHttpClientBuilder.
  • Настраивает IRoutingStrategyBuilder для использования метода ConfigureWeightedGroups в настройке взвешенных групп.
  • Устанавливает SelectionMode на WeightedGroupSelectionMode.EveryAttempt.
  • Добавляет WeightedEndpointGroup в weightedGroup, который распределяет 33% запросов на конечную точку https://example.net/api/a, 33% запросов на конечную точку https://example.net/api/b и 33% запросов на конечную точку https://example.net/api/c.

Подсказка

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

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

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

Добавьте пользовательские обработчики устойчивости

Чтобы получить дополнительные возможности управления, можно настроить обработчики устойчивости с помощью 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));
});

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

  • Добавляет обработчик резилиентности с именем "CustomPipeline" в качестве pipelineName в контейнер службы.
  • Добавляет стратегию повторных попыток с экспоненциальной задержкой повторов, пятью попытками и предпочитаемым интервалом случайных изменений времени в механизм повышения надежности.
  • Добавляет стратегию размыкания цепи с периодом выборки 10 секунд, коэффициентом отказов 0,2 (20%), минимальным количеством операций в секунду, равным трем, а также предикат, обрабатывающий RequestTimeout и TooManyRequests коды состояния HTTP, к системе обеспечения устойчивости.
  • Добавляет стратегию тайм-аута с истечением 5 секунд в конструктор устойчивости.

Существует множество вариантов для каждой стратегии устойчивости. Дополнительные сведения см. в документации Полли : Стратегии. Для получения дополнительной информации о настройке ShouldHandle делегатов см. документацию Polly: Обработка ошибок в реактивных стратегиях.

Предупреждение

Если вы используете стратегии повторных попыток и времени ожидания, и вы хотите настроить ShouldHandle делегат в стратегии повторных попыток, убедитесь, следует ли обрабатывать исключение времени ожидания Polly. Полли бросает TimeoutRejectedException (который наследует от Exception), вместо стандартного TimeoutException.

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

Polly поддерживает динамическую перезагрузку настроенных стратегий устойчивости. Это означает, что можно изменить конфигурацию стратегий устойчивости во время выполнения. Чтобы включить динамическую перезагрузку, используйте соответствующую AddResilienceHandler перегрузку, которая открывает ResilienceHandlerContext. Учитывая контекст, вызовите EnableReloads соответствующих опций стратегии устойчивости.

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

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

  • Добавляет обработчик резилиентности с именем "AdvancedPipeline" в качестве pipelineName в контейнер службы.
  • Обеспечивает перезагрузку конвейера "AdvancedPipeline" при изменении именованных RetryStrategyOptions опций.
  • Извлекает именованные параметры из службы IOptionsMonitor<TOptions>.
  • Добавляет стратегию повторных попыток с полученными параметрами в модуль устойчивости.

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

В этом примере используется раздел параметров, который может изменяться, например файл 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.

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

Ваше приложение использует внедрение зависимостей для разрешения ExampleClient и соответствующего HttpClient. Код создает IServiceProvider и извлекает из него ExampleClient.

IHost host = builder.Build();

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

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

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

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

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

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

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

  • Запрос ExampleClient HTTP GET отправляется в конечную точку /comments .
  • Вычисляется HttpResponseMessage :
    • Если ответ выполнен успешно (HTTP 200), возвращается ответ.
    • Если ответ неуспешен (HTTP не 200), система устойчивости использует настроенные стратегии надежности.

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

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

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

Совместимость с пакетом 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();