Compartir a través de


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étodo ConfigureOrderedGroups para configurar los grupos ordenados.
  • Agrega un EndpointGroup al orderedGroup que enruta el 3 % de las solicitudes al punto de conexión https://example.net/api/experimental y el 97 % de las solicitudes al punto de conexión https://example.net/api/stable.
  • Configura el IRoutingStrategyBuilder para usar el método ConfigureWeightedGroups 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étodo ConfigureWeightedGroups para configurar los grupos ponderados.
  • Establece SelectionMode en WeightedGroupSelectionMode.EveryAttempt.
  • Agrega un WeightedEndpointGroup al weightedGroup que enruta el 33 % de las solicitudes al punto de conexión https://example.net/api/a, el 33 % de las solicitudes al punto de conexión https://example.net/api/b y el 33 % de las solicitudes al punto de conexión https://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 el pipelineName 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 y TooManyRequests 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 el pipelineName al contenedor de servicios.
  • Habilita las recargas de la canalización "AdvancedPipeline" cada vez que cambian las opciones RetryStrategyOptions 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:

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:

Flujo de trabajo HTTP GET de ejemplo con canalización de resistencia.

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