Partilhar via


Crie aplicativos HTTP resilientes: principais padrões de desenvolvimento

A criação de aplicativos HTTP robustos que podem se recuperar de erros de falha transitórios é um requisito comum. Este artigo pressupõe que você já tenha lido Introdução ao desenvolvimento resiliente de aplicativos, 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. Este pacote NuGet depende da Microsoft.Extensions.Resilience biblioteca e do Polly, que é um projeto de código aberto popular. Para obter mais informações, consulte Polly.

Começar agora

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 package add ou Manage package dependencies in .NET applications.

Adicionar resiliência a um cliente HTTP

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

Existem várias extensões centradas na resiliência disponíveis. 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 empilhamento de manipuladores. Se você precisar adicionar vários manipuladores de resiliência, considere o uso do AddResilienceHandler método extension, que permite personalizar as estratégias de resiliência.

Importante

Todos os exemplos neste artigo dependem da API AddHttpClient, da biblioteca de Microsoft.Extensions.Http, que retorna uma instância IHttpClientBuilder. A IHttpClientBuilder instância é usada para configurar e adicionar o HttpClient 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 umas sobre as outras, com opções padrão para enviar as solicitações e lidar com quaisquer erros transitórios. O manipulador de resiliência padrão é adicionado chamando um 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 ServiceCollection instância.
  • Adiciona um HttpClient para o tipo ExampleClient ao contenedor de serviços.
  • Configura o HttpClient para usar "https://jsonplaceholder.typicode.com" como o endereço base.
  • Cria o httpClientBuilder que é usado em todos os outros exemplos deste artigo.

Um exemplo mais real dependeria de hospedagem, como o descrito no artigo .NET Generic Host . 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 ServiceCollection, mas, em vez disso, depende do Host.CreateApplicationBuilder() para construir um anfitrião que expõe os serviços.

O ExampleClient é definido da seguinte forma:

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 ExampleClient tipo que tem um construtor que aceita um HttpClient.
  • Expõe um método GetCommentsAsync que envia um pedido GET ao endpoint /comments e retorna a resposta.

O Comment tipo é definido da seguinte forma:

namespace Http.Resilience.Example;

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

Uma vez que criou um IHttpClientBuilder (httpClientBuilder) e agora compreende a implementação e o modelo correspondente ExampleClient, 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.

Remover manipuladores de resiliência padrão

Há um método RemoveAllResilienceHandlers que remove todos os manipuladores de resiliência registrados anteriormente. É útil quando você precisa limpar gestores de resiliência existentes para adicionar o seu personalizado. O exemplo a seguir demonstra como configurar um HttpClient personalizado usando o método AddHttpClient, remover todas as estratégias de resiliência predefinidas e substituí-las por novos manipuladores. Essa abordagem permite que você limpe as configurações existentes e defina novas de acordo com seus requisitos específicos.

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

O código anterior:

  • Cria uma ServiceCollection instância.
  • Adiciona o manipulador de resiliência padrão a todas as instâncias HttpClient.
  • Para o "personalizado" HttpClient:
    • Remove todos os manipuladores de resiliência predefinidos que foram registrados anteriormente. Isso é útil quando você deseja começar com um estado limpo para adicionar suas próprias estratégias personalizadas.
    • Adiciona um StandardHedgingHandler ao HttpClient. Você pode substituir AddStandardHedgingHandler() por qualquer estratégia que atenda às necessidades do seu aplicativo, como mecanismos de repetição, disjuntores ou outras técnicas de resiliência.

Configurações padrão do manipulador de resiliência

A configuração padrão encadeia cinco estratégias de resiliência na seguinte ordem (da mais externa para a mais interna):

Encomenda Estratégia Descrição Predefinições
1 Limitador de taxa A pipeline do rate limiter limita o número máximo de pedidos simultâneos enviados para a dependência. Fila: 0
Permissão: 1_000
2 Tempo total de espera O "pipeline" de tempo limite total do pedido aplica um tempo limite geral à execução, garantindo que o pedido, incluindo tentativas de nova execução, não exceda o limite configurado. Tempo limite total: 30s
3 Tentar novamente O fluxo de repetição reitera a solicitação caso o sistema dependente seja lento ou retorne um erro transitório. Máximo de tentativas: 3
Recuar: Exponential
Use a instabilidade: true
Atraso:2s
4 Disjuntor O disjuntor bloqueia a execução se forem detetadas muitas falhas diretas ou expirações de tempo. Taxa de falha: 10%
Rendimento mínimo: 100
Duração da amostragem: 30s
Duração do intervalo: 5s
5 Tempo limite para a tentativa O pipeline de prazo de tentativa limita a duração de cada tentativa de solicitação e gera um erro se for excedido. Tempo limite para a tentativa: 10s

Tentativas e disjuntores de circuito

As estratégias de repetição e de circuit breaker lidam com um conjunto de códigos de status HTTP específicos 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 (Demasiados pedidos)

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

  • HttpRequestException
  • TimeoutRejectedException

Desabilitar novas tentativas para uma determinada lista de métodos HTTP

Por padrão, o manipulador de resiliência padrão é configurado para fazer novas tentativas para todos os métodos HTTP. Para algumas aplicações, tal comportamento pode ser indesejável ou até prejudicial. Por exemplo, se uma solicitação POST insere um novo registro em um banco de dados, fazer novas tentativas para tal solicitação pode levar à duplicação de dados. Se você precisar desabilitar novas tentativas para uma determinada lista de métodos HTTP, você pode usar o método DisableFor(HttpRetryStrategyOptions, HttpMethod[]):

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

Como alternativa, você pode usar o método DisableForUnsafeHttpMethods(HttpRetryStrategyOptions), que desabilita novas tentativas para solicitações POST, PATCH, PUT, DELETEe CONNECT. De acordo com RFC, estes métodos são considerados inseguros; o que significa que a sua semântica não é só de leitura:

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

Adicionar manipulador de cobertura de risco padrão

O manipulador de hedging padrão envolve a execução da solicitação com um mecanismo de hedging padrão. As repetições de cobertura aceleram pedidos lentos em paralelo.

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

httpClientBuilder.AddStandardHedgingHandler();

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

Configurações padrão do manipulador de gestão de risco

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

Sugestão

É recomendável configurar 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 gestor de cobertura padrão ao IHttpClientBuilder. A configuração padrão encadeia cinco estratégias de resiliência na seguinte ordem (da mais externa para a mais interna):

Encomenda Estratégia Descrição Predefinições
1 Tempo limite total do pedido O pipeline de tempo limite global aplica um tempo limite geral à execução, garantindo que a solicitação, incluindo tentativas de proteção, não exceda o limite configurado. Tempo limite total: 30s
2 Estratégia de cobertura A estratégia de hedging executa as solicitações em vários endpoints caso a dependência seja lenta ou retorne um erro transitório. O roteamento é opcional, por padrão, ele apenas gerencia a URL fornecida pelo original HttpRequestMessage. Tentativas mínimas: 1
Máximo de tentativas: 10
Atraso: 2s
3 Limitador de taxa (por ponto final) A pipeline do rate limiter limita o número máximo de pedidos simultâneos enviados para a dependência. Fila: 0
Permissão: 1_000
4 Disjuntor (por extremidade) O disjuntor bloqueia a execução se forem detetadas muitas falhas diretas ou expirações de tempo. Taxa de falha: 10%
Rendimento mínimo: 100
Duração da amostragem: 30s
Duração do intervalo: 5s
5 Tempo limite de tentativa (por endpoint) O pipeline de prazo de tentativa limita a duração de cada tentativa de solicitação e gera um erro se for excedido. Tempo limite: 10s

Personalizar a seleção de rotas do gestor de cobertura

Ao usar o gestor de cobertura padrão, pode personalizar a maneira como os pontos de extremidade da solicitação são selecionados chamando várias extensões do tipo IRoutingStrategyBuilder. Isso pode ser útil para cenários como testes A/B, em que você deseja rotear uma porcentagem 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 hedging ao IHttpClientBuilder.
  • Configura o IRoutingStrategyBuilder para usar o método ConfigureOrderedGroups para configurar os grupos ordenados.
  • Adiciona um EndpointGroup ao orderedGroup que encaminha 3% das solicitações para o https://example.net/api/experimental ponto final e 97% das solicitações para o https://example.net/api/stable ponto final.
  • Configura o 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 ConfigureWeightedGroups método 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 hedging 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 encaminha 33% das solicitações para o https://example.net/api/a endpoint, 33% das solicitações para o https://example.net/api/b endpoint e 33% das solicitações para o https://example.net/api/c endpoint.

Sugestão

O número máximo de tentativas de hedge está diretamente correlacionado com o 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 Polly docs: Hedging resilience strategy.

É 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 dos testes A/B.

Adicionar manipuladores de resiliência personalizados

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

Para configurar um manipulador de resiliência nomeado, chame o AddResilienceHandler método de extensão 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 para o contêiner de serviço.
  • Adiciona ao construtor de resiliência uma estratégia de repetição com backoff exponencial, cinco tentativas e preferência de jitter.
  • Adiciona uma estratégia de disjuntor com uma duração de amostragem de 10 segundos, uma taxa de falha de 0,2 (20%), uma largura de banda 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.

Existem muitas opções disponíveis para cada uma das estratégias de resiliência. Para obter mais informações, consulte os documentos Polly : Estratégias. Para obter mais informações sobre como configurar ShouldHandle delegados, consulte a documentação Polly: Gestão de falhas em estratégias reativas.

Advertência

Se estiver a usar as estratégias de repetição e limite de tempo e quiser configurar o delegado ShouldHandle na sua estratégia de nova tentativa, certifique-se de considerar se ele deve lidar com a exceção de limite de tempo do Polly. Polly lança um TimeoutRejectedException (que herda de Exception), em vez do padrão TimeoutException.

Recarregamento dinâmico

Polly suporta 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 em tempo de execução. Para ativar a recarga dinâmica, use a sobrecarga apropriada AddResilienceHandler que expõe o ResilienceHandlerContext. Dado o contexto, invoque EnableReloads as 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 para o contêiner de serviço.
  • Permite recarregar o "AdvancedPipeline" pipeline quando as opções nomeadas RetryStrategyOptions são alteradas.
  • Recupera as opções nomeadas do IOptionsMonitor<TOptions> serviço.
  • Adiciona ao construtor de resiliência uma estratégia de repetição com as opções recuperadas.

Para obter mais informações, consulte Documentação Polly: Injeção de dependências avançada.

Este exemplo depende de uma seção de opções que pode ser alterada, como um arquivo appsettings.json . Considere o seguinte arquivo appsettings.json :

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

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

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 utilização

A sua aplicação depende da injeção de dependências para resolver o ExampleClient e o seu correspondente 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 cai ou o servidor deixa de responder. O diagrama a seguir mostra como as estratégias de resiliência lidariam com a situação, dado o ExampleClient e o GetCommentsAsync método:

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

O diagrama anterior mostra:

  • O ExampleClient envia uma solicitação HTTP GET para o /comments endpoint.
  • 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 emprega as estratégias de resiliência configuradas.

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

Problemas conhecidos

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

Compatibilidade com o Grpc.Net.ClientFactory pacote

Se estiver a usar a versão Grpc.Net.ClientFactory2.63.0 ou anterior, habilitar os manipuladores de resiliência ou cobertura padrão para um cliente gRPC pode causar uma exceção de execução. 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 a Grpc.Net.ClientFactory versão 2.64.0 ou posterior.

Há uma verificação no tempo de compilação que verifica se está a usar a versão Grpc.Net.ClientFactory 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 versão 2.22.0 ou inferior, habilitar a funcionalidade de resiliência em seu aplicativo pode fazer com que toda a telemetria do Application Insights esteja 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 pode ser corrigido atualizando o .NET Application Insights para a versão 2.23.0 ou superior. Se não for possível atualizá-lo, registrar os serviços do Application Insights antes da funcionalidade de resiliência, conforme mostrado abaixo, corrigirá o problema:

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