Compartilhar via


Criar aplicativos HTTP resilientes: principais padrões de desenvolvimento

Criar aplicativos HTTP robustos que podem se recuperar de erros de falha transitórios é um requisito comum. Este artigo pressupõe que você já leu a Introdução ao desenvolvimento de aplicativos resilientes, pois este artigo estende os principais conceitos transmitidos. Para ajudar a criar aplicativos HTTP resilientes, o pacote NuGet Microsoft.Extensions.Http.Resilience fornece mecanismos de resiliência especificamente para o HttpClient. Esse pacote NuGet depende da biblioteca Microsoft.Extensions.Resilience e da biblioteca Polly, que é um projeto popular de código aberto. Para obter mais informações, consulte Polly.

Introdução

Para usar padrões de resiliência em aplicativos HTTP, instale o pacote NuGet Microsoft.Extensions.Http.Resilience.

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

Para obter mais informações, consulte dotnet add package ou Gerenciar dependências de pacotes em aplicativos .NET.

Adicionar resiliência a um cliente HTTP

Para adicionar resiliência a um HttpClient, encadeie uma chamada no tipo IHttpClientBuilder retornado da chamada de qualquer um dos métodos AddHttpClient disponíveis. Para obter mais informações, confira IHttpClientFactory com .NET.

Há várias extensões disponíveis centradas em resiliência. Alguns são padrão, empregando assim várias práticas recomendadas do setor e outros são mais personalizáveis. Ao adicionar resiliência, você deve adicionar apenas um manipulador de resiliência e evitar o empilhamento de manipuladores. Se você precisar adicionar vários manipuladores de resiliência, considere usar o método de extensão AddResilienceHandler, que permite personalizar as estratégias de resiliência.

Importante

Todos os exemplos neste artigo dependem da API AddHttpClient, da biblioteca Microsoft.Extensions.Http, que retorna uma instância IHttpClientBuilder. A instância IHttpClientBuilder é usada para configurar o HttpClient e adicionar o manipulador de resiliência.

Adicionar manipulador de resiliência padrão

O manipulador de resiliência padrão usa várias estratégias de resiliência empilhadas entre si, com opções padrão para enviar as solicitações e lidar com erros transitórios. O manipulador de resiliência padrão é adicionado chamando o método de extensão AddStandardResilienceHandler em uma instância IHttpClientBuilder.

var services = new ServiceCollection();

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

O código anterior:

  • Cria uma instância ServiceCollection.
  • Adiciona um HttpClient para o tipo ExampleClient ao contêiner de serviço.
  • Configura o HttpClient para usar "https://jsonplaceholder.typicode.com" como o endereço base.
  • Cria o httpClientBuilder que é usado em todos os outros exemplos neste artigo.

Um exemplo mais real dependeria da hospedagem, como a descrita no artigo do Host Genérico .NET. Usando o pacote NuGet Microsoft.Extensions.Hosting, considere o seguinte exemplo atualizado:

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

O código anterior é semelhante à abordagem de criação manual de ServiceCollection, mas depende do Host.CreateApplicationBuilder() para criar um host que exponha os serviços.

O ExampleClient é definida da seguinte maneira:

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

O código anterior:

  • Define um tipo ExampleClient que tem um construtor que aceita um HttpClient.
  • Expõe um método GetCommentsAsync que envia uma solicitação GET para o ponto de extremidade /comments e retorna a resposta.

Este tipo de Comment é definido da seguinte forma:

namespace Http.Resilience.Example;

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

Considerando que você criou um IHttpClientBuilder (httpClientBuilder) e agora entende a implementação de ExampleClient e o modelo de Comment correspondente, considere o seguinte exemplo:

httpClientBuilder.AddStandardResilienceHandler();

O código anterior adiciona o manipulador de resiliência padrão ao HttpClient. Como a maioria das APIs de resiliência, há sobrecargas que permitem personalizar as opções padrão e as estratégias de resiliência aplicadas.

Padrões de manipulador de resiliência padrão

A configuração padrão encadeia cinco estratégias de resiliência na seguinte ordem (do mais externo ao mais interno):

Pedido Estratégia Descrição Padrões
1 Limitador de taxa O pipeline do limitador de taxa limita o número máximo de solicitações simultâneas enviadas para a dependência. Fila: 0
Permitir: 1_000
2 Tempo limite total O pipeline de tempo limite total da solicitação aplica um tempo limite geral à execução, garantindo que a solicitação, incluindo tentativas de repetição, não exceda o limite configurado. Tempo limite total: 30 segundos
3 Repetir O pipeline de repetição tenta novamente a solicitação caso a dependência seja lenta ou retorne um erro transitório. Máximo de tentativas: 3
Retirada: Exponential
Usar jitter: true
Atraso: 2 segundos
4 Disjuntor O disjuntor bloqueia a execução se forem detectadas muitas falhas diretas ou tempos limite. Taxa de falha: 10%
Taxa de transferência mínima: 100
Duração da amostragem: 30 segundos
Duração da pausa: 5 segundos
5 Tempo limite da tentativa O pipeline de tempo limite de tentativa limita a duração de cada tentativa de solicitação e é gerado se ele for excedido. Tempo limite da tentativa: 10 segundos

Repetições e disjuntores

As estratégias de tentativa e de disjuntor lidam com um conjunto específico de códigos de status HTTP e exceções. Considere os seguintes códigos de status HTTP:

  • HTTP 500 e superior (erros do servidor)
  • HTTP 408 (Tempo limite da solicitação)
  • HTTP 429 (Muitas solicitações)

Além disso, essas estratégias lidam com as seguintes exceções:

  • HttpRequestException
  • TimeoutRejectedException

Adicionar manipulador de cobertura padrão

O manipulador de cobertura padrão encapsula a execução da solicitação com um mecanismo de cobertura padrão. Esse mecanismo repete solicitações lentas em paralelo.

Para usar o manipulador de cobertura padrão, chame o método de extensão AddStandardHedgingHandler. O exemplo a seguir configura o ExampleClient para usar o manipulador de cobertura padrão para usar.

httpClientBuilder.AddStandardHedgingHandler();

O código anterior adiciona o manipulador de cobertura padrão ao HttpClient.

Padrões do manipulador de cobertura padrão

A cobertura padrão usa um pool de disjuntores para garantir que pontos de extremidade não íntegros não sejam protegidos. Por padrão, a seleção do pool é baseada na autoridade de URL (esquema + host + porta).

Dica

É recomendável que você configure a maneira como as estratégias são selecionadas chamando StandardHedgingHandlerBuilderExtensions.SelectPipelineByAuthority ou StandardHedgingHandlerBuilderExtensions.SelectPipelineBy para cenários mais avançados.

O código anterior adiciona o manipulador de cobertura padrão ao IHttpClientBuilder. A configuração padrão encadeia cinco estratégias de resiliência na seguinte ordem (do mais externo ao mais interno):

Pedido Estratégia Descrição Padrões
1 Tempo limite total da solicitação O pipeline de tempo limite total da solicitação aplica um tempo limite geral à execução, garantindo que a solicitação, incluindo tentativas de cobertura, não exceda o limite configurado. Tempo limite total: 30 segundos
2 Hedging A estratégia de cobertura executa as solicitações em vários pontos de extremidade caso a dependência seja lenta ou retorne um erro transitório. O roteamento é uma opção, por padrão, ele apenas protege a URL fornecida pelo original HttpRequestMessage. Mínimo de tentativas: 1
Máximo de tentativas: 10
Atraso: 2 segundos
3 Limitador de taxa (por ponto de extremidade) O pipeline do limitador de taxa limita o número máximo de solicitações simultâneas enviadas para a dependência. Fila: 0
Permitir: 1_000
4 Disjuntor (por ponto de extremidade) O disjuntor bloqueia a execução se forem detectadas muitas falhas diretas ou tempos limite. Taxa de falha: 10%
Taxa de transferência mínima: 100
Duração da amostragem: 30 segundos
Duração da pausa: 5 segundos
5 Tempo limite da tentativa (por ponto de extremidade) O pipeline de tempo limite de tentativa limita a duração de cada tentativa de solicitação e é gerado se ele for excedido. Tempo limite: 10 segundos

Personalizar a seleção de rota do manipulador de cobertura

Ao usar o manipulador de cobertura padrão, você pode personalizar a maneira como os pontos de extremidade de solicitação são selecionados chamando várias extensões no tipo IRoutingStrategyBuilder. Isso pode ser útil para cenários como o teste A/B, em que você deseja rotear um percentual das solicitações para um ponto de extremidade diferente:

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

O código anterior:

  • Adiciona o manipulador de cobertura ao IHttpClientBuilder.
  • Configura o IRoutingStrategyBuilder para usar o método ConfigureOrderedGroups para configurar os grupos ordenados.
  • Adiciona um EndpointGroup ao orderedGroup que roteia 3% das solicitações para o ponto de extremidade https://example.net/api/experimental e 97% das solicitações para o ponto de extremidade https://example.net/api/stable.
  • Configura o método IRoutingStrategyBuilder para usar o método ConfigureWeightedGroups para configurar o

Para configurar um grupo ponderado, chame o método ConfigureWeightedGroups no tipo IRoutingStrategyBuilder. O exemplo a seguir configura o IRoutingStrategyBuilder para usar o método ConfigureWeightedGroups para configurar os grupos ponderados.

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

O código anterior:

  • Adiciona o manipulador de cobertura ao IHttpClientBuilder.
  • Configura o IRoutingStrategyBuilder para usar o método ConfigureWeightedGroups para configurar os grupos ponderados.
  • Define o SelectionMode como WeightedGroupSelectionMode.EveryAttempt.
  • Adiciona um WeightedEndpointGroup ao weightedGroup que roteia 33% das solicitações para o ponto de extremidade https://example.net/api/a, 33% das solicitações para o ponto de extremidade https://example.net/api/b e 33% das solicitações para o ponto de extremidade https://example.net/api/c.

Dica

O número máximo de tentativas de cobertura correlaciona-se diretamente ao número de grupos configurados. Por exemplo, se você tiver dois grupos, o número máximo de tentativas será dois.

Para obter mais informações, consulte a Documentação do Polly: estratégia de resiliência de cobertura.

É comum configurar um grupo ordenado ou um grupo ponderado, mas é válido configurar ambos. O uso de grupos ordenados e ponderados é útil em cenários em que você deseja enviar uma porcentagem das solicitações para um ponto de extremidade diferente, como é o caso do teste A/B.

Adicionar manipuladores de resiliência personalizados

Para ter mais controle, você pode personalizar os manipuladores de resiliência usando a API AddResilienceHandler. Esse método aceita um delegado que configura a instância ResiliencePipelineBuilder<HttpResponseMessage>usada para criar as estratégias de resiliência.

Para configurar um manipulador de resiliência nomeado, chame o método de extensão AddResilienceHandler com o nome do manipulador. O exemplo a seguir configura um manipulador de resiliência nomeado chamado "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));
});

O código anterior:

  • Adiciona um manipulador de resiliência com o nome "CustomPipeline" como o pipelineName ao contêiner de serviço.
  • Adiciona uma estratégia de repetição com retirada exponencial, cinco repetições e preferência de tremulação ao construtor de resiliência.
  • Adiciona uma estratégia de disjuntor (circuit breaker) com uma duração de amostragem de 10 segundos, uma taxa de falha de 0,2 (20%), uma taxa de transferência mínima de três e um predicado que manipula os códigos de status HTTP RequestTimeout e TooManyRequests para o construtor de resiliência.
  • Adiciona uma estratégia de tempo limite com um tempo limite de cinco segundos ao construtor de resiliência.

Há muitas opções disponíveis para cada uma das estratégias de resiliência. Para obter mais informações, consulte a Documentação do Polly: estratégias. Para obter mais informações sobre como configurar delegados ShouldHandle, consulte a Documentação do Polly: tratamento de falhas em estratégias reativas.

Recarga dinâmica

O Polly dá suporte à recarga dinâmica das estratégias de resiliência configuradas. Isso significa que você pode alterar a configuração das estratégias de resiliência no tempo de execução. Para habilitar a recarga dinâmica, use a sobrecarga AddResilienceHandler apropriada que expõe o ResilienceHandlerContext. Considerando o contexto, chame EnableReloads das opções de estratégia de resiliência correspondentes:

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

O código anterior:

  • Adiciona um manipulador de resiliência com o nome "AdvancedPipeline" como o pipelineName ao contêiner de serviço.
  • Habilita os recarregamentos do pipeline "AdvancedPipeline" sempre que as opções nomeadas RetryStrategyOptions forem alteradas.
  • Recupera as opções nomeadas do serviço IOptionsMonitor<TOptions>.
  • Adiciona uma estratégia de repetição com as opções recuperadas ao construtor de resiliência.

Para obter mais informações, consulte a Documentação do Polly: injeção avançada de dependência.

Este exemplo se baseia em uma seção de opções que é capaz de alterar, como um arquivo appsettings.json. Usando o seguinte arquivo appsettings.json:

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

Agora imagine que essas opções estavam associadas à configuração do aplicativo, associando o HttpRetryStrategyOptions à seção "RetryOptions":

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

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

Para obter mais informações, consulte Padrão de opções no .NET.

Exemplo de uso

Seu aplicativo depende da injeção de dependência para resolver o ExampleClient e seu respectivo HttpClient. O código cria o IServiceProvider e resolve o ExampleClient a partir dele.

IHost host = builder.Build();

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

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

O código anterior:

Imagine uma situação em que a rede fica inoperante ou o servidor fica sem resposta. O diagrama a seguir mostra como as estratégias de resiliência lidariam com a situação, considerando o ExampleClient e o método GetCommentsAsync:

Exemplo de fluxo de trabalho HTTP GET com pipeline de resiliência.

O diagrama anterior ilustra:

  • O ExampleClient envia uma solicitação HTTP GET para o ponto de extremidade /comments.
  • O HttpResponseMessage é avaliado:
    • Se a resposta for bem-sucedida (HTTP 200), a resposta será retornada.
    • Se a resposta não for bem-sucedida (HTTP não 200), o pipeline de resiliência empregará as estratégias de resiliência configuradas.

Embora este seja um exemplo simples, ele demonstra como as estratégias de resiliência podem ser usadas para lidar com erros transitórios. Para obter mais informações, consulte a Documentação do Polly: estratégias.

Problemas conhecidos

As seções a seguir detalham vários problemas conhecidos.

Compatibilidade com o pacote Grpc.Net.ClientFactory

Se você estiver usando Grpc.Net.ClientFactory versão 2.63.0 ou anterior, habilitar a resiliência padrão ou os manipuladores de hedge para um cliente gRPC poderá causar uma exceção de runtime. Especificamente, considere o seguinte exemplo de código:

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

O código anterior resulta na seguinte exceção:

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

Para resolver esse problema, recomendamos atualizar para Grpc.Net.ClientFactory versão 2.64.0 ou posterior.

Há uma verificação de tempo de compilação que verifica se você está usando Grpc.Net.ClientFactory versão 2.63.0 ou anterior e, se estiver, a verificação produz um aviso de compilação. Você pode suprimir o aviso definindo a seguinte propriedade no arquivo de projeto:

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

Compatibilidade com o .NET Application Insights

Se você estiver usando o .NET Application Insights, habilitar a funcionalidade de resiliência em seu aplicativo poderá fazer com que toda a telemetria do Application Insights fique ausente. O problema ocorre quando a funcionalidade de resiliência é registrada antes dos serviços do Application Insights. Considere o seguinte exemplo que está causando o problema:

// 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();

O problema é causado pelo seguinte bug no Application Insights. Ele pode ser corrigido registrando os serviços do Application Insights antes da funcionalidade de resiliência, conforme mostrado abaixo:

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