Operatività EDA: delivery semantics, idempotenza e DLQ 🚨
Se l’EDA è la parte “cool”, questa è la parte “ti salva in produzione”. Spoiler: è più importante.
Delivery semantics: cosa stai promettendo davvero 📦
Le tre etichette classiche:
- At-most-once: potrebbe perdersi qualche evento, ma niente duplicati.
- At-least-once: non perdi eventi (idealmente), ma puoi avere duplicati.
- Exactly-once: l’obiettivo più ambizioso. Kafka supporta EOS (Exactly-Once Semantics) tramite producer idempotente e transazioni, ma questa garanzia vale solo tra topic Kafka (operazioni Kafka-to-Kafka). Non appena esci dal cluster — scrivi su un DB, chiami un’API, aggiorni un’altra store — la garanzia EOS non si estende. In scenari cross-system, quello che ottieni davvero è effectively-once: idempotenza applicativa + deduplicazione.
Regola pratica: in architetture distribuite, assumi duplicati e progetta di conseguenza. L’EOS del broker non ti solleva dall’obbligo di idempotenza sul consumer.
Idempotenza: rendere i consumer “a prova di replay” 🛡️
Un consumer è idempotente se processare lo stesso evento più volte porta allo stesso risultato.
Strategie comuni:
- dedup store: salva
event_idprocessati e scarta duplicati; - upsert su chiave business (es.
order_id) invece di insert puro; - side effects controllati: invio email con “exactly-once” applicativo via lock/dedup.
Ordering e partitioning: l’ordine non è gratis 🧵
Molte piattaforme garantiscono l’ordine solo:
- dentro una partizione;
- per eventi con la stessa key.
Quindi la domanda diventa: qual è la key corretta?
- per ordini: spesso
order_id; - per utenti: spesso
user_id.
Se scegli una key “casuale”, l’ordine diventa un concetto poetico.
Error handling: retry, backoff, DLQ 🧯
flowchart LR
B[(Broker)] --> C[Consumer]
C -->|OK| ACK[Ack / Commit]
C -->|Fail| R{Retry\n< max?}
R -->|Sì| B
R -->|No| DLQ[(DLQ)]
DLQ --> OPS[Runbook\ntriage + reprocess]
Un modello robusto include:
- retry con backoff (evita storm);
- limite tentativi;
- DLQ (Dead Letter Queue) per messaggi “poison”;
- alerting su DLQ e su consumer lag.
Classificazione degli errori e DLQ: non un cestino, un processo 🧠
Non tutti gli errori meritano lo stesso trattamento. Gli errori transitori (rete, timeout, dipendenze lente) richiedono retry con backoff; gli errori permanenti (schema invalido, dato corrotto) richiedono stop, invio in DLQ e triage. Deciderlo “durante l’incidente” è il modo migliore per peggiorare l’incidente.
Esempio pratico: se un consumer fallisce per timeout verso un servizio esterno, prova retry con backoff e circuit breaker; se fallisce per schema unrecognized, invia su DLQ e segnala per triage manuale.
La DLQ non è un limbo silenzioso: è utile solo se c’è un processo attivo che la presidia. Condizioni minime:
- sai distinguere errori transitori vs permanenti (e lo decidi a priori, non durante l’incidente);
- hai un runbook: chi guarda, con quale priorità, come si reprocessa;
- puoi correggere e riprocessare in modo sicuro (idempotenza — sempre, non «probabilmente»).
Runbook minimale per DLQ:
- alert immediato quando un messaggio finisce in DLQ;
- classificazione automatica (schema error, transient, business error) dove possibile;
- assegnazione a un owner e procedura di reprocess con idempotenza verificata;
- metriche DLQ nel dashboard principale, non in una tab nascosta.
Consumer lag e backpressure 📈
In EDA, spesso “non hai errori”… hai ritardi.
Il consumer lag misura quanti eventi il consumer non ha ancora processato rispetto all’ultimo offset pubblicato. Un lag crescente è il primo segnale che il consumer non riesce a stare al passo: dipendenza lenta, saturazione CPU, bug di performance, o semplicemente traffico aumentato. In Kafka il lag si misura per partizione e per consumer group; in altri broker il concetto è analogo.
La backpressure è la pressione che si accumula a monte quando un consumer rallenta. Se non gestita, si propaga verso il producer (che continua a scrivere) e può degradare l’intero sistema a cascata. Alcune piattaforme offrono meccanismi nativi di backpressure; altrove tocca gestirla esplicitamente.
Strategie pratiche per gestire picchi di lag:
- scalatura orizzontale del consumer group (più istanze = più parallelismo, nel limite del numero di partizioni);
- batch processing per ammortizzare il costo per-evento su operazioni costose;
- circuit breaker verso dipendenze lente, per evitare che un servizio esterno draini silenziosamente tutta la capacità;
- alerting su soglie di lag (es. “lag > X per più di Y minuti”) con escalation graduata.
Metriche tipiche da monitorare:
- consumer lag (per topic/partizione e per consumer group);
- throughput (input vs output, per evidenziare il delta);
- error rate per tipo (transient vs permanent);
- tempo medio e percentile p99 di processamento per evento.
Osservabilità end-to-end: correlation ID o caos 🧬
Senza correlazione, debuggare un flusso asincrono è come cercare un ago in un pagliaio… al buio… mentre il pagliaio prende fuoco.
Pratiche minime:
correlation_idpropagato su eventi e log;- tracing distribuito quando possibile;
- dashboard per flussi di business (non solo metriche tecniche).
Se vuoi una base “standard”, OpenTelemetry ti aiuta a correlare segnali, ma serve comunque disciplina applicativa.
Responsabilità di producer e consumer 🤝
Un producer serio pubblica eventi coerenti, con metadata obbligatori e semantica stabile. Un consumer serio assume duplicati, gestisce out-of-order quando necessario e fallisce in modo osservabile. Quando questa responsabilità non è esplicita, il sistema “funziona” finché non scala.
Cosa ci si aspetta da un producer responsabile:
- pubblica eventi con schema stabile e versioning esplicito;
- include sempre
event_id,timestampecorrelation_idnei metadata; - non rompe la compatibilità dello schema senza preavviso e senza versione;
- documenta la semantica dell’evento (cosa significa, quando viene emesso, cosa non significa).
Cosa ci si aspetta da un consumer responsabile:
- non assume unicità dei messaggi: implementa deduplicazione o idempotenza;
- non assume ordine globale: gestisce out-of-order almeno per gli scenari noti;
- logga ogni errore in modo strutturato e correlato;
- espone metriche di salute (lag, error rate, throughput) senza che qualcuno le debba cercare.
Quando questi contratti non sono scritti da nessuna parte — non nell’ADR, non nel runbook, non nella documentazione del topic — vengono «negoziati» durante il primo incidente in produzione. Un timing subottimale.
Checklist operativa (minima) ✅
Qui non c’è “best practice da slide”: ci sono i mattoni minimi per non trasformare i retry in una stampante di incidenti. Se devi scegliere cosa fare prima, fai queste cose prima.
- Retry policy documentata per ogni consumer.
- DLQ con ownership e runbook.
- Idempotenza verificata (test e revisione design).
- Dashboard: lag, errori, DLQ, throughput.
- Logging strutturato + correlation ID.
Prossimi passi 🚀
Quando l’operatività è a posto, puoi iniziare a progettare flussi multi-step e migrazioni senza vivere nel terrore del dual-write.
- Per Outbox/Saga e migrazioni: vedi la guida sui pattern da produzione.
- Per governance e catalogo: vedi la guida di governance.