Blog

Avvisi che scattano una volta sola, non una volta per server

Uno dei nostri avvisi operativi, quello di "crediti in esaurimento" per l'app pubblicata di un builder, dovrebbe arrivare al massimo una volta al giorno. Un pomeriggio è arrivato cinque volte. La causa: ogni copia della nostra API ricordava in memoria quali avvisi aveva già inviato, quindi due server lo inviavano due volte, e ogni deploy cancellava quella memoria e lo faceva ripartire. Lo stesso schema faceva eseguire due volte il lavoro di un job in background. Ora entrambi si coordinano tramite un archivio condiviso, e in caso di indisponibilità di quell'archivio sbagliano dalla parte sicura.

Il sintomo

Mythex invia avvisi al team quando qualcosa richiede attenzione: un servizio è giù, un job si è bloccato, il saldo crediti di un builder si sta esaurendo mentre la sua app è pubblicata. Ogni avviso ha una finestra di deduplica, cioè un periodo durante il quale lo stesso avviso viene inviato una sola volta. Per l'avviso sui crediti in esaurimento, quella finestra è di 24 ore.

Cinque copie dello stesso avviso in un pomeriggio volevano dire che la finestra non teneva. Peggio ancora, arrivavano a coppie, a circa 11 minuti di distanza.

Perché è successo

Il nostro pacchetto di avvisi teneva la deduplica in memoria: una tabella "chiave dell'avviso → quando è stato inviato l'ultima volta", dentro il processo in esecuzione. Funziona quando c'è un solo processo che non si riavvia mai. In produzione non succede nessuna delle due cose:

  • Due repliche dell'API. In produzione girano due copie dell'API. Ognuna aveva la sua tabella, quindi ognuna inviava l'avviso una volta. Ecco le coppie.
  • I deploy. Ogni deploy sostituisce entrambe le repliche con processi nuovi e tabelle vuote. Dopo un deploy, l'avviso partiva di nuovo, come se non fosse mai stato inviato.

Il gap di circa 11 minuti tra i due avvisi di ogni coppia dipendeva dal job che genera questo avviso. Gira su entrambe le repliche e prende un lock, quindi le repliche se lo contendono a turno, e ognuna generava l'avviso quando arrivava il suo turno.

C'era anche un secondo bug, più silenzioso, nello stesso codice. Per evitare che la tabella crescesse all'infinito, si ripuliva da sola una volta arrivata a 500 chiavi, cancellando ogni voce più vecchia di 15 minuti, a prescindere dalla sua finestra. Sotto carico, una deduplica di 24 ore poteva durare un quarto d'ora.

La correzione

Un solo record condiviso per avviso

Prima di inviare un avviso, l'API ora rivendica la chiave di deduplica dell'avviso in Redis, un archivio condiviso in memoria con cui parlano entrambe le repliche e che non si riavvia quando si riavvia l'API. La rivendicazione è una singola operazione del tipo "imposta questa chiave solo se non esiste, e falla scadere al termine della finestra". La replica che la rivendica per prima invia l'avviso; l'altra trova la chiave già presa e resta in silenzio. Un processo nuovo dopo un deploy trova ancora la chiave lì.

La chiave e la finestra derivano dalla stessa regola usata dal pacchetto di avvisi, ora condivisa tra i due livelli, così non possono essere in disaccordo su cosa conta come "lo stesso avviso".

Fallire in modalità aperta, non chiusa

La decisione più importante è stata cosa fare quando Redis non è raggiungibile. Abbiamo scelto di inviare comunque l'avviso. Se Redis è giù, o non risponde entro 1,5 secondi, l'avviso parte. Un avviso duplicato è rumore. Un avviso perso è qualcuno che non viene avvisato.

Gli avvisi più gravi, P0, non vengono mai trattenuti dalla deduplica.

Ogni voce mantiene la propria finestra

Nella deduplica in memoria del pacchetto di avvisi, ogni voce ora memorizza quando termina la sua finestra, e la pulizia rimuove solo le voci la cui finestra è scaduta. Una chiave da 24 ore resta per 24 ore, qualunque sia il numero di altri avvisi arrivati.

Lo stesso bug, altrove

Lo stesso schema aveva raddoppiato anche un pezzo di lavoro in background: un job di riconciliazione che tiene allineato il nostro registro dello stato di ogni workspace con quello del provider delle sandbox. Ogni passaggio elenca ogni sandbox dell'account (migliaia in produzione, circa cinque secondi) più ogni progetto in esecuzione o in pausa. Entrambe le repliche lo eseguivano ogni due minuti.

Le sue scritture sono idempotenti, cioè eseguirle due volte dà lo stesso risultato che eseguirle una volta, quindi nulla si rompeva. Era solo il doppio del lavoro. Ora ogni finestra di due minuti viene rivendicata in Redis nello stesso modo in cui i nostri contatori d'uso rivendicavano già le loro, e solo la replica che la rivendica esegue il passaggio. Se Redis non è raggiungibile, il passaggio viene eseguito comunque: fare il lavoro due volte è innocuo, non farlo no.

Come l'abbiamo testato

I test riproducono l'impostazione reale. Caricano il modulo degli avvisi due volte, come due "repliche" separate, le puntano entrambe allo stesso archivio condiviso, fanno scattare lo stesso avviso da ciascuna, poi caricano una terza copia nuova per simulare "dopo un deploy" e lo fanno scattare di nuovo. Viene inviato esattamente un avviso. Altri test verificano che l'avviso di un altro builder parta ancora, che un P0 venga inviato da entrambe le repliche e che l'avviso venga inviato anche quando l'archivio è giù o supera il timeout. La correzione della pulizia ha un test tutto suo: una chiave da 24 ore sopravvive a 600 altri avvisi arrivati 20 minuti dopo.

Per il job di riconciliazione, due repliche che eseguono il tick insieme, più un terzo tick nella stessa finestra, producono una sola lista.

Cosa ne abbiamo ricavato

  • Lo stato in memoria appartiene al singolo processo. Tutto ciò che deve essere vero tra server, "inviato una volta" o "eseguito una volta", deve vivere in un posto che tutti condividono.
  • I deploy sono riavvii. Lo stato che sopravvive solo fino al riavvio successivo non sopravvive a una normale giornata di lavoro.
  • Decidi in quale direzione fallire. Per gli avvisi, un archivio condiviso giù dovrebbe significare più avvisi, non meno.
  • Cerca lo schema, non solo l'istanza. L'avviso era il caso rumoroso; il job di riconciliazione raddoppiato era lo stesso bug, che costava lavoro in silenzio.

Nulla di tutto questo è visibile dall'interno di Mythex. Per saperne di più su come manteniamo stabile la piattaforma, vedi come siamo arrivati a zero errori 502 nei deploy e il file di swap mancante. Se non hai familiarità con l'idea di far girare più copie di un servizio, la nostra guida su cos'è un server copre le basi.

Mythex · Pricing · Crea qualsiasi cosa con l'AI · Template per siti web e app · Mythex a confronto · Glossario per creare app con l'AI · Strumenti gratuiti per creare app con l'AI · English

English አማርኛ العربية Български বাংলা Bosanski Català Čeština Dansk Deutsch Ελληνικά Español Español (España) Eesti فارسی Suomi Français Français (Canada) ગુજરાતી हिन्दी Hrvatski Magyar Հայերեն Bahasa Indonesia Íslenska 日本語 ქართული Қазақ тілі ಕನ್ನಡ 한국어 Lietuvių Latviešu Македонски മലയാളം Монгол मराठी Bahasa Melayu မြန်မာ Norsk bokmål Nederlands ਪੰਜਾਬੀ Polski Português (Brasil) Português (Portugal) Română Русский Slovenčina Slovenščina Soomaali Shqip Srpski Svenska Kiswahili தமிழ் తెలుగు ไทย Tagalog Türkçe Українська اردو Tiếng Việt 简体中文 繁體中文(香港) 繁體中文(台灣)