Come siamo arrivati a zero errori 502 nei deploy
Ogni volta che facevamo il deploy del backend di Mythex, un certo numero di richieste falliva con 502 Bad Gateway. Non molte, ma un 502 durante un deploy significa che il clic di qualcuno non ha fatto nulla, oppure che una build che stava guardando si è bloccata per un attimo. Abbiamo ricondotto il problema a due questioni separate: un watchdog di auto-riparazione che lottava contro i nostri deploy, e un intervallo tra il momento in cui un server smetteva di ascoltare e quello in cui il load balancer smetteva di inviargli traffico. Su tre deploy di produzione consecutivi dopo le correzioni, il conteggio è sceso da 10 a 6 e poi a 0.
La configurazione
La nostra API e l'orchestratore girano come container su un piccolo gruppo di server dietro un Application Load Balancer (ALB) di AWS. Un deploy aggiorna un server alla volta: svuota i vecchi container, avvia quelli nuovi e passa al successivo. Un watchdog su ogni server verifica che i container siano attivi e riavvia quelli che non lo sono: esiste perché un processo andato in crash alle 3 di notte torni su senza che nessuno debba svegliarsi.
Presi singolarmente, entrambi sono ragionevoli. Insieme, non lo erano.
Problema 1: il watchdog faceva a gara con il deploy
Il watchdog girava una volta al minuto e «riparava» al primo controllo di integrità fallito. Un deploy svuota volutamente i servizi per fino a 150 secondi, durante i quali un controllo di integrità deve fallire per forza. Quindi il watchdog scattava durante ogni singolo deploy.
Nella maggior parte dei casi perdeva la gara senza conseguenze. Una volta l'ha vinta: ha riscritto il file compose (la descrizione di quali container devono girare) nel mezzo di un deploy, e ha lasciato un server senza alcun container gateway, mentre il job di deploy risultava completato con successo.
Le correzioni:
- Un solo lock. Ora il deploy prende lo stesso lock su file dello script di bootstrap del server, così watchdog e deploy non possono mai girare insieme.
- Quattro segnali, non uno. Il watchdog ha bisogno di quattro controlli consecutivi falliti prima di agire. Un servizio in svuotamento non sembra più morto.
- Una sola definizione. Il file compose ha ora un'unica fonte, pubblicata sui server, invece di copie che potevano divergere.
- Verificare, non fidarsi. Il job di deploy non risulta riuscito solo perché un comando ha restituito un esito. Controlla le API, l'orchestratore, il gateway e il worker di pubblicazione di ogni server prima di poter completare.
Problema 2: il server se ne andava prima del traffico
Il secondo problema era più silenzioso. Quando un deploy svuota un server, il server inizia a rispondere al suo endpoint di integrità con 503, così il load balancer lo toglie dalla rotazione. Il nostro lo faceva per 5 secondi, poi si spegneva.
Ma l'ALB controllava l'integrità solo a intervalli, e aveva bisogno di diversi fallimenti consecutivi prima di smettere di instradare verso un target: fino a circa 60 secondi in tutto. Quindi per circa 55 secondi il load balancer continuava a inviare richieste reali a un server che aveva già smesso di ascoltare. Quelli erano i nostri 502.
La soluzione è stata far concordare le due tempistiche:
- Il target group ora controlla ogni 5 secondi e segna un target come non integro dopo 2 fallimenti: circa 10 secondi per accorgersene.
- Lo svuotamento continua a servire traffico reale per 15 secondi dopo che inizia a fallire i controlli di integrità, così c'è sempre sovrapposizione.
- Queste impostazioni vivono nello script che crea l'infrastruttura, non in una console che qualcuno ha cliccato una volta.
Far reggere i numeri tra loro
Tempi di attesa come questi dipendono l'uno dall'altro. Lo svuotamento deve durare più del tempo che il bilanciatore impiega ad accorgersi; svuotamento e spegnimento devono finire prima che il container venga forzatamente terminato. È facile che, dopo sei mesi, qualcuno cambi un numero e reintroduca il bug senza accorgersene.
Quindi abbiamo scritto quelle relazioni come test: lo svuotamento deve essere più lungo dell'intervallo del controllo di integrità moltiplicato per la soglia di fallimento; il tempo per tornare integro deve restare breve; l'intero svuotamento deve stare nel periodo di grazia di arresto del container. Se cambi un numero senza gli altri, la CI fallisce. Abbiamo anche sottoposto a mutation test gli stessi test: abbiamo cambiato ogni numero con un valore sbagliato e verificato che un test lo intercettasse.
Misurare un deploy è facile da sbagliare
La nostra prima misura diceva che un deploy pulito era rotto per circa il 3%. Non era vero. Contavamo ogni risposta diversa da 200, e un 503 dall'endpoint di integrità durante lo svuotamento non è un fallimento: è lo svuotamento che fa il suo lavoro, dicendo al bilanciatore di non scegliere quel server mentre continua a servire richieste reali.
Ciò che l'utente percepisce è un 502: la richiesta non ha raggiunto nulla. Contare solo quelli dava il quadro reale: 10, poi 6, poi 0 su tre deploy di produzione.
Cosa ci siamo portati a casa
- L'auto-riparazione deve sapere della manutenzione pianificata. Un watchdog che non distingue un deploy da un crash finirà per lottare contro il deploy.
- Lo svuotamento è una stretta di mano. Il server deve continuare a servire finché il load balancer ha dimostrato di aver smesso di inviargli traffico, e quella tempistica è impostata sul bilanciatore, non sul server.
- Codifica gli invarianti come test. I numeri operativi si disallineano. I test che li mettono in relazione tra loro no.
- Decidi cosa misurare prima di misurare. «Errori» era la metrica sbagliata; «richieste che non hanno raggiunto nulla» era quella giusta.
Se pubblichi un'app su Mythex, nulla di tutto questo è visibile: ed è proprio il punto. Correlato: come la tua app pubblicata resta online mentre la ripubblichi.