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 metodoConfigureOrderedGroups
per configurare i gruppi ordinati. - Aggiunge un oggetto
EndpointGroup
all'oggettoorderedGroup
che instrada il 3% delle richieste all'endpointhttps://example.net/api/experimental
e il 97% delle richieste all'endpointhttps://example.net/api/stable
. - Configura
IRoutingStrategyBuilder
per l'uso del metodoConfigureWeightedGroups
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 metodoConfigureWeightedGroups
per configurare i gruppi ponderati. - Imposta
SelectionMode
suWeightedGroupSelectionMode.EveryAttempt
. - Aggiunge un oggetto
WeightedEndpointGroup
all'oggettoweightedGroup
che instrada il 33% delle richieste all'endpointhttps://example.net/api/a
e il 33% delle richieste all'endpointhttps://example.net/api/b
e il 33% delle richieste all'endpointhttps://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"
comepipelineName
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
eTooManyRequests
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"
comepipelineName
al contenitore di servizi. - Abilita i ricaricamenti della pipeline
"AdvancedPipeline"
ogni volta che cambiano le opzioniRetryStrategyOptions
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:
- Compila IServiceProvider da ServiceCollection.
- Risolve
ExampleClient
da IServiceProvider. - Chiama il metodo
GetCommentsAsync
suExampleClient
per ottenere i commenti. - Scrive ogni commento nella console.
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
:
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.ClientFactory
2.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();