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 biblioteca e do Microsoft.Extensions.Resilience 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 add package ou Manage package dependencies in .NET applications.

Adicionar resiliência a um cliente HTTP

Para adicionar resiliência a um , encadeie IHttpClientBuilder uma chamada no tipo que é retornado da chamada de qualquer um HttpClientdos métodos disponíveisAddHttpClient. 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 AddHttpClient da API, da biblioteca Microsoft.Extensions.Http , que retorna uma IHttpClientBuilder instância. 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 o AddStandardResilienceHandler método de extensão em uma IHttpClientBuilder instância.

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 ExampleClient tipo 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 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 criar um host 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 HttpClientarquivo .
  • Expõe um GetCommentsAsync método que envia uma solicitação GET para o /comments ponto de extremidade 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);

Dado que você criou um IHttpClientBuilder (httpClientBuilder) e agora entende a implementação e o ExampleClient modelo correspondente Comment , 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 do manipulador de resiliência padrão

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

Ordenar Estratégia Description Defaults
1 Limitador de taxa O pipeline do limitador de taxa limita o número máximo de solicitações simultâneas que estão sendo enviadas para a dependência. Fila: 0
Licença: 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: 30s
3 Tentar novamente 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
Recuo: Exponential
Use jitter: true
Atraso:2s
4 Disjuntor O disjuntor bloqueia a execução se forem detetadas muitas falhas diretas ou tempos limites. Taxa de falha: 10%
Rendimento mínimo: 100
Duração da amostragem: 30s
Duração do intervalo: 5s
5 Tempo limite da tentativa O pipeline de tempo limite de tentativa limita a duração de cada tentativa de solicitação e lança se for excedido. Tempo limite de tentativa: 10s

Repetições e disjuntores

As estratégias de repetição e disjuntor 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

Adicionar manipulador de cobertura padrão

O manipulador de cobertura padrão envolve a execução da solicitação com um mecanismo de cobertura padrão. Hedging retenta solicitações lentas em paralelo.

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

httpClientBuilder.AddStandardHedgingHandler();

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

Padrões padrão do manipulador de cobertura

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

Gorjeta

É 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 manipulador 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):

Ordenar Estratégia Description Defaults
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: 30s
2 Cobertura 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. Roteamento é opções, por padrão ele apenas protege a URL fornecida pelo original HttpRequestMessage. Tentativas mínimas: 1
Máximo de tentativas: 10
Atraso: 2s
3 Limitador de taxa (por ponto final) O pipeline do limitador de taxa limita o número máximo de solicitações simultâneas que estão sendo enviadas para a dependência. Fila: 0
Licença: 1_000
4 Disjuntor (por ponto final) O disjuntor bloqueia a execução se forem detetadas muitas falhas diretas ou tempos limites. Taxa de falha: 10%
Rendimento mínimo: 100
Duração da amostragem: 30s
Duração do intervalo: 5s
5 Tempo limite de tentativa (por ponto de extremidade) O pipeline de tempo limite de tentativa limita a duração de cada tentativa de solicitação e lança se for excedido. Tempo limite: 10s

Personalizar a seleção de rotas 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 IRoutingStrategyBuilder tipo. 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 cobertura ao IHttpClientBuilder.
  • Configura o IRoutingStrategyBuilder para usar o ConfigureOrderedGroups método para configurar os grupos ordenados.
  • Adiciona um EndpointGroup ao que encaminha orderedGroup 3% das solicitações para o https://example.net/api/experimental ponto de extremidade e 97% das solicitações para o https://example.net/api/stable ponto de extremidade.
  • Configura o IRoutingStrategyBuilder para usar o ConfigureWeightedGroups método para configurar o

Para configurar um grupo ponderado, chame o ConfigureWeightedGroups IRoutingStrategyBuilder método no tipo. 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 cobertura ao IHttpClientBuilder.
  • Configura o IRoutingStrategyBuilder para usar o ConfigureWeightedGroups método para configurar os grupos ponderados.
  • Define o SelectionMode como WeightedGroupSelectionMode.EveryAttempt.
  • Adiciona a WeightedEndpointGroup ao que encaminha weightedGroup 33% das solicitações para o https://example.net/api/a ponto de extremidade, 33% das solicitações para o https://example.net/api/b ponto de extremidade e 33% das solicitações para o https://example.net/api/c ponto de extremidade.

Gorjeta

O número máximo de tentativas de cobertura 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 uma estratégia de repetição com backoff exponencial, cinco tentativas e preferência de jitter ao construtor de resiliência.
  • Adiciona uma estratégia de disjuntor 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 códigos TooManyRequests de RequestTimeout status HTTP 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 Polly docs: Tratamento de falhas em estratégias reativas.

Recarga dinâmica

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 habilitar a recarga dinâmica, use a sobrecarga apropriada AddResilienceHandler que expõe o ResilienceHandlerContext. Dado o contexto, apelo EnableReloads às 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 as recargas do "AdvancedPipeline" pipeline sempre que as opções nomeadas RetryStrategyOptions são alteradas.
  • Recupera as opções nomeadas do IOptionsMonitor<TOptions> serviço.
  • Adiciona uma estratégia de repetição com as opções recuperadas ao construtor de resiliência.

Para obter mais informações, consulte Polly docs: Advanced dependency injection.

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

Seu aplicativo depende da injeção de dependência para resolver o ExampleClient e 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 ponto de extremidade.
  • Avalia-se HttpResponseMessage :
    • 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, 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 você estiver usando Grpc.Net.ClientFactory a versão 2.63.0 ou anterior, habilitar os manipuladores de resiliência ou cobertura padrão para um cliente gRPC pode causar uma exceção de tempo 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 de tempo de compilação que verifica se você está usando Grpc.Net.ClientFactory a versão 2.63.0 ou anterior, e se você está 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 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 é causado pelo seguinte bug no Application Insights e 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();