Condividi tramite


Creare app HTTP resilienti: modelli di sviluppo principali

Lo sviluppo di app HTTP affidabili che possono essere ripristinate da errori temporanei è un requisito comune. Questo articolo presuppone che sia già stato letto l'articolo Introduzione allo sviluppo di app resilienti, in quanto il presente documento estende i concetti di base illustrati. Per creare app HTTP resilienti, il pacchetto NuGet Microsoft.Extensions.Http.Resilience fornisce meccanismi di resilienza specifici per HttpClient. Questo pacchetto NuGet si basa sulla libreria Microsoft.Extensions.Resilience e Polly, un progetto open source diffuso. Per altre informazioni, vedere Polly.

Attività iniziali

Per usare modelli di resilienza nelle app HTTP, installare il pacchetto NuGet Microsoft.Extensions.Http.Resilience.

dotnet add package Microsoft.Extensions.Http.Resilience --version 8.0.0

Per altre informazioni, vedere dotnet add package o Gestire le dipendenze dei pacchetti nelle applicazioni .NET.

Aggiungere resilienza a un client HTTP

Per aggiungere resilienza a un HttpClient, concatenare una chiamata al tipo IHttpClientBuilder restituito dalla chiamata a uno dei metodi AddHttpClient disponibili. Per altre informazioni, vedere IHttpClientFactory con .NET.

Sono disponibili diverse estensioni incentrate sulla resilienza. Alcune sono standard e usano quindi varie procedure consigliate del settore e altre sono più personalizzabili. Quando si aggiunge la resilienza, è consigliabile aggiungere un solo gestore di resilienza per evitare un accumulo di gestori. Se è necessario aggiungere più gestori di resilienza, è consigliabile usare il metodo di estensione AddResilienceHandler, che consente di personalizzare le strategie di resilienza.

Importante

Tutti gli esempi all'interno di questo articolo si basano sull'API AddHttpClient, inclusa nella libreria Microsoft.Extensions.Http, che restituisce un'istanza di IHttpClientBuilder. L'istanza di IHttpClientBuilder viene usata per configurare HttpClient e aggiungere il gestore di resilienza.

Aggiungere un gestore di resilienza standard

Il gestore di resilienza standard usa più strategie di resilienza sovrapposte l'una sull'altra, con opzioni predefinite per inviare le richieste e gestire eventuali errori temporanei. Il gestore della resilienza standard viene aggiunto chiamando il metodo di estensione AddStandardResilienceHandler in un'istanza di IHttpClientBuilder.

var services = new ServiceCollection();

var httpClientBuilder = services.AddHttpClient<ExampleClient>(
    configureClient: static client =>
    {
        client.BaseAddress = new("https://jsonplaceholder.typicode.com");
    });

Il codice precedente:

  • Crea un'istanza di ServiceCollection.
  • Aggiunge un oggetto HttpClient per il tipo ExampleClient al contenitore di servizi.
  • Configura l'oggetto HttpClient da usare "https://jsonplaceholder.typicode.com" come indirizzo di base.
  • Crea l'oggetto httpClientBuilder usato in tutti gli altri esempi all'interno di questo articolo.

Un esempio più reale si basa sull'hosting, come quello descritto nell'articolo Host generico .NET. Usando il pacchetto NuGet Microsoft.Extensions.Hosting, esaminare l'esempio aggiornato seguente:

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");
    });

Il codice precedente è simile all'approccio di creazione manuale di ServiceCollection, ma si basa su Host.CreateApplicationBuilder() per creare un host che espone i servizi.

L'oggetto ExampleClient viene definito come segue:

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");
    }
}

Il codice precedente:

  • Definisce un tipo ExampleClient con un costruttore che accetta un HttpClient.
  • Espone un metodo GetCommentsAsync che invia una richiesta GET all'endpoint /comments e restituisce la risposta.

Il tipo Comment è definito come segue:

namespace Http.Resilience.Example;

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

Poiché è stato creato un oggetto IHttpClientBuilder (httpClientBuilder) e ora si è in grado di comprendere l'implementazione ExampleClient e il modello Comment corrispondente, esaminare l'esempio seguente:

httpClientBuilder.AddStandardResilienceHandler();

Il codice precedente aggiunge il gestore di resilienza standard a HttpClient. Analogamente alla maggior parte delle API di resilienza, esistono overload che consentono di personalizzare le opzioni predefinite e le strategie di resilienza applicate.

Impostazioni predefinite del gestore di resilienza standard

La configurazione predefinita concatena cinque strategie di resilienza nell'ordine seguente (dalla più esterna alla più interna):

Ordinamento Strategia Descrizione Defaults
1 Limitatore di frequenza La pipeline del limitatore di frequenza limita il numero massimo di richieste simultanee inviate alla dipendenza. Coda: 0
Permesso: 1_000
2 Timeout totale La pipeline di timeout totale delle richieste applica un timeout complessivo all'esecuzione, assicurandosi che la richiesta, inclusi i tentativi di ripetizione, non superi il limite configurato. Timeout totale: 30s
3 Riprova La pipeline di ripetizione dei tentativi ritenta la richiesta nel caso in cui la dipendenza sia lenta o restituisca un errore temporaneo. Numero massimo di tentativi: 3
Backoff: Exponential
Uso di jitter: true
Ritardo: 2s
4 Interruttore automatico L'interruttore blocca l'esecuzione se vengono rilevati troppi errori diretti o timeout. Rapporto errori: 10%
Velocità effettiva minima: 100
Durata campionamento: 30s
Durata interruzione: 5s
5 Timeout tentativi La pipeline di timeout dei tentativi limita la durata di ogni tentativo di richiesta e genera un'eccezione se viene superata. Timeout tentativi: 10s

Tentativi e interruttori di circuito

Le strategie di nuovi tentativi e interruttori di circuito gestiscono un set di codici di stato HTTP e eccezioni specifici. Prendere in considerazione i codici di stato HTTP seguenti:

  • HTTP 500 e superiore (errori del server)
  • HTTP 408 (timeout richiesta)
  • HTTP 429 (troppe richieste)

Inoltre, queste strategie gestiscono le eccezioni seguenti:

  • HttpRequestException
  • TimeoutRejectedException

Aggiungere un gestore di hedging standard

Il gestore di hedging standard esegue il wrapping dell'esecuzione della richiesta con un meccanismo di hedging standard. L'hedging ripete i tentativi per le richieste lente in parallelo.

Per usare il gestore dell'hedging standard, chiamare il metodo di estensione AddStandardHedgingHandler. L'esempio seguente configura ExampleClient per l'utilizzo del gestore di hedging standard.

httpClientBuilder.AddStandardHedgingHandler();

Il codice precedente aggiunge il gestore di hedging standard a HttpClient.

Impostazioni predefinite del gestore di hedging standard

L'hedging standard usa un pool di interruttori per garantire che gli endpoint non integri vengano esclusi. Per impostazione predefinita, la selezione del pool si basa sull'autorità URL (schema + host + porta).

Suggerimento

È consigliabile configurare la modalità di selezione delle strategie chiamando StandardHedgingHandlerBuilderExtensions.SelectPipelineByAuthority o StandardHedgingHandlerBuilderExtensions.SelectPipelineBy per scenari più avanzati.

Il codice precedente aggiunge il gestore di hedging standard a IHttpClientBuilder. La configurazione predefinita concatena cinque strategie di resilienza nell'ordine seguente (dalla più esterna alla più interna):

Ordinamento Strategia Descrizione Defaults
1 Timeout totale delle richieste La pipeline di timeout totale delle richieste applica un timeout complessivo all'esecuzione, assicurandosi che la richiesta, inclusi i tentativi di hedging, non superi il limite configurato. Timeout totale: 30s
2 Hedging La strategia di hedging esegue le richieste su più endpoint nel caso in cui la dipendenza sia lenta o restituisca un errore temporaneo. Il routing è un'opzione che per impostazione predefinita si limita a proteggere l'URL fornito dall'originale HttpRequestMessage. Numero minimo di tentativi: 1
Numero massimo di tentativi: 10
Ritardo: 2s
3 Limitatore di frequenza (per endpoint) La pipeline del limitatore di frequenza limita il numero massimo di richieste simultanee inviate alla dipendenza. Coda: 0
Permesso: 1_000
4 Interruttore (per endpoint) L'interruttore blocca l'esecuzione se vengono rilevati troppi errori diretti o timeout. Rapporto errori: 10%
Velocità effettiva minima: 100
Durata campionamento: 30s
Durata interruzione: 5s
5 Timeout tentativi (per endpoint) La pipeline di timeout dei tentativi limita la durata di ogni tentativo di richiesta e genera un'eccezione se viene superata. Timeout: 10s

Personalizzare la selezione della route del gestore di hedging

Quando si usa il gestore di hedging standard, è possibile personalizzare la modalità di selezione degli endpoint delle richieste chiamando varie estensioni sul tipo IRoutingStrategyBuilder. Ciò può essere utile per scenari come test A/B, in cui si vuole instradare una percentuale delle richieste a un endpoint diverso:

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

Il codice precedente:

  • Aggiunge il gestore di hedging all'oggetto IHttpClientBuilder.
  • Configura IRoutingStrategyBuilder per l'utilizzo del metodo ConfigureOrderedGroups per configurare i gruppi ordinati.
  • Aggiunge un oggetto EndpointGroup all'oggetto orderedGroup che instrada il 3% delle richieste all'endpoint https://example.net/api/experimental e il 97% delle richieste all'endpoint https://example.net/api/stable.
  • Configura IRoutingStrategyBuilder per l'uso del metodo ConfigureWeightedGroups per configurare

Per configurare un gruppo ponderato, chiamare il metodo ConfigureWeightedGroups sul tipo IRoutingStrategyBuilder. Nell'esempio seguente viene configurato IRoutingStrategyBuilder per l'utilizzo del metodo ConfigureWeightedGroups per configurare i gruppi ponderati.

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

Il codice precedente:

  • Aggiunge il gestore di hedging all'oggetto IHttpClientBuilder.
  • Configura IRoutingStrategyBuilder per l'utilizzo del metodo ConfigureWeightedGroups per configurare i gruppi ponderati.
  • Imposta SelectionMode su WeightedGroupSelectionMode.EveryAttempt.
  • Aggiunge un oggetto WeightedEndpointGroup all'oggetto weightedGroup che instrada il 33% delle richieste all'endpoint https://example.net/api/a e il 33% delle richieste all'endpoint https://example.net/api/b e il 33% delle richieste all'endpoint https://example.net/api/c.

Suggerimento

Il numero massimo di tentativi di hedging è direttamente correlato al numero di gruppi configurati. Ad esempio, se sono presenti due gruppi, il numero massimo di tentativi è due.

Per altre informazioni, vedere Polly docs: Hedging resilience strategy (documentazione di Polly: Strategia di resilienza tramite hedging).

È comune configurare un gruppo ordinato o un gruppo ponderato, ma è possibile configurare entrambi. L'uso di gruppi ordinati e ponderati è utile negli scenari in cui si vuole inviare una percentuale delle richieste a un endpoint diverso, come nel caso dei test A/B.

Aggiungere gestori di resilienza personalizzati

Per avere più controllo, è possibile personalizzare i gestori di resilienza usando l'API AddResilienceHandler. Questo metodo accetta un delegato che configura l'istanza di ResiliencePipelineBuilder<HttpResponseMessage> usata per creare le strategie di resilienza.

Per configurare un gestore di resilienza denominato, chiamare il metodo di estensione AddResilienceHandler con il nome del gestore. Nell'esempio seguente viene configurato un gestore di resilienza denominato chiamato "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));
});

Il codice precedente:

  • Aggiunge un gestore di resilienza con il nome "CustomPipeline" come pipelineName al contenitore di servizi.
  • Aggiunge una strategia di ripetizione dei tentativi con backoff esponenziale, cinque tentativi e preferenza di jitter al generatore di resilienza.
  • Aggiunge una strategia di interruttore con una durata di campionamento di 10 secondi, un rapporto di errore pari a 0,2 (20%), una velocità effettiva minima di tre e un predicato che gestisce i codici di stato RequestTimeout e TooManyRequests al generatore di resilienza.
  • Aggiunge una strategia di timeout con un timeout di cinque secondi al generatore di resilienza.

Sono disponibili molte opzioni per ognuna delle strategie di resilienza. Per altre informazioni, vedere Polly docs: Strategies (documentazione di Polly: Strategie). Per altre informazioni sulla configurazione dei delegati ShouldHandle, vedere Polly docs: Fault handling in reactive strategies (documentazione di Polly: Gestione degli errori nelle strategie reattive).

Ricaricamento dinamico

Polly supporta il ricaricamento dinamico delle strategie di resilienza configurate. Ciò significa che è possibile modificare la configurazione delle strategie di resilienza in fase di esecuzione. Per abilitare il ricaricamento dinamico, usare l'overload AddResilienceHandler appropriato che espone ResilienceHandlerContext. Dato il contesto, chiamare EnableReloads delle opzioni della strategia di resilienza corrispondenti:

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

Il codice precedente:

  • Aggiunge un gestore di resilienza con il nome "AdvancedPipeline" come pipelineName al contenitore di servizi.
  • Abilita i ricaricamenti della pipeline "AdvancedPipeline" ogni volta che cambiano le opzioni RetryStrategyOptions denominate.
  • Recupera le opzioni denominate dal servizio IOptionsMonitor<TOptions>.
  • Aggiunge una strategia di ripetizione dei tentativi con le opzioni recuperate al generatore di resilienza.

Per altre informazioni, vedere Polly docs: Advanced dependency injection (documentazione di Polly: inserimento di dipendenze avanzato).

Questo esempio si basa su una sezione delle opzioni che può essere modificata, ad esempio un file appsettings.json. Considerare il file appsettings.json seguente:

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

Si supponga ora che queste opzioni siano associate alla configurazione dell'app, che associa HttpRetryStrategyOptions alla sezione "RetryOptions":

var section = builder.Configuration.GetSection("RetryOptions");

builder.Services.Configure<HttpStandardResilienceOptions>(section);

Per altre informazioni, vedere Modello options in .NET.

Esempio di utilizzo

L'app si basa sull'inserimento delle dipendenze per risolvere ExampleClient e l'HttpClient corrispondente. Il codice compila IServiceProvider risolve ExampleClient da esso.

IHost host = builder.Build();

ExampleClient client = host.Services.GetRequiredService<ExampleClient>();

await foreach (Comment? comment in client.GetCommentsAsync())
{
    Console.WriteLine(comment);
}

Il codice precedente:

Si immagini uno scenario in cui la rete si arresta o il server non risponde. Il diagramma seguente illustra come le strategie di resilienza gestirebbero la situazione, con ExampleClient e il metodo GetCommentsAsync:

Flusso di lavoro HTTP GET di esempio con pipeline di resilienza.

Il diagramma precedente illustra:

  • ExampleClient invia una richiesta HTTP GET all'endpoint /comments.
  • Il messaggio HttpResponseMessage viene quindi valutato:
    • Se la risposta ha esito positivo (codice HTTP 200), viene restituita la risposta.
    • Se la risposta ha esito negativo (codice HTTP diverso da 200), la pipeline di resilienza usa le strategie di resilienza configurate.

Anche se si tratta di un semplice esempio, illustra come è possibile usare le strategie di resilienza per gestire gli errori temporanei. Per altre informazioni, vedere la Polly docs: Strategies (documentazione di Polly: Strategie).

Problemi noti

Le sezioni seguenti illustrano in dettaglio vari problemi noti.

Compatibilità con il pacchetto Grpc.Net.ClientFactory

Se si usa la versione Grpc.Net.ClientFactory 2.63.0 o precedente, l'abilitazione dei gestori di resilienza o hedging standard per un client gRPC potrebbe causare un'eccezione di runtime. In particolare, si consideri l'esempio di codice seguente:

services
    .AddGrpcClient<Greeter.GreeterClient>()
    .AddStandardResilienceHandler();

Il codice precedente genera l'eccezione seguente:

System.InvalidOperationException: The ConfigureHttpClient method is not supported when creating gRPC clients. Unable to create client with name 'GreeterClient'.

Per risolvere questo problema, è consigliabile eseguire l'aggiornamento alla versione Grpc.Net.ClientFactory 2.64.0 o successiva.

È presente un controllo in fase di compilazione che verifica se si usa la versione Grpc.Net.ClientFactory2.63.0 o versioni precedenti e se si è il controllo genera un avviso di compilazione. È possibile eliminare l'avviso impostando la proprietà seguente nel file di progetto:

<PropertyGroup>
  <SuppressCheckGrpcNetClientFactoryVersion>true</SuppressCheckGrpcNetClientFactoryVersion>
</PropertyGroup>

Compatibilità con Application Insights .NET

Se si usa Application Insights .NET, l'abilitazione della funzionalità di resilienza nell'applicazione potrebbe causare la mancanza di tutti i dati di telemetria di Application Insights. Il problema si verifica quando la funzionalità di resilienza viene registrata prima dei servizi di Application Insights. Si consideri l'esempio seguente che causa il 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();

Il problema è causato dal seguente bug in Application Insights e può essere risolto registrando i servizi di Application Insights prima della funzionalità di resilienza, come illustrato di seguito:

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