Sintomo ≠ Causa: come l'auto-healer è diventato il vero problema
Un PostgreSQL primary al 91 % di CPU. L'auto-healer killa la query più rumorosa. Un'ora dopo: di nuovo 91 %. La lezione: i quickfix possono incastrarsi in loop infinito se nessuno chiede quale pattern si stia ripetendo.
Un PostgreSQL primary al 91 % di CPU. L'auto-healer gira ogni 5 minuti, trova la query più rumorosa, invia pg_cancel_backend(), e il load scende al 35 %. Un'ora dopo: di nuovo al 91 %. L'healer killa la query successiva. Di nuovo al 35 %. Un'altra ora. Di nuovo al 91 %.
Si potrebbe dichiarare che «il sistema funziona». Non funziona. Sta bruciando cicli di CPU per combattere un sintomo che non capisce mai.
Il momento dell'onestà
Dopo tre cicli ci siamo fermati. Invece di tunare l'healer in modo più aggressivo, abbiamo posto la domanda che avremmo dovuto porre fin dall'inizio: Cos'è che fa tornare il load?
Risposta, dopo 20 minuti di audit: 14 endpoint del backend chiamano la stessa aggregazione su 1,4 milioni di righe — COUNT(*) FILTER (...) — senza cache lock. A ogni cache miss, otto worker uvicorn paralleli sparano simultaneamente la stessa query costosa. Thundering herd.
L'healer aveva ragione: esistevano query che giravano e non avrebbero dovuto. L'healer aveva torto: ucciderle non risolveva il problema. Il problema era che 14 client stavano afferrando la stessa mucca per le mammelle contemporaneamente.
Cosa serviva davvero
- Esattamente un lock per cache key. Abbiamo usato Redis
SET NX EXcome distributed lock più unasyncio.Lockper worker come short-circuit locale. - Trovati 14 endpoint, fixati subito i tre critici, gli altri undici documentati in un ticket con tempistiche.
- L'healer non l'abbiamo tunato. L'healer non era mai stato il problema.
Dopo il fix: CPU load stabile al 53 %. Niente più escalation. Zero cicli di healer necessari.
La meta-lezione
Ogni sistema ha un punto in cui il sintomo appare più interessante della causa. Il sintomo è rumoroso, visibile e «risolvibile» con un piccolo script. La causa siede tre livelli di astrazione più in basso, non ha un nome, e richiede che qualcuno comprenda davvero il sottosistema.
La regola che ci poniamo ora a ogni incidente ricorrente:
«Se applichiamo questo fix adesso — cosa ci impedisce di tornare qui tra un'ora?»
Se la risposta non è chiara, il fix pianificato non è un fix. È un soppressore di sintomi, e il sistema troverà un altro modo, più rumoroso, di segnalare la stessa causa.
Applicabile ben oltre i database
Ogni senior engineer conosce questo pattern:
- Test flaky? Un blind-retry nella CI è soppressione di sintomi. Il test rivela una race condition reale.
- Memory leak? Pod restart ogni 6 h è soppressione di sintomi. Nel frattempo il leak si mangia dati cliente.
- Bounce rate su un button? A/B test con un colore nuovo è soppressione di sintomi. La feature non risolve il problema utente.
Per ogni problema ricorrente: identifica i cinque consumatori del sottosistema interessato e controlla il pattern lì — non solo nel punto dove l'alert era rumoroso.
Stesso fuoco anche da te?
Probabilmente abbiamo già visto qualcosa di simile. Parliamone.
Iniziamo a parlarne→