Antipattern tempesta di tentativi
Quando un servizio non è disponibile o è occupato, la ripetizione troppo frequente dei tentativi di connessione da parte dei client può rallentare ulteriormente il ripristino del servizio e peggiorare il problema. Inoltre, non ha senso riprovare all'infinito, perché le richieste sono in genere valide solo per un periodo di tempo definito.
Descrizione del problema
Nel cloud talvolta i servizi riscontrano problemi e diventano non disponibili per i client o devono limitare i client o ridurne la velocità. Sebbene sia normale che i client tentino di ripetere le connessioni non riuscite ai servizi, è importante che non eseguano tentativi troppo frequentemente o troppo a lungo. È improbabile che i tentativi in un breve periodo di tempo abbiano esito positivo perché è probabile che i servizi non siano stati ripristinati. Inoltre, i servizi sono già sotto pressione quando vengono effettuati numerosi tentativi di connessione durante il tentativo di ripristino e tentativi di connessione ripetuti possono persino sovraccaricare il servizio e peggiorare il problema sottostante.
L'esempio seguente illustra uno scenario in cui un client si connette a un'API basata su server. Se la richiesta non riesce, il client riprova immediatamente e continua a riprovare per sempre. Spesso questo tipo di comportamento è più sottile rispetto a questo esempio, ma si applica lo stesso principio.
public async Task<string> GetDataFromServer()
{
while(true)
{
var result = await httpClient.GetAsync(string.Format("http://{0}:8080/api/...", hostName));
if (result.IsSuccessStatusCode) break;
}
// ... Process result.
}
Come risolvere il problema
Le applicazioni client devono seguire alcune procedure consigliate per evitare di causare una tempesta di tentativi.
- Impostare un limite per il numero di tentativi e non continuare a riprovare per un lungo periodo di tempo. Anche se può sembrare facile scrivere un ciclo
while(true)
, quasi certamente non si vuole effettivamente riprovare per un lungo periodo di tempo, poiché è probabile che la situazione che ha portato all'avvio della richiesta sia cambiata. Nella maggior parte delle applicazioni è sufficiente riprovare per alcuni secondi o minuti. - Eseguire una pausa tra i tentativi. Se un servizio non è disponibile, è improbabile che un nuovo tentativo immediato abbia esito positivo. Aumentare gradualmente la quantità di tempo di attesa tra i tentativi, ad esempio usando una strategia di backoff esponenziale.
- Gestire normalmente gli errori. Se il servizio non risponde, valutare se è opportuno interrompere il tentativo e restituire un errore all'utente o al chiamante del componente. Prendere in considerazione questi scenari di errore durante la progettazione dell'applicazione.
- Valutare l'utilizzo del modello interruttore, progettato appositamente per evitare nuove tempeste di tentativi.
- Se il server fornisce un'intestazione della risposta
retry-after
, assicurarsi di non eseguire alcun tentativo fino a quando non è trascorso il periodo di tempo specificato. - Usare gli SDK ufficiali durante la comunicazione con i servizi di Azure. Questi SDK hanno in genere criteri di ripetizione dei tentativi predefiniti e protezioni per evitare di causare o peggiorare tempeste di tentativi. Se si comunica con un servizio che non ha un SDK o in cui l'SDK non gestisce correttamente la logica di ripetizione dei tentativi, è consigliabile usare una libreria come Polly (per .NET) o retry (per JavaScript) per gestire correttamente la logica di ripetizione dei tentativi ed evitare di scrivere il codice manualmente.
- Se è in esecuzione un ambiente che la supporta, usare una mesh di servizi (o un altro livello di astrazione) per inviare chiamate in uscita. In genere questi strumenti, ad esempio Dapr, supportano i criteri di ripetizione dei tentativi e seguono automaticamente le procedure consigliate, ad esempio il back off dopo tentativi ripetuti. Questo approccio significa che non è necessario scrivere manualmente il codice di ripetizione dei tentativi.
- Valutare la possibilità di creare un batch di richieste e di usare i pool di richieste, ove disponibili. Molti SDK gestiscono la creazione di batch di richieste e i pool di connessioni per conto dell'utente, riducendo così il numero totale di tentativi di connessione in uscita eseguiti dall'applicazione, anche se è comunque necessario prestare attenzione a non ripetere queste connessioni con una frequenza eccessiva.
Anche i servizi devono proteggersi dalle tempeste di tentativi.
- Aggiungere un livello gateway per poter arrestare le connessioni durante un evento imprevisto. Questo è un esempio del modello Bulkhead. Azure offre molti servizi gateway diversi per diversi tipi di soluzioni, tra cui Frontdoor, Gateway applicazione e Gestione API.
- Limitare le richieste a livello del gateway per garantire che non venga accettato un numero di richieste così elevato da compromettere il funzionamento dei componenti back-end.
- In caso di limitazione delle richieste, inviare un'intestazione
retry-after
per consentire ai client di comprendere quando potranno riprendere i tentativi di connessione.
Considerazioni
- I client devono valutare il tipo di errore restituito. Alcuni tipi di errore non indicano un errore del servizio, ma indicano invece che il client ha inviato una richiesta non valida. Se, ad esempio, un'applicazione client riceve una risposta di errore
400 Bad Request
, è probabile che la ripetizione della stessa richiesta non sia utile perché il server indica che la richiesta non è valida. - I client devono valutare l'intervallo di tempo opportuno per provare a ripetere i tentativi di connessione. Il periodo di tempo per l'esecuzione dei tentativi sarà basato sui requisiti aziendali e sulla possibilità di propagare o meno un errore a un utente o a un chiamante. Nella maggior parte delle applicazioni è sufficiente riprovare per alcuni secondi o minuti.
Come rilevare il problema
Dal punto di vista di un client, i sintomi di questo problema possono includere tempi di risposta o di elaborazione molto lunghi, insieme ai dati di telemetria che indicano tentativi di connessione ripetuti.
Dal punto di vista di un servizio, i sintomi di questo problema possono includere un numero elevato di richieste da un client in un breve periodo di tempo o un numero elevato di richieste da un singolo client durante il ripristino da eventuali interruzioni del servizio. I sintomi possono anche includere difficoltà durante il ripristino del servizio o continui errori a catena del servizio subito dopo la correzione di un errore.
Diagnosi di esempio
Le sezioni seguenti illustrano un approccio al rilevamento di una potenziale tempesta di tentativi, sia sul lato client che sul lato servizio.
Identificazione dai dati di telemetria del client
Azure Application Insights registra i dati di telemetria dalle applicazioni e rende disponibili i dati per l'esecuzione di query e la visualizzazione. Le connessioni in uscita vengono rilevate come dipendenze e le relative informazioni sono accessibili e rappresentate in un grafico per identificare quando un client effettua un numero elevato di richieste in uscita allo stesso servizio.
Il grafico seguente è stato tratto dalla scheda Metriche all'interno del portale di Application Insights e visualizza la metrica Errori di dipendenza suddivisa per Remote dependency name (Nome dipendenza remota). Viene illustrato uno scenario in cui si è riscontrato un numero elevato (oltre 21.000) di tentativi di connessione non riusciti a una dipendenza in un breve periodo di tempo.
Identificazione dai dati di telemetria del server
Le applicazioni server potrebbero riuscire a rilevare un numero elevato di connessioni da un singolo client. Nell'esempio seguente il servizio Frontdoor di Azure funge da gateway per un'applicazione ed è stato configurato per registrare tutte le richieste a un'area di lavoro Log Analytics.
È possibile eseguire la query Kusto seguente su Log Analytics. Identificherà gli indirizzi IP client che hanno inviato un numero elevato di richieste all'applicazione nell'ultimo giorno.
AzureDiagnostics
| where ResourceType == "FRONTDOORS" and Category == "FrontdoorAccessLog"
| where TimeGenerated > ago(1d)
| summarize count() by bin(TimeGenerated, 1h), clientIp_s
| order by count_ desc
L'esecuzione di questa query durante una tempesta di tentativi mostra un numero elevato di tentativi di connessione da un singolo indirizzo IP.