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étodoConfigureOrderedGroups
para configurar os grupos ordenados. - Adiciona um
EndpointGroup
aoorderedGroup
que roteia 3% das solicitações para o ponto de extremidadehttps://example.net/api/experimental
e 97% das solicitações para o ponto de extremidadehttps://example.net/api/stable
. - Configura o método
IRoutingStrategyBuilder
para usar o métodoConfigureWeightedGroups
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étodoConfigureWeightedGroups
para configurar os grupos ponderados. - Define o
SelectionMode
comoWeightedGroupSelectionMode.EveryAttempt
. - Adiciona um
WeightedEndpointGroup
aoweightedGroup
que roteia 33% das solicitações para o ponto de extremidadehttps://example.net/api/a
, 33% das solicitações para o ponto de extremidadehttps://example.net/api/b
e 33% das solicitações para o ponto de extremidadehttps://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 opipelineName
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
eTooManyRequests
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 opipelineName
ao contêiner de serviço. - Habilita os recarregamentos do pipeline
"AdvancedPipeline"
sempre que as opções nomeadasRetryStrategyOptions
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:
- Compila o IServiceProvider a partir do ServiceCollection.
- Resolve o
ExampleClient
a partir do IServiceProvider. - Chama o método
GetCommentsAsync
noExampleClient
para obter os comentários. - Grava cada um dos comentários no console.
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
:
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();