Elaborazione "exactly-once" e riconoscimento "at-most-once"

La creazione di code di messaggi e microservizi affidabili in database cloud distribuiti come Spanner comporta sfide uniche. Questo documento descrive i concetti di base e i pattern di progettazione utilizzati per eseguire l'elaborazione esattamente una volta e il riconoscimento al massimo una volta nelle code Spanner.

Concetti principali e il dilemma dell'idempotenza

"Idempotenza" significa che, indipendentemente dal numero di volte in cui viene eseguita un'operazione, il risultato è sempre lo stesso. Quando crei code di messaggi o sistemi di distribuzione del lavoro, esistono tre metodi di distribuzione principali che gestiscono l'idempotenza:

  • At-least-once: i messaggi vengono consegnati ed elaborati. Se si verificano errori di rete, un messaggio potrebbe essere recapitato ed elaborato più volte.
  • At-most-once::i messaggi vengono consegnati al massimo una volta. L'elaborazione duplicata viene impedita, ma se si verifica un errore, il messaggio potrebbe essere perso o eliminato.
  • Exactly-once:ogni messaggio viene elaborato esattamente una volta, senza essere perso o duplicato.

Le code Spanner forniscono automaticamente la consegna "at-least-once" e l'acknowledgement "at-most-once". Tuttavia, puoi utilizzare i pattern di progettazione e gli esempi descritti nelle sezioni seguenti per ridurre a livello di programmazione l'elaborazione e la nuova pubblicazione dei duplicati.

Il problema dello stato di commit sconosciuto

In un database a una sola macchina, le transazioni hanno esito positivo o negativo. In un database cloud distribuito, le transazioni possono andare perse man mano che i dati vengono replicati in più data center fisici.

Quando l'applicazione elabora un messaggio e invia un riconoscimento come DELETE FROM MessageQueue WHERE message_id = '123' ASSERT_ROWS_MODIFIED 1, la richiesta attraversa più hop di rete:

[Your App] <--(gRPC)--> [Spanner API] <--(Google network)--> [Spanner Storage]

Quando il leader dello spazio di archiviazione Spanner riceve il commit, lo sincronizza tra le repliche di archiviazione. Una volta raggiunto il consenso e scritti i dati sul disco, la transazione viene eseguita correttamente nel database.

Tuttavia, un errore di rete temporaneo (ad esempio un riavvio del proxy o una disconnessione di rete) potrebbe verificarsi dopo il commit dei dati sul disco, ma prima che la conferma di esito positivo raggiunga l'applicazione. In questo caso, la tua applicazione riceve un errore di timeout di rete (DEADLINE_EXCEEDED o UNAVAILABLE).

Poiché la connessione è stata interrotta fuori banda, la tua applicazione non ha modo di sapere se la connessione è stata interrotta prima o dopo la riuscita del commit. Questo è noto come il problema dello "stato del commit sconosciuto".

Perché i tentativi automatici possono essere pericolosi

Se la tua applicazione intercetta un errore di timeout di rete e riprova la stessa transazione senza deduplicazione, rischia di eseguire la logica di business due volte. Ad esempio, se la transazione prevedeva l'addebito sulla carta di credito di un cliente o la spedizione di un articolo, il nuovo tentativo di una transazione riuscita (ma non confermata) comporta un addebito o una spedizione duplicati.

Per proteggere i tuoi utenti, le librerie client ufficiali Google Cloud non tentano mai automaticamente di ripetere le transazioni che non vanno a buon fine a causa di errori di commit sconosciuti. Generano un errore esplicito che ti avvisa che il risultato della transazione è sconosciuto, lasciando al codice dell'applicazione la gestione del nuovo tentativo in modo sicuro utilizzando pattern di idempotenza.

Strategie reali per l'elaborazione "exactly-once"

Per riprovare in modo sicuro le transazioni e ridurre l'elaborazione e la riconsegna dei messaggi duplicati, implementa uno dei seguenti pattern di progettazione principali.

Assert rows modified

Se tutta la logica di business (ad esempio l'aggiornamento di altre tabelle) e l'acknowledgement del messaggio della coda avvengono in una singola transazione Spanner, puoi utilizzare ASSERT_ROWS_MODIFIED nell'istruzione DELETE. Questa è la strategia più semplice per l'elaborazione exactly-once perché non richiede una tabella di deduplicazione separata.

Se aggiungi ASSERT_ROWS_MODIFIED 1 all'istruzione, la conferma ha esito positivo solo se è stato eliminato esattamente un messaggio. Se si verifica un errore di rete e l'applicazione riprova la transazione, il messaggio non esiste più nella coda. Il nuovo tentativo elimina 0 righe e l'istruzione non va a buon fine con OUT_OF_RANGE.

L'errore OUT_OF_RANGE è permanente: la riga è già stata eliminata, quindi la riesecuzione dell'istruzione modificherà sempre 0 righe e l'operazione non andrà a buon fine. Inoltre, è a livello di istruzione: solo DELETE non va a buon fine. La transazione rimane aperta e tutte le scritture che hai già eseguito al suo interno vengono ancora memorizzate nel buffer e verranno commit se procedi. Spanner non interrompe o esegue il rollback della transazione per te. Per ottenere la protezione exactly-once prevista, devi assicurarti che la transazione venga eseguita il rollback:

  • Consenti la propagazione dell'errore: se utilizzi un'API basata su runner (come ReadWriteTransaction in Go), consenti la propagazione dell'errore OUT_OF_RANGE al di fuori della funzione di transazione. La libreria client abbandonerà la transazione e la eseguirà il rollback, garantendo che non vengano eseguite scritture di logica di business.
  • Do not catch and continue: non intercettare mai l'errore di asserzione all'interno della funzione di transazione e continua. In questo caso, Spanner eseguirà comunque il commit delle altre istruzioni, causando l'aggiornamento del duplicato esatto che questo pattern dovrebbe impedire.
  • Rollback manuali:se gestisci le transazioni manualmente (ad esempio con ReadWriteStmtBasedTransaction in Go o con le API REST/gRPC), devi chiamare esplicitamente il metodo di rollback corrispondente quando l'asserzione non va a buon fine.

Posta in uscita transazionale

Se la tua logica di business non può verificarsi all'interno di una singola transazione Spanner (ad esempio flussi di lavoro in più passaggi o aggiornamenti che interessano più sistemi) o se l'elaborazione dei messaggi richiede la chiamata di servizi esterni (ad esempio l'invio di un SMS o l'elaborazione di un pagamento), non puoi accoppiare in modo atomico la logica di business con l'acknowledgement della coda.

In questi scenari, non chiamare mai API esterne o eseguire operazioni non transazionali direttamente all'interno di un blocco di transazioni Spanner. Se Spanner ritenta o interrompe la transazione, la tua applicazione potrebbe eseguire la logica di business più volte.

La tua applicazione deve implementare una propria strategia di idempotenza, ma puoi utilizzare i seguenti pattern per ridurre il lavoro duplicato:

  • Operazioni rapide (completate entro il lease predefinito di 10 secondi): esegui la logica di business o la chiamata API esterna quando viene ricevuto il messaggio. Conferma il messaggio solo dopo che l'operazione è andata a buon fine. Se l'operazione non va a buon fine o se il worker si arresta in modo anomalo prima di confermare la ricezione, non confermare la ricezione del messaggio; il lease scade e Spanner recapita automaticamente il messaggio per il nuovo tentativo.

  • Operazioni a lunga esecuzione (richiedono più di 10 secondi): conferma la ricezione del messaggio e, all'interno della stessa transazione, invia nuovamente un nuovo messaggio pianificato per la consegna futura (utilizzando un ritardo di consegna superiore al tempo di esecuzione stimato). In alternativa, estendi periodicamente il lease del messaggio utilizzando il TVF RENEWLEASE_QUEUE_NAME(). Quando la logica di business o la chiamata esterna va a buon fine, conferma il messaggio appena accodato o esteso.

Idempotenza della sessione multiplexata

Le sessioni Spanner possono essere multiplexate. Le sessioni multiplex monitorano gli stati delle transazioni in memoria nelle sessioni condivise. In questo modo, si protegge dagli errori di rete lato client consentendo alle librerie client di riconnettersi e risolvere in modo più affidabile gli stati di commit sconosciuti.

Tuttavia, le sessioni multiplexate non possono eliminare i risultati di commit sconosciuti se un server frontend si arresta in modo anomalo. Se il server specifico che ospita la tabella delle transazioni della sessione viene riavviato immediatamente dopo un commit, lo stato in memoria viene perso e viene restituito un errore di risultato della transazione sconosciuto al momento della riconnessione. Per affrontare questo rischio, implementa i pattern di idempotenza in questa pagina.

Passaggi successivi