Compilación de aplicaciones HTTP resistentes: patrones de desarrollo de claves
La compilación de aplicaciones HTTP sólidas que pueden recuperarse de errores transitorios es un requisito común. En este artículo se da por supuesto que ya ha leído Introducción al desarrollo de aplicaciones resistentes, ya que en este artículo se amplían los conceptos básicos que se han transmitido. Para ayudar a compilar aplicaciones HTTP resistentes, el paquete NuGet Microsoft.Extensions.Http.Resilience proporciona mecanismos de resistencia específicamente para el HttpClient. Este paquete NuGet se basa en la biblioteca Microsoft.Extensions.Resilience
y Polly, que es un proyecto de código abierto popular. Para obtener más información, consulte Polly.
Introducción
Para usar patrones de resistencia en aplicaciones HTTP, instale el paquete NuGet Microsoft.Extensions.Http.Resilience.
dotnet add package Microsoft.Extensions.Http.Resilience --version 8.0.0
Para obtener más información, consulte dotnet add package o Administración de dependencias de paquetes en aplicaciones .NET.
Adición de resistencia a un cliente HTTP
Para agregar resistencia a un HttpClient, se encadena una llamada al tipo IHttpClientBuilder que se devuelve al llamar a cualquiera de los métodos AddHttpClient disponibles. Para obtener más información, vea IHttpClientFactory con .NET.
Hay varias extensiones centradas en la resistencia disponibles. Algunas son estándar, por lo que emplean varios procedimientos recomendados del sector, y otras son más personalizables. Al agregar resistencia, solo debe agregar un controlador de resistencia y evitar controladores de apilamiento. Si necesita agregar varios controladores de resistencia, debe considerar el uso del método de extensión AddResilienceHandler
, lo que le permite personalizar las estrategias de resistencia.
Importante
Todos los ejemplos de este artículo se basan en la API AddHttpClient, de la biblioteca Microsoft.Extensions.Http, que devuelve una instancia IHttpClientBuilder. La instancia IHttpClientBuilder se usa para configurar el HttpClient y agregar el controlador de resistencia.
Adición de un controlador de resistencia estándar
El controlador de resistencia estándar usa varias estrategias de resistencia apiladas entre sí, con opciones predeterminadas para enviar las solicitudes y controlar los errores transitorios. El controlador de resistencia estándar se agrega llamando al método de extensión AddStandardResilienceHandler
en una instancia IHttpClientBuilder.
var services = new ServiceCollection();
var httpClientBuilder = services.AddHttpClient<ExampleClient>(
configureClient: static client =>
{
client.BaseAddress = new("https://jsonplaceholder.typicode.com");
});
El código anterior:
- Crea una instancia de ServiceCollection.
- Agrega un HttpClient para el tipo
ExampleClient
al contenedor de servicios. - Configura el HttpClient para usar
"https://jsonplaceholder.typicode.com"
como dirección base. - Crea el
httpClientBuilder
que se usa en los otros ejemplos de este artículo.
Un ejemplo más real se basaría en el hospedaje, como el descrito en el artículo Host genérico de .NET. Con el paquete NuGet Microsoft.Extensions.Hosting, considere el siguiente ejemplo actualizado:
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");
});
El código anterior es similar al enfoque de creación manual ServiceCollection
, pero en su lugar se basa en el Host.CreateApplicationBuilder() para compilar un host que expone los servicios.
El ExampleClient
se define de la siguiente manera:
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");
}
}
El código anterior:
- Define un tipo
ExampleClient
que tiene un constructor que acepta un HttpClient. - Expone un método
GetCommentsAsync
que envía una solicitud GET al punto de conexión/comments
y devuelve la respuesta.
El tipo Comment
se define de la siguiente manera:
namespace Http.Resilience.Example;
public record class Comment(
int PostId, int Id, string Name, string Email, string Body);
Dado que ha creado un IHttpClientBuilder (httpClientBuilder
) y ahora comprende la implementación de ExampleClient
y el modelo correspondiente de Comment
, considere el siguiente ejemplo:
httpClientBuilder.AddStandardResilienceHandler();
El código anterior agrega el controlador de resistencia estándar a HttpClient. Al igual que la mayoría de las API de resistencia, hay sobrecargas que permiten personalizar las opciones predeterminadas y las estrategias de resistencia aplicadas.
Valores predeterminados del controlador de resistencia estándar
La configuración predeterminada encadena cinco estrategias de resistencia en el siguiente orden (desde el exterior hasta el más interno):
Orden | Estrategia | Descripción | Defaults |
---|---|---|---|
1 | Limitador de velocidad | La canalización del limitador de velocidad limita el número máximo de solicitudes simultáneas que se envían a la dependencia. | Queue: 0 Permiso: 1_000 |
2 | Tiempo de expiración total | La canalización total de tiempo de espera de solicitud aplica un tiempo de espera general a la ejecución, lo que garantiza que la solicitud, incluidos los intentos de reintento, no supere el límite configurado. | Tiempo de expiración total: 30s |
3 | Volver a intentar | La canalización de reintento reintenta la solicitud en caso de que la dependencia sea lenta o devuelva un error transitorio. | Número máximo de reintentos: 3 Retroceso: Exponential Usar vibración: true Retraso:2s |
4 | Interruptor automático | El interruptor bloquea la ejecución si se detectan demasiados errores directos o tiempos de espera. | Proporción de errores: 10 % Rendimiento mínimo: 100 Duración del muestreo: 30s Duración de descanso: 5s |
5 | Tiempo de espera de intento | La canalización de tiempo de espera de intento limita la duración de cada intento de solicitud e inicia si se supera. | Tiempo de espera de intento: 10s |
Reintentos y disyuntores
Las estrategias de reintentos y disyuntor controlan un conjunto de códigos de estado HTTP y excepciones específicos. Tenga en cuenta los siguientes códigos de estado HTTP:
- HTTP 500 y versiones posteriores (errores del servidor)
- HTTP 408 (tiempo de espera de solicitud)
- HTTP 429 (demasiadas solicitudes)
Además, estas estrategias controlan las siguientes excepciones:
HttpRequestException
TimeoutRejectedException
Adición de un controlador de cobertura estándar
El controlador de cobertura estándar ajusta la ejecución de la solicitud con un mecanismo de cobertura estándar. La cobertura reintenta las solicitudes lentas en paralelo.
Para usar el controlador de cobertura estándar, llame al método de extensión AddStandardHedgingHandler
. En el siguiente ejemplo se configura el ExampleClient
para usar el controlador de cobertura estándar.
httpClientBuilder.AddStandardHedgingHandler();
El código anterior agrega el controlador de cobertura estándar al HttpClient.
Valores predeterminados del controlador de cobertura estándar
La cobertura estándar usa un grupo de disyuntores para asegurarse de que los puntos de conexión incorrectos no están cubiertos. De forma predeterminada, la selección del grupo se basa en la entidad de dirección URL (esquema + host + puerto).
Sugerencia
Se recomienda configurar la forma en que se seleccionan las estrategias mediante una llamada a StandardHedgingHandlerBuilderExtensions.SelectPipelineByAuthority
o StandardHedgingHandlerBuilderExtensions.SelectPipelineBy
para escenarios más avanzados.
El código anterior agrega el controlador de cobertura estándar al IHttpClientBuilder. La configuración predeterminada encadena cinco estrategias de resistencia en el siguiente orden (desde el exterior hasta el más interno):
Orden | Estrategia | Descripción | Defaults |
---|---|---|---|
1 | Tiempo de espera total de solicitud | La canalización total de tiempo de espera de solicitud aplica un tiempo de espera general a la ejecución, lo que garantiza que la solicitud, incluidos los intentos de cobertura, no supere el límite configurado. | Tiempo de expiración total: 30s |
2 | Cobertura | La estrategia de cobertura ejecuta las solicitudes en varios puntos de conexión en caso de que la dependencia sea lenta o devuelva un error transitorio. El enrutamiento es opcional, de forma predeterminada solo cubre la dirección URL proporcionada por el HttpRequestMessage original. | Intentos mínimos: 1 Intentos máximos: 10 Retraso: 2s |
3 | Limitador de velocidad (por punto de conexión) | La canalización del limitador de velocidad limita el número máximo de solicitudes simultáneas que se envían a la dependencia. | Queue: 0 Permiso: 1_000 |
4 | Interruptor (por punto de conexión) | El interruptor bloquea la ejecución si se detectan demasiados errores directos o tiempos de espera. | Proporción de errores: 10 % Rendimiento mínimo: 100 Duración del muestreo: 30s Duración de descanso: 5s |
5 | Tiempo de espera de intento (por punto de conexión) | La canalización de tiempo de espera de intento limita la duración de cada intento de solicitud e inicia si se supera. | Tiempo de expiración: 10s |
Personalización de la selección de rutas de controlador de cobertura
Al usar el controlador de cobertura estándar, puede personalizar la forma en que se seleccionan los puntos de conexión de solicitud llamando a varias extensiones en el tipo IRoutingStrategyBuilder
. Esto puede ser útil para escenarios como las pruebas A/B, en las que desea dirigir un porcentaje de las solicitudes a un punto de conexión 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 }
}
});
});
});
El código anterior:
- Agrega el controlador de cobertura al IHttpClientBuilder.
- Configura el
IRoutingStrategyBuilder
para usar el métodoConfigureOrderedGroups
para configurar los grupos ordenados. - Agrega un
EndpointGroup
alorderedGroup
que enruta el 3 % de las solicitudes al punto de conexiónhttps://example.net/api/experimental
y el 97 % de las solicitudes al punto de conexiónhttps://example.net/api/stable
. - Configura el
IRoutingStrategyBuilder
para usar el métodoConfigureWeightedGroups
para configurar el
Para configurar un grupo ponderado, llame al método ConfigureWeightedGroups
en el tipo IRoutingStrategyBuilder
. En el siguiente ejemplo se configura el IRoutingStrategyBuilder
para usar el método ConfigureWeightedGroups
para configurar los 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 }
}
});
});
});
El código anterior:
- Agrega el controlador de cobertura al IHttpClientBuilder.
- Configura el
IRoutingStrategyBuilder
para usar el métodoConfigureWeightedGroups
para configurar los grupos ponderados. - Establece
SelectionMode
enWeightedGroupSelectionMode.EveryAttempt
. - Agrega un
WeightedEndpointGroup
alweightedGroup
que enruta el 33 % de las solicitudes al punto de conexiónhttps://example.net/api/a
, el 33 % de las solicitudes al punto de conexiónhttps://example.net/api/b
y el 33 % de las solicitudes al punto de conexiónhttps://example.net/api/c
.
Sugerencia
El número máximo de intentos de cobertura se correlaciona directamente con el número de grupos configurados. Por ejemplo, si tiene dos grupos, el número máximo de intentos es dos.
Para más información, consulte Documentación de Polly: Estrategia de resistencia de cobertura.
Es habitual configurar un grupo ordenado o un grupo ponderado, pero es válido configurar ambos. El uso de grupos ordenados y ponderados es útil en escenarios en los que desea enviar un porcentaje de las solicitudes a un punto de conexión diferente, como es el caso de las pruebas A/B.
Adición de controladores de resistencia personalizados
Para tener más control, puede personalizar los controladores de resistencia mediante la API AddResilienceHandler
. Este método acepta un delegado que configura la instancia ResiliencePipelineBuilder<HttpResponseMessage>
que se usa para crear las estrategias de resistencia.
Para configurar un controlador de resistencia con nombre, llame al método de extensión AddResilienceHandler
con el nombre del controlador. En el siguiente ejemplo se configura un controlador de resistencia denominado "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));
});
El código anterior:
- Agrega un controlador de resistencia con el nombre
"CustomPipeline"
como elpipelineName
al contenedor de servicios. - Agrega una estrategia de reintento con retroceso exponencial, cinco reintentos y preferencia de vibración al generador de resistencia.
- Agrega una estrategia de disyuntor con una duración de muestreo de 10 segundos, una proporción de errores de 0,2 (20 %), un rendimiento mínimo de tres y un predicado que controla los códigos de estado HTTP
RequestTimeout
yTooManyRequests
al generador de resistencia. - Agrega una estrategia de tiempo de espera con un tiempo de espera de cinco segundos al generador de resistencia.
Hay muchas opciones disponibles para cada una de las estrategias de resistencia. Para obtener más información, consulte Documentación de Polly: Estrategias. Para obtener más información sobre la configuración de delegados ShouldHandle
, consulte Documentación de Polly: Control de errores en estrategias reactivas.
Recarga dinámica
Polly admite la recarga dinámica de las estrategias de resistencia configuradas. Esto significa que puede cambiar la configuración de las estrategias de resistencia en tiempo de ejecución. Para habilitar la recarga dinámica, use la sobrecarga AddResilienceHandler
adecuada que expone el ResilienceHandlerContext
. Dado el contexto, llame a EnableReloads
de las opciones de estrategia de resistencia correspondientes:
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);
});
El código anterior:
- Agrega un controlador de resistencia con el nombre
"AdvancedPipeline"
como elpipelineName
al contenedor de servicios. - Habilita las recargas de la canalización
"AdvancedPipeline"
cada vez que cambian las opcionesRetryStrategyOptions
con nombre. - Recupera las opciones con nombre del servicio IOptionsMonitor<TOptions>.
- Agrega una estrategia de reintento con las opciones recuperadas al generador de resistencia.
Para obtener más información, consulte Documentación de Polly: inserción de dependencias avanzadas.
Este ejemplo se basa en una sección de opciones que es capaz de cambiar, como un archivo appsettings.json. Fíjese en el siguiente archivo appsettings.json:
{
"RetryOptions": {
"Retry": {
"BackoffType": "Linear",
"UseJitter": false,
"MaxRetryAttempts": 7
}
}
}
Ahora imagine que estas opciones estaban enlazadas a la configuración de la aplicación, enlazando el HttpRetryStrategyOptions
a la sección "RetryOptions"
:
var section = builder.Configuration.GetSection("RetryOptions");
builder.Services.Configure<HttpStandardResilienceOptions>(section);
Para obtener más información, consulte Patrón de opciones en .NET.
Ejemplo de uso
La aplicación se basa en la inserción de dependencias para resolver y el ExampleClient
y su HttpClient correspondiente. El código compila el IServiceProvider y resuelve el ExampleClient
a partir de él.
IHost host = builder.Build();
ExampleClient client = host.Services.GetRequiredService<ExampleClient>();
await foreach (Comment? comment in client.GetCommentsAsync())
{
Console.WriteLine(comment);
}
El código anterior:
- Compila el IServiceProvider a partir del ServiceCollection.
- Resuelve el
ExampleClient
a partir del IServiceProvider. - Llama al método
GetCommentsAsync
en elExampleClient
para obtener los comentarios. - Escribe cada comentario en la consola.
Imagine una situación en la que la red deja de funcionar o el servidor deja de responder. En el siguiente diagrama se muestra cómo las estrategias de resistencia controlarían la situación, dado el método ExampleClient
y el GetCommentsAsync
:
El diagrama anterior muestra:
- El
ExampleClient
envía una solicitud HTTP GET al punto de conexión/comments
. - El HttpResponseMessage se evalúa:
- Si la respuesta es correcta (HTTP 200), se devuelve la respuesta.
- Si la respuesta no es correcta (HTTP no 200), la canalización de resistencia emplea las estrategias de resistencia configuradas.
Aunque se trata de un ejemplo sencillo, muestra cómo se pueden usar las estrategias de resistencia para controlar errores transitorios. Para obtener más información, consulte Documentación de Polly: Estrategias.
Problemas conocidos
En las secciones siguientes se describen varios problemas conocidos.
Compatibilidad con el paquete Grpc.Net.ClientFactory
Si usa Grpc.Net.ClientFactory
en su versión 2.63.0
o versiones anteriores, al habilitar los controladores de cobertura o resistencia estándar para un cliente gRPC podría generar una excepción en tiempo de ejecución. Fíjese en concreto en el siguiente ejemplo de código:
services
.AddGrpcClient<Greeter.GreeterClient>()
.AddStandardResilienceHandler();
El código anterior da como resultado la siguiente excepción:
System.InvalidOperationException: The ConfigureHttpClient method is not supported when creating gRPC clients. Unable to create client with name 'GreeterClient'.
Para resolver este problema, se recomienda actualizar Grpc.Net.ClientFactory
a la versión 2.64.0
o posterior.
Hay una comprobación del tiempo de compilación que verifica si usa Grpc.Net.ClientFactory
en su versión 2.63.0
o anterior y, si hay comprobación, se crea una advertencia de compilación. Puede suprimir la advertencia creando la siguiente propiedad en el archivo del proyecto:
<PropertyGroup>
<SuppressCheckGrpcNetClientFactoryVersion>true</SuppressCheckGrpcNetClientFactoryVersion>
</PropertyGroup>
Compatibilidad con Application Insights de .NET
Si usa Application Insights de .NET, habilitar la funcionalidad de resistencia en la aplicación podría hacer que falte toda la telemetría de Application Insights. El problema se produce cuando la funcionalidad de resistencia se registra antes que los servicios de Application Insights. Tenga en cuenta el ejemplo siguiente que causa el 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();
El problema se debe al siguiente error en Application Insights y se puede corregir registrando los servicios de Application Insights antes que la funcionalidad de resistencia, como se muestra a continuación:
// We register Application Insights first, and now it will be working correctly.
services.AddApplicationInsightsTelemetry();
services.AddHttpClient().AddStandardResilienceHandler();