Deploy riuscito, ma il sito è irraggiungibile
La dashboard indica che è in esecuzione, ma gli utenti visualizzano un errore. Scopri perché il successo del deploy e la salute dell'applicazione sono segnali diversi e come fare in modo che un deploy verde significhi davvero che l'app risponde.
Esiste un tipo specifico di brutta mattinata che inizia con un segno di spunta verde. Il deploy è riuscito ma il sito è irraggiungibile, la dashboard indica running e qualcuno ti sta mandando lo screenshot di un 502.
Non è un caso limite raro. È il risultato prevedibile di una piattaforma che segnala una cosa e ne misura un'altra, e vale la pena capirlo con precisione, perché la soluzione non consiste nel "controllare meglio": consiste nel cambiare ciò che la parola running può significare.
Tre domande diverse, una sola spia di stato
Quando una piattaforma indica che un servizio è in esecuzione, potrebbe rispondere a una di queste domande:
- Il container è stato avviato? Il processo esiste e non è terminato.
- La porta è aperta? Qualcosa è in ascolto dove la piattaforma si aspetta che lo sia.
- L'applicazione risponde correttamente? Una richiesta riceve una risposta che indica che l'app è pronta a lavorare.
Sono garanzie molto diverse tra loro e la maggior parte degli incidenti di questo tipo nasce dal fatto che la dashboard risponde alla domanda 1, mentre tu davi per scontata la domanda 3.
Un processo Node che si avvia, non riesce a connettersi al database e rimane in un ciclo di retry soddisfa per sempre la domanda 1. Non è andato in crash. Non servirà mai una richiesta. Il container è "running" sotto ogni aspetto che interessi all'orchestrator.
Il varco in cui si annida l'outage
La finestra pericolosa si trova tra "la nuova versione è stata avviata" e "la nuova versione è in grado di funzionare". In questa finestra una piattaforma ingenua ha già spostato il traffico, perché l'unica cosa che misurava era l'avvio.
Questo è peggio di un semplice crash per via del rollback. Un crash loop è rumoroso: il container termina, si riavvia, termina di nuovo e alla fine la piattaforma se ne accorge. Un processo che si avvia e poi si blocca è silenzioso. Nulla viene riavviato, non scatta alcun alert e la versione precedente, ormai, è quasi sempre già stata rimossa.
È quest'ultimo aspetto a causare il danno reale. La versione precedente funzionava. È stata rimossa perché una nuova versione era stata avviata, e l'avvio è stato scambiato per funzionamento.
Cosa fa un vero health gate
La soluzione è strutturale, non procedurale. Il traffico non dovrebbe essere spostato finché la nuova versione non ha risposto a una richiesta.
Su Dockup una release funziona così: la nuova versione viene creata in isolamento, avviata accanto alla versione che sta servendo il traffico e poi le viene posta una domanda. Solo quando risponde il dominio viene indirizzato verso di essa. Se non risponde mai, la release si interrompe e la versione precedente continua a servire il traffico: nessuno al di fuori della tua dashboard saprà mai che è stato tentato un deploy.
Ecco perché un deploy fallito su Dockup non è un outage. Il container precedente non è mai stato rimosso dando per scontato che quello nuovo avrebbe funzionato.
# The health gate is per-service configuration, not a platform default you inherit
dockup info my-project/my-api --json
Il blocco healthCheck in quell'output è l'intero contratto: quale percorso viene richiesto, quanto a lungo attendere una risposta, quante volte riprovare e quanto tempo aspettare tra un tentativo e l'altro.
Configura il check perché risponda alla domanda 3
Un health endpoint che restituisce sempre 200 è peggio di non averne alcuno, perché trasforma un vero gate in un semplice timbro. Lo scopo del check è fallire quando l'applicazione non è in grado di svolgere il proprio lavoro.
Un endpoint di readiness utile verifica ciò di cui l'app non può fare a meno:
// Not this — it proves only that the process is alive
app.get('/healthz', (req, res) => res.send('ok'))
// This — it proves the app can actually serve a request
app.get('/healthz', async (req, res) => {
try {
await db.query('select 1') // the dependency that is usually the problem
if (!cacheReady) throw new Error('cache warming')
res.status(200).json({ ok: true })
} catch (err) {
res.status(503).json({ ok: false, reason: err.message })
}
})
Due regole fanno funzionare tutto nella pratica:
Controlla le dipendenze senza le quali non puoi servire richieste, e nient'altro. Se la tua app è in grado di degradare correttamente quando l'indice di ricerca non è disponibile, non considerare l'indice di ricerca come condizione per la readiness: bloccheresti i deploy per qualcosa che non costituisce un outage.
Mantienilo leggero. L'endpoint viene chiamato ripetutamente durante ogni release. Un readiness check che esegue una query costosa crea un problema di carico che ti sei procurato da solo.
Dagli tempo a sufficienza, ma non illimitato
Due impostazioni determinano se il gate è d'aiuto o crea problemi:
- Il timeout per tentativo dovrebbe essere superiore al tuo cold start legittimo più lento. Un'app che si connette a un database e riscalda una cache in otto secondi fallirà ogni volta un check di tre secondi e finirai per "risolvere" il problema disabilitando il gate: tornando esattamente al punto di partenza.
- I retry dovrebbero coprire il tempo totale di avvio, non un singolo tentativo. Intervallo × retry è il budget reale.
Su Dockup queste impostazioni sono healthCheckInterval, healthCheckTimeout e healthCheckRetries, e sono specifiche per ogni servizio, perché un monolite Rails e un sidecar Go non si avviano secondo le stesse tempistiche.
Quando il sito è già irraggiungibile
Se stai leggendo questo articolo durante un incidente, l'ordine che lo risolve più rapidamente è il seguente:
- Verifica se l'app risponde direttamente, bypassando il dominio. Se risponde sulla sua porta ma non attraverso il dominio, si tratta di un problema di routing, non di un problema dell'applicazione: smetti di fare debug del codice.
- Leggi i runtime log, non i build log. Il build è riuscito: è questo il presupposto. Quello che vuoi sapere è cosa ha fatto il processo dopo l'avvio.
- Esegui il rollback prima di fare la diagnosi. La diagnosi costa meno quando nessuno sta guardando.
dockup logs my-project/my-api --follow # what the running process is saying
dockup deployments my-project/my-api # what was live before this
dockup rollback <deployment-id> my-project/my-api # put that back
Su Dockup un rollback è uno switch e non una nuova build, perché la versione precedente è ancora sul disco. Alle 3 di notte fa la differenza: il ripristino più rapido è quello che non deve compilare nulla.
La domanda da fare a una piattaforma
Quando scegli dove eseguire la produzione, questo è un test utile da fare intenzionalmente: esegui il deploy di un'applicazione che si avvia correttamente e poi non riesce a raggiungere il database. Osserva cosa indica la dashboard.
Se indica running, ora sai esattamente quanto varrà quella parola durante il prossimo incidente.
Domande frequenti
Perché la dashboard indica running quando il sito è irraggiungibile? Perché "running" di solito significa che il processo del container esiste, non che l'applicazione sia in grado di servire una richiesta. Un processo bloccato mentre ritenta la connessione al database soddisfa indefinitamente questa definizione.
Un health check dovrebbe interrogare il database? Sì, se l'applicazione non può servire richieste senza di esso. Controlla le dipendenze di cui hai realmente bisogno e ignora quelle senza le quali l'app può degradare correttamente.
Qual è la differenza tra liveness e readiness? La liveness verifica se il processo debba essere riavviato. La readiness verifica se debba ricevere traffico. Il gate che impedisce questo tipo di errore è la readiness e deve essere eseguito prima dello spostamento del traffico.
Come posso impedire del tutto a un deploy problematico di rendere irraggiungibile il sito? Sposta il traffico solo dopo che la nuova versione ha risposto a una richiesta reale e mantieni la versione precedente finché lo switch non è confermato. In questo modo una release fallita è una release che non è mai avvenuta, non un outage.
