Modello di ripetizione dei tentativi

Azure

È possibile abilitare un'applicazione per gestire gli errori temporanei durante il tentativo di connessione a un servizio o a una risorsa di rete ritentando in modo trasparente un'operazione non riuscita. Ciò può migliorare la stabilità dell'applicazione.

Contesto e problema

Un'applicazione che comunica con gli elementi in esecuzione nel cloud deve essere sensibile agli errori temporanei che possono verificarsi in questo ambiente. Gli errori includono la perdita momentanea della connettività di rete a componenti e servizi, l'indisponibilità temporanea di un servizio o i timeout che si verificano quando un servizio è occupato.

Questi errori si risolvono di solito automaticamente e, se l'azione che ha generato l'errore viene ripetuta dopo un intervallo di tempo appropriato, è probabile che abbia esito positivo. Ad esempio, un servizio di database che elabora un numero elevato di richieste simultanee può implementare una strategia di limitazione che rifiuta temporaneamente eventuali altre richieste fino a quando il carico di lavoro non è stato facilitato. Un'applicazione che tenta di accedere al database in questa situazione potrebbe non riuscire a connettersi, ma un tentativo successivo potrebbe invece avere esito positivo.

Soluzione

Nel cloud, gli errori temporanei sono abbastanza comuni e un'applicazione deve essere progettata per gestirli in modo elegante e trasparente. Si riducono così i possibili effetti degli errori sulle attività aziendali eseguite dall'applicazione.

Se un'applicazione rileva un errore durante il tentativo di inviare una richiesta a un servizio remoto, può adottare le strategie seguenti per gestire l'errore:

  • Annullare. Se l'errore indica che non si tratta di un problema temporaneo o che è improbabile che l'operazione abbia esito positivo se ripetuta, l'applicazione deve annullare l'operazione e segnalare un'eccezione. Ad esempio, è improbabile che un errore di autenticazione causato dalla specifica di credenziali non valide abbia esito positivo indipendentemente dal numero di tentativi.

  • Nuovo tentativo. Se l'errore specifico segnalato è insolito o raro, potrebbe essere stato causato da circostanze non comuni, ad esempio un pacchetto di rete danneggiato durante la trasmissione. In questo caso, l'applicazione può ritentare immediatamente la richiesta in errore, perché è improbabile che si ripeta lo stesso errore ed è invece probabile che la richiesta venga completata.

  • Riprovare dopo un certo intervallo di tempo. Se l'errore è causato da uno degli errori di più comuni di connettività o di servizio occupato, potrebbe essere necessario aspettare un breve periodo di tempo per fare in modo che i problemi di connettività vengano risolti o che venga completato il backlog di lavoro. In questo caso, l'applicazione deve attendere per un periodo di tempo appropriato prima di ritentare la richiesta.

Per gli errori temporanei più comuni, è necessario scegliere il periodo tra i tentativi per distribuire le richieste da più istanze dell'applicazione nel modo più uniforme possibile. In questo modo si riduce la possibilità che un servizio occupato continui a essere sovraccaricato. Se molte istanze di un'applicazione sovraccaricano continuamente un servizio con richieste di nuovi tentativi, il servizio impiegherà più tempo per il ripristino.

Se la richiesta continua a non riuscire, l'applicazione può attendere ed eseguire un altro tentativo. Se necessario, questo processo può essere ripetuto con ritardi crescenti tra i tentativi, fino a quando non viene tentato un numero massimo di richieste. Il ritardo può essere aumentato in modo incrementale o esponenziale, a seconda del tipo di errore e delle probabilità che venga risolto durante questo periodo.

Il diagramma seguente illustra come richiamare un'operazione in un servizio ospitato usando questo modello. Se la richiesta ha esito negativo dopo un numero predefinito di tentativi, l'applicazione deve considerare l'errore come un'eccezione e gestirlo di conseguenza.

Figura 1 - Richiamare un'operazione in un servizio ospitato usando il modello di ripetizione dei tentativi

L'applicazione deve eseguire il wrapping di tutti i tentativi di accesso a un servizio remoto nel codice che implementa un criterio di ripetizione corrispondente a una delle strategie elencate sopra. Le richieste inviate a servizi diversi possono essere soggette a criteri diversi. Alcuni fornitori offrono librerie che implementano criteri di ripetizione dei tentativi, con i quali l'applicazione può specificare il numero massimo di tentativi, l'intervallo tra i tentativi e altri parametri.

Un'applicazione deve registrare i dettagli degli errori e delle operazioni non riuscite. Queste informazioni sono utili per gli operatori. Detto questo, per evitare l'inondazione degli operatori con avvisi sulle operazioni in cui i tentativi successivi hanno avuto esito positivo, è consigliabile registrare errori iniziali come voci informative e solo l'errore dell'ultimo tentativo di ripetizione come errore effettivo. Di seguito è riportato un esempio dell'aspetto di questo modello di registrazione.

Se un servizio è spesso non disponibile o occupato, spesso è perché il servizio ha esaurito le risorse. È possibile ridurre la frequenza di questi errori ridimensionando il servizio. Ad esempio, se un servizio di database è continuamente sovraccarico, potrebbe essere utile partizionare il database e distribuire il carico tra più server.

Microsoft Entity Framework offre strumenti per la ripetizione dei tentativi di esecuzione delle operazioni su database. La maggior parte dei servizi di Azure e degli SDK client include inoltre un meccanismo di ripetizione dei tentativi. Per altre informazioni, vedere Retry guidance for specific services (Linee guida per la ripetizione dei tentativi per servizi specifici).

Considerazioni e problemi

Prima di decidere come implementare questo modello, è opportuno considerare quanto segue.

I criteri di ripetizione dei tentativi devono essere adattati ai requisiti aziendali dell'applicazione e alla natura dell'errore. Per alcune operazioni non critiche, è preferibile fallire e rispondere immediatamente agli errori piuttosto che ripetere vari tentativi con effetti sulla velocità effettiva dell'applicazione. Ad esempio, in un'applicazione Web interattiva che accede a un servizio remoto, è preferibile non riuscire dopo un numero minore di tentativi con un breve ritardo tra i tentativi e visualizzare un messaggio appropriato all'utente, ad esempio "riprova più tardi". Per un'applicazione che esegue operazioni in batch, potrebbe essere invece più appropriato aumentare il numero di tentativi con un ritardo esponenzialmente maggiore tra i vari tentativi.

Criteri di ripetizione aggressivi con un ritardo minimo tra tentativi, così come un numero elevato di tentativi, potrebbero peggiorare ulteriormente la situazione per un servizio occupato eseguito ai limiti della capacità. Questo tipo di criteri di ripetizione possono influire anche sulla velocità di risposta dell'applicazione, se tenta continuamente di eseguire un'operazione che non riesce.

Se una richiesta non riesce dopo un numero significativo di tentativi, è consigliabile che l'applicazione eviti ulteriori richieste per la stessa risorsa e che invece segnali immediatamente un errore. Al termine del periodo, l'applicazione può provare a consentire una o più richieste per verificare se riescono. Per altri dettagli su questa strategia, vedere Modello a interruttore.

Valutare se l'operazione è idempotente. In caso affermativo, la ripetizione dei tentativi è intrinsecamente sicura. In caso contrario, i tentativi potrebbero causare più esecuzioni dell'operazione, con effetti collaterali imprevisti. Ad esempio, un servizio potrebbe ricevere la richiesta ed elaborarla correttamente, ma non riuscire invece a inviare una risposta. A questo punto, la logica di ripetizione dei tentativi potrebbe inviare nuovamente la richiesta, partendo dal presupposto che la prima richiesta non sia stata ricevuta.

Una richiesta per un servizio può avere esito negativo per svariati motivi e generare eccezioni diverse a seconda della natura dell'errore. Alcune eccezioni indicano un errore che può essere risolto rapidamente, mentre altre indicano errori di più lunga durata. È utile che i criteri per la ripetizione dei tentativi adattino l'intervallo di tempo tra i tentativi in base al tipo dell'eccezione.

Valutare gli effetti della ripetizione di un'operazione che fa parte di una transazione sulla coerenza complessiva delle transazioni. Mettere a punto i criteri di ripetizione dei tentativi per le operazioni transazionali per ottenere la massima probabilità di successo e ridurre la necessità di annullare tutti i passaggi della transazione.

Assicurarsi di testare in modo completo tutto il codice per la ripetizione dei tentativi per un'ampia gamma di condizioni di errore. Verificare che non ci siano effetti gravi sulle prestazioni o l'affidabilità dell'applicazione, che non causi un carico eccessivo su servizi e risorse o che non si generino colli di bottiglia o race condition.

Implementare la logica di ripetizione dei tentativi solo nei casi in cui è noto il contesto completo di un'operazione con esito negativo. Ad esempio, se un'attività che contiene criteri di ripetizione richiama un'altra attività che contiene a sua volta criteri di ripetizione, questo livello aggiuntivo di tentativi può introdurre lunghi ritardi per l'elaborazione. Potrebbe essere più appropriato configurare l'attività di livello più basso in modo che fallisca e risponda immediatamente agli errori e segnali il motivo dell'errore all'attività che l'ha richiamata. Questa attività di livello superiore può quindi gestire l'errore in base ai propri criteri.

È importante registrare tutti gli errori di connettività che causano un nuovo tentativo, in modo da poter identificare i problemi sottostanti con l'applicazione, i servizi o le risorse.

Esaminare gli errori che è più probabile che si verifichino per un servizio o una risorsa per stabilire se è probabile che siano durevoli o terminali. In questo caso, è preferibile gestire l'errore come un'eccezione. L'applicazione può segnalare o registrare l'eccezione e quindi tentare di continuare richiamando un servizio alternativo (se disponibile) o offrendo funzionalità ridotte. Per altre informazioni su come rilevare e gestire gli errori di lunga durata, vedere il modello di interruttore.

Quando usare questo modello

Usare questo modello quando un'applicazione può subire errori temporanei quando interagisce con un servizio remoto o accede a una risorsa remota. Questi errori sono prevedibilmente di breve durata e la ripetizione di una richiesta non riuscita in precedenza potrebbe avere esito positivo durante un tentativo successivo.

Questo modello potrebbe non essere utile:

  • Quando è probabile che un errore sia di lunga durata, in quanto questo può influire sulla velocità di risposta di un'applicazione. L'applicazione potrebbe sprecare tempo e risorse nel tentativo di ripetere una richiesta che probabilmente non riuscirà.
  • Per la gestione degli errori che non sono dovuti a problematiche temporanee, ad esempio le eccezioni interne causate da errori nella logica di business di un'applicazione.
  • Come alternativa alla risoluzione dei problemi di scalabilità in un sistema. Se un'applicazione riscontra errori frequenti per condizioni di occupato, questo spesso è segno della necessità di potenziare il servizio o la risorsa a cui si accede.

Progettazione del carico di lavoro

Un architetto deve valutare il modo in cui il modello di ripetizione dei tentativi può essere usato nella progettazione del carico di lavoro per soddisfare gli obiettivi e i principi trattati nei pilastri di Azure Well-Architected Framework. Ad esempio:

Concetto fondamentale Come questo modello supporta gli obiettivi di pilastro
Le decisioni di progettazione dell'affidabilità consentono al carico di lavoro di diventare resilienti a malfunzionamenti e di assicurarsi che venga ripristinato in uno stato completamente funzionante dopo che si verifica un errore. La mitigazione degli errori temporanei in un sistema distribuito è una tecnica di base per migliorare la resilienza di un carico di lavoro.

- RE:07 Conservazione automatica
- RE:07 Errori temporanei

Come per qualsiasi decisione di progettazione, prendere in considerazione eventuali compromessi rispetto agli obiettivi degli altri pilastri che potrebbero essere introdotti con questo modello.

Esempio

Questo esempio in C# illustra un'implementazione del modello di ripetizione dei tentativi. Il metodo OperationWithBasicRetryAsync, illustrato di seguito, richiama un servizio esterno in modo asincrono tramite il metodo TransientOperationAsync. I dettagli del metodo TransientOperationAsync sono specifici del servizio e sono stati omessi dal codice di esempio.

private int retryCount = 3;
private readonly TimeSpan delay = TimeSpan.FromSeconds(5);

public async Task OperationWithBasicRetryAsync()
{
  int currentRetry = 0;

  for (;;)
  {
    try
    {
      // Call external service.
      await TransientOperationAsync();

      // Return or break.
      break;
    }
    catch (Exception ex)
    {
      Trace.TraceError("Operation Exception");

      currentRetry++;

      // Check if the exception thrown was a transient exception
      // based on the logic in the error detection strategy.
      // Determine whether to retry the operation, as well as how
      // long to wait, based on the retry strategy.
      if (currentRetry > this.retryCount || !IsTransient(ex))
      {
        // If this isn't a transient error or we shouldn't retry,
        // rethrow the exception.
        throw;
      }
    }

    // Wait to retry the operation.
    // Consider calculating an exponential delay here and
    // using a strategy best suited for the operation and fault.
    await Task.Delay(delay);
  }
}

// Async method that wraps a call to a remote service (details not shown).
private async Task TransientOperationAsync()
{
  ...
}

L'istruzione che richiama questo metodo è contenuta in un blocco try/catch di cui è stato eseguito il wrapping in un ciclo for. Il ciclo for restituisce il controllo se la chiamata del metodo TransientOperationAsync ha esito positivo senza generare un'eccezione. Se il metodo TransientOperationAsync ha esito negativo, il blocco catch esamina il motivo dell'errore. Se si ritiene che l'errore sia temporaneo, il codice attende per un breve intervallo di tempo prima di ritentare l'operazione.

Il ciclo for tiene anche traccia del numero di tentativi di esecuzione dell'operazione e se il codice ha esito negativo tre volte, l'eccezione viene considerata di più lunga durata. Se l'eccezione non è temporanea o è di lunga durata, il gestore catch genera un'eccezione. Questa eccezione causa l'uscita dal ciclo for e deve essere intercettata dal codice che richiama il metodo OperationWithBasicRetryAsync.

Il metodo IsTransient, illustrato di seguito, verifica la presenza di un set specifico di eccezioni rilevanti per l'ambiente in cui viene eseguito il codice. La definizione di eccezione temporanea varia in base alle risorse a cui si accede e all'ambiente in cui viene eseguita l'operazione.

private bool IsTransient(Exception ex)
{
  // Determine if the exception is transient.
  // In some cases this is as simple as checking the exception type, in other
  // cases it might be necessary to inspect other properties of the exception.
  if (ex is OperationTransientException)
    return true;

  var webException = ex as WebException;
  if (webException != null)
  {
    // If the web exception contains one of the following status values
    // it might be transient.
    return new[] {WebExceptionStatus.ConnectionClosed,
                  WebExceptionStatus.Timeout,
                  WebExceptionStatus.RequestCanceled }.
            Contains(webException.Status);
  }

  // Additional exception checking logic goes here.
  return false;
}

Passaggi successivi

  • Prima di scrivere logica di ripetizione dei tentativi personalizzata, provare a usare un framework generale, ad esempio Polly per .NET o Resilience4j per Java.

  • Quando si elaborano comandi che modificano i dati aziendali, tenere presente che i tentativi possono comportare l'esecuzione dell'azione due volte, che potrebbe essere problematico se tale azione è simile all'addebito della carta di credito di un cliente. L'uso del modello di Idempotenza descritto in questo post di blog può aiutare a gestire queste situazioni.

  • Il modello di app Web affidabile illustra come applicare il modello di ripetizione dei tentativi alle applicazioni Web convergenti nel cloud.

  • Per la maggior parte dei servizi di Azure, gli SDK client includono la logica di ripetizione dei tentativi predefinita. Per altre informazioni, vedere Linee guida per i tentativi per i servizi di Azure.

  • Modello di interruttore. Se si prevede che un errore sia di maggiore durata, potrebbe essere più appropriato implementare il modello di interruttore. La combinazione dei modelli retry e circuit breaker offre un approccio completo alla gestione degli errori.