Parliamone
// tecnologie.change-data-capture

Change Data Capture (CDC)

Propagazione affidabile delle mutazioni nei database relazionali: architetture log-based, pattern transazionali e problemi aperti nella sincronizzazione di sistemi distribuiti.

Data Engineering

Executive summary

Quando un'azienda gestisce decine di sistemi che devono restare allineati (archivi analitici, motori di ricerca, memorie temporanee, servizi indipendenti) il problema di propagare ogni modifica ai dati in modo tempestivo e senza perdite diventa un collo di bottiglia critico. Questo articolo analizza le tecniche che permettono di intercettare automaticamente le variazioni nei database e trasmetterle ad altri sistemi in tempo quasi reale, esaminando come i registri interni dei database vengano sfruttati per ottenere una propagazione fedele e a basso impatto, quali architetture garantiscano che nessuna modifica vada persa anche in caso di guasto, e come si gestisca l'evoluzione inevitabile della struttura dei dati senza interrompere i sistemi a valle. L'analisi mostra che le soluzioni attuali hanno raggiunto una maturità significativa per scenari consolidati, ma le sfide principali si concentrano sulla cattura dello stato iniziale senza bloccare i sistemi, sulla coerenza delle garanzie di consegna attraverso l'intera catena di elaborazione e sulla propagazione controllata delle modifiche alla struttura dei dati.


Background

L'esigenza di propagare le mutazioni di un database verso sistemi eterogenei non è recente: la replica dei dati è un problema fondamentale nell'ingegneria dei sistemi distribuiti fin dagli anni Ottanta. Ciò che è cambiato radicalmente nell'ultimo decennio è il contesto architetturale. La transizione verso architetture a microservizi ha frammentato il database monolitico in decine di store specializzati (relazionali, documentali, colonnari, vettoriali) ciascuno ottimizzato per un pattern di accesso specifico. In questo scenario, mantenere la coerenza tra sistemi diventa un problema di prima classe: l'aggiornamento di un record in un database operazionale deve riflettersi nel data warehouse analitico, nell'indice di ricerca, nella cache e nei servizi downstream, idealmente con latenza nell'ordine dei secondi e senza perdita di eventi.

Il Change Data Capture (CDC) è la classe di tecniche che cattura le mutazioni (insert, update, delete) applicate a un database e le propaga come flusso ordinato di eventi a sistemi downstream. Kleppmann [1] ha inquadrato il CDC all'interno di una visione più ampia dell'architettura dati: il database non è l'unica sorgente di verità, ma un derived view di un log append-only di eventi. Questa prospettiva, che Kreps [2] aveva già articolato nel 2013 proponendo il log come astrazione unificante per l'integrazione dati in tempo reale, rovescia la relazione tradizionale tra database e replica: il log è la sorgente primaria, e ogni database, indice o cache è una materializzazione derivata. Il CDC, in questa visione, è il meccanismo che rende esplicito e accessibile il log implicito che ogni database relazionale già mantiene internamente.

Il sistema pionieristico in ambito industriale è stato LinkedIn Databus [3], presentato a SOCC 2012: una piattaforma source-agnostic che catturava le mutazioni dal transaction log di Oracle e le distribuiva a consumatori downstream con latenza nell'ordine dei millisecondi e throughput di migliaia di eventi al secondo per server. Databus ha introdotto concetti architetturali che restano centrali: il pull model basato su clock logici per la distribuzione, la separazione tra relay (che leggono il log sorgente) e bootstrap (che forniscono snapshot completi ai nuovi consumatori), e la garanzia di ordinamento totale degli eventi per partizione. Netflix ha successivamente sviluppato DBLog [4], un framework CDC basato su watermark che risolve uno dei limiti fondamentali dei sistemi precedenti: l'impossibilità di interleaving tra eventi del transaction log e snapshot selettivi delle tabelle. L'approccio a watermark consente di acquisire lo stato completo di qualsiasi tabella, o di un sottoinsieme di chiavi primarie, senza bloccare il log e senza richiedere lock sulle tabelle sorgente, un requisito critico per i sistemi in produzione con carichi elevati [4].

Oggi il panorama degli strumenti CDC è dominato da Debezium [5], una piattaforma open-source basata su Apache Kafka Connect che supporta i principali database relazionali e non. La maturità dell'ecosistema, con alternative come Apache Flink CDC [6] per l'integrazione diretta in pipeline di stream processing, ha spostato il dibattito tecnico dalle questioni fondamentali (come catturare le mutazioni) ai problemi di ingegneria di produzione: gestione degli snapshot senza downtime, evoluzione degli schemi, exactly-once semantics end-to-end e operatività su infrastrutture cloud-native.


Meccanismi di cattura: dal polling al transaction log

Le tecniche di CDC si dividono in due famiglie architetturali con caratteristiche radicalmente diverse: query-based (o polling) e log-based. La scelta tra le due non è una questione di preferenza, ma di vincoli operativi che determinano la fattibilità stessa della soluzione in produzione.

Il CDC query-based interroga periodicamente le tabelle sorgente utilizzando una colonna di timestamp (tipicamente last_modified) o un identificatore sequenziale per individuare i record modificati dall'ultima esecuzione. L'approccio ha il vantaggio della semplicità implementativa, dato che non richiede configurazione del database né accesso a componenti interni, ma presenta limiti strutturali. Le operazioni di DELETE non sono catturabili senza meccanismi aggiuntivi (soft delete o trigger), perché il record eliminato non è più presente nella tabella. La risoluzione temporale è limitata alla frequenza di polling: con un intervallo di 60 secondi, la latenza media è di 30 secondi e la massima di 60. Il carico sul database sorgente cresce linearmente con il numero di tabelle monitorate e con il volume dei dati, poiché ogni ciclo di polling esegue query sull'intero delta temporale. Per sistemi con volumi elevati di scritture su decine di tabelle, il polling diventa rapidamente insostenibile: il carico di query sul database sorgente cresce proporzionalmente, degradando le performance delle operazioni applicative [1].

Il CDC log-based sfrutta il transaction log che ogni database relazionale mantiene per garantire la durabilità ACID: il Write-Ahead Log (WAL) in PostgreSQL, il binary log (binlog) in MySQL, il redo log in Oracle. Ogni mutazione viene scritta nel log prima di essere applicata alle pagine dati: è il principio stesso del write-ahead logging, che garantisce il recovery in caso di crash. Il CDC log-based si inserisce come consumatore aggiuntivo di questo log, leggendo le mutazioni nella sequenza esatta in cui il database le ha committate, senza eseguire query sulle tabelle e quindi con un impatto sulle performance del sistema sorgente significativamente inferiore rispetto al polling, sebbene non nullo, in quanto la configurazione per il CDC (ad esempio, REPLICA IDENTITY FULL in PostgreSQL) aumenta il volume del log generato.

In PostgreSQL, il meccanismo si basa sulla logical decoding introdotta nella versione 9.4: un output plugin (come pgoutput, nativo dal 10, o decoderbufs per il formato Protobuf) decodifica il WAL binario in un formato logico leggibile, estraendo le mutazioni a livello di riga. I logical replication slot mantengono un puntatore (Log Sequence Number, LSN) che garantisce al consumatore che i segmenti WAL necessari non vengano eliminati dal database fino al loro completo processamento: un meccanismo di backpressure implicito che, se il consumatore si arresta per un periodo prolungato, può però causare una crescita illimitata dello spazio disco occupato dal WAL [5]. L'impostazione wal_level = logical e REPLICA IDENTITY FULL (per ottenere le immagini before e after del record) sono prerequisiti di configurazione non trascurabili, perché impattano il volume del WAL generato.

In MySQL, il CDC log-based si basa sul binary log in formato row-based (configurazione binlog_format = ROW, binlog_row_image = FULL). A differenza di PostgreSQL, il binlog è un meccanismo di replica nativo progettato per la replica primary-secondary, e il connettore CDC si presenta al database come una replica secondaria, ricevendo il flusso di eventi attraverso il protocollo di replication. La retention del binlog è basata su tempo o dimensione (binlog_expire_logs_seconds), e se il connettore CDC rimane disconnesso oltre il periodo di retention, gli eventi intermedi sono irrecuperabilmente persi: un failure mode qualitativamente diverso dal replication slot di PostgreSQL, che preserva i dati a costo di spazio disco [5].

Il trade-off fondamentale tra query-based e log-based è dunque chiaro: il polling è semplice da implementare ma intrinsecamente limitato in completezza (nessun DELETE), latenza (polling interval) e scalabilità (carico proporzionale al volume). Il log-based è completo, a bassa latenza e a basso impatto, ma richiede accesso privilegiato al database, configurazione specifica, e introduce complessità operativa nella gestione dei replication slot e della retention dei log.


Architettura di una pipeline CDC: Debezium e Kafka Connect

Debezium [5] è la piattaforma CDC open-source più adottata nell'ecosistema Apache Kafka. L'architettura è modulare e si basa su tre componenti fondamentali: i source connector, il framework Kafka Connect e il broker Apache Kafka come canale di distribuzione degli eventi.

Kafka Connect [7] è un framework distribuito per lo streaming di dati tra Apache Kafka e sistemi esterni. Opera come un cluster separato dal broker Kafka, gestendo il ciclo di vita dei connettori, il bilanciamento del carico tra i worker e il tracking degli offset di processamento. I connettori Debezium sono implementati come source connector di Kafka Connect: ogni istanza si connette a un database sorgente, legge il transaction log e produce eventi di mutazione su topic Kafka. La separazione tra il framework (Kafka Connect) e la logica di cattura (Debezium) consente di gestire scalabilità, fault tolerance e configurazione a livello infrastrutturale, senza duplicare questa logica in ogni connettore.

Il diagramma seguente illustra il flusso architetturale di una pipeline CDC basata su Debezium e Kafka Connect.

graph LR
    A[Database sorgente] -->|Transaction Log<br/>WAL / Binlog| B[Debezium Connector]
    B -->|Kafka Connect<br/>Framework| C[Apache Kafka<br/>Topic per tabella]
    C --> D[Sink Connector<br/>Elasticsearch]
    C --> E[Sink Connector<br/>Data Warehouse]
    C --> F[Consumer applicativo<br/>Microservizio]

    subgraph Kafka Connect Cluster
        B
        D
        E
    end

    G[Schema Registry] -.->|Validazione schema| B
    G -.->|Validazione schema| D
    G -.->|Validazione schema| E

Figura 1. Architettura di una pipeline CDC con Debezium, Kafka Connect e consumatori downstream eterogenei.

Il flusso di una mutazione attraverso la pipeline segue un percorso deterministico. Una transazione viene committata nel database sorgente e scritta nel transaction log. Il connettore Debezium, che mantiene una connessione persistente al log (come replica in MySQL, come consumer di replication slot in PostgreSQL), legge l'evento, lo deserializza dal formato binario nativo e lo trasforma in un record strutturato con schema definito. Il record contiene metadati dell'operazione (tipo: create, update, delete), il valore before (lo stato del record prima della mutazione), il valore after (lo stato dopo la mutazione), e i metadati della sorgente (database, schema, tabella, LSN/binlog position, timestamp della transazione). Questo record viene serializzato, tipicamente in formato Avro con schema registrato nel Confluent Schema Registry [8] o in JSON, e pubblicato su un topic Kafka il cui nome corrisponde per default alla combinazione <server>.<schema>.<tabella>.

La gestione degli snapshot iniziali è uno degli aspetti architetturali più critici. Quando un connettore viene avviato per la prima volta, il transaction log non contiene lo storico completo dei dati: contiene solo le mutazioni recenti, nei limiti della retention configurata. È quindi necessario uno snapshot iniziale che catturi lo stato corrente di tutte le tabelle monitorate, prima di passare alla modalità di streaming continuo dal log. Lo snapshot tradizionale richiede un lock globale sulle tabelle, un'operazione inaccettabile su database in produzione con traffico elevato. Debezium ha introdotto l'incremental snapshot [5], un meccanismo ispirato all'approccio a watermark di DBLog [4]: le tabelle vengono lette in chunk ordinati per chiave primaria, interleaving la lettura dei chunk con il processamento continuo degli eventi dal transaction log. Watermark di segnalazione, inseriti in una tabella dedicata e catturati dal log stesso, consentono al connettore di risolvere i conflitti tra gli eventi del log e i record dello snapshot, garantendo che ogni record appaia esattamente una volta nell'output finale. L'incremental snapshot può essere avviato, sospeso e ripreso senza interrompere lo streaming, e può essere ri-eseguito su tabelle specifiche tramite segnali inviati attraverso la stessa tabella di segnalazione [5].

Un'alternativa architetturale a Debezium su Kafka Connect è Apache Flink CDC [6], che integra la cattura delle mutazioni direttamente nel motore di stream processing. Anziché scrivere gli eventi su un broker intermedio, Flink CDC li inietta come sorgenti native di una pipeline Flink, consentendo trasformazioni, join e aggregazioni in un unico job distribuito. La pipeline CDC viene definita in YAML o SQL, con supporto per la sincronizzazione di interi database, l'evoluzione degli schemi e la trasformazione inline. L'approccio elimina un componente infrastrutturale (il cluster Kafka Connect separato) ma introduce un accoppiamento architetturale più stretto tra la cattura e l'elaborazione, che può risultare un vincolo quando i consumatori downstream sono molteplici e indipendenti: uno scenario in cui il disaccoppiamento offerto dal broker Kafka è preferibile.


Outbox pattern e garanzie transazionali

In un'architettura a microservizi, un'operazione di business tipicamente richiede due azioni atomiche: aggiornare il database locale del servizio e pubblicare un evento su un message broker per notificare i servizi downstream. Il problema è noto come dual write: se il database commit avviene ma la pubblicazione dell'evento fallisce (o viceversa), il sistema si trova in uno stato inconsistente. I meccanismi di distributed transaction (XA/2PC) risolvono il problema in teoria, ma in pratica introducono latenza, complessità operativa e un accoppiamento tra il database e il broker che contraddice il principio di indipendenza dei microservizi.

Il Transactional Outbox Pattern, formalizzato da Richardson [9] nel contesto dei pattern per microservizi, risolve il dual write eliminandolo: anziché scrivere nel database e pubblicare un evento come due operazioni separate, il servizio scrive sia i dati di business sia l'evento in una tabella outbox all'interno della stessa transazione ACID. L'evento è ora durabilmente persistito nel database, con le stesse garanzie di consistenza dei dati di business. Un processo separato, il relay, è responsabile di leggere la tabella outbox e propagare gli eventi al message broker.

L'implementazione del relay può seguire due strategie. Il polling publisher interroga periodicamente la tabella outbox per individuare i record non ancora pubblicati, li trasmette al broker e li marca come processati (o li elimina). Questa strategia è semplice ma soffre delle stesse limitazioni del CDC query-based: latenza proporzionale all'intervallo di polling e carico aggiuntivo sul database. La strategia alternativa, e architetturalmente superiore, è il transaction log tailing: un connettore CDC (tipicamente Debezium) cattura le mutazioni sulla tabella outbox direttamente dal transaction log, senza query aggiuntive e con latenza nell'ordine dei millisecondi. Debezium fornisce un componente dedicato, l'Outbox Event Router [5], una Single Message Transform (SMT) che ristruttura gli eventi catturati dalla tabella outbox nel formato atteso dai consumatori downstream: estrae il tipo di aggregato, l'identificatore, il tipo di evento e il payload, e instrada l'evento sul topic Kafka corrispondente.

La struttura tipica della tabella outbox prevede colonne per l'identificatore univoco dell'evento, il tipo di aggregato (che determina il topic di destinazione), l'identificatore dell'aggregato (che determina la chiave di partizionamento in Kafka), il tipo di evento e il payload serializzato (tipicamente JSON o Avro). La scelta del tipo di aggregato come criterio di routing e dell'identificatore dell'aggregato come chiave di partizionamento garantisce che tutti gli eventi relativi alla stessa entità di business siano ordinati nello stesso topic e nella stessa partizione, una proprietà essenziale per i consumatori che devono ricostruire lo stato dell'entità in ordine cronologico.

La relazione tra outbox pattern e event sourcing merita una distinzione esplicita, perché i due approcci vengono spesso confusi. Nell'event sourcing, il log degli eventi è la sorgente di verità: lo stato corrente dell'aggregato viene ricostruito riapplicando tutti gli eventi dalla storia. Nel pattern outbox, la sorgente di verità resta il database relazionale con le sue tabelle tradizionali; la tabella outbox è un meccanismo di trasporto, non di persistenza dello stato. Il blog tecnico di Debezium [10] ha esplicitamente confrontato i due approcci, evidenziando che l'outbox pattern con CDC offre un percorso di adozione incrementale, dato che si integra con schemi relazionali esistenti senza richiedere una riscrittura dell'intero modello di persistenza, mentre l'event sourcing richiede un ripensamento architetturale profondo del dominio. In pratica, molti sistemi in produzione adottano l'outbox pattern come strategia pragmatica per ottenere una pubblicazione di eventi affidabile, riservando l'event sourcing ai sotto-domini dove il valore della storia completa delle mutazioni giustifica la complessità aggiuntiva.


Exactly-once semantics nella pipeline CDC

La garanzia di exactly-once semantics (EOS) in una pipeline CDC end-to-end è un problema che attraversa tre confini di sistema: la lettura dal transaction log, il trasporto attraverso il broker, e la scrittura nel sistema di destinazione. Ogni confine introduce possibilità di duplicazione o perdita.

Sul lato sorgente, il connettore Debezium mantiene l'offset di lettura del transaction log (LSN in PostgreSQL, binlog position in MySQL) come parte dello stato gestito da Kafka Connect. In caso di restart, il connettore riprende dalla posizione salvata. Il rischio di duplicazione si presenta quando il connettore ha prodotto eventi su Kafka ma non ha ancora committato l'offset: al restart, gli stessi eventi verranno riprodotti. La semantica risultante è at-least-once dal lato sorgente.

Sul lato broker, Apache Kafka fornisce meccanismi nativi per l'exactly-once, introdotti con KIP-98 [11]: l'idempotent producer (che assegna un Producer ID e sequence number a ogni messaggio, consentendo al broker di deduplicare i retry) e le transazioni (che raggruppano atomicamente la produzione di messaggi su più partizioni e il commit degli offset del consumer). Kafka Connect supporta l'exactly-once source connector a partire da KIP-618, dove il framework coordina la produzione degli eventi e il salvataggio degli offset in una singola transazione Kafka. Questa funzionalità, disponibile in Debezium, riduce significativamente la finestra di duplicazione sul lato sorgente, ma richiede che il cluster Kafka sia configurato con exactly.once.source.support = enabled e che i connettori siano eseguiti in un cluster Connect dedicato [7].

Sul lato sink (il sistema di destinazione), l'exactly-once dipende interamente dalle capacità del sistema downstream. Se il sink è un database relazionale, l'idempotenza può essere ottenuta con operazioni UPSERT basate sulla chiave primaria: un evento duplicato sovrascrive il record con lo stesso valore, producendo un risultato identico. Se il sink è un sistema che non supporta UPSERT nativi (un sistema di notifiche, un'API esterna), l'idempotenza deve essere implementata applicativamente, tipicamente attraverso un registro degli identificatori di eventi già processati: il pattern dell'idempotent consumer. La combinazione di at-least-once delivery dal broker e idempotent consumer al sink produce una semantica effectively-once: gli eventi possono essere consegnati più di una volta, ma il risultato osservabile è equivalente a una consegna singola [1].

L'exactly-once end-to-end è dunque una proprietà emergente della composizione di meccanismi distribuiti tra sorgente, broker e destinazione, e non una garanzia monolitica fornita da un singolo componente. In pratica, la configurazione robusta richiede: idempotent producer e transazioni Kafka sul lato broker, exactly-once source support in Kafka Connect, e idempotent consumer o UPSERT semantics sul lato sink. L'omissione di uno qualsiasi di questi livelli riduce la garanzia complessiva a at-least-once.


Schema evolution e propagazione delle modifiche strutturali

In un sistema reale, lo schema del database sorgente evolve nel tempo: nuove colonne vengono aggiunte, tipi modificati, colonne rimosse. In una pipeline CDC, ogni modifica strutturale al database sorgente deve propagarsi attraverso l'intera catena (connettore, broker, schema registry, consumatori) senza interrompere il funzionamento dei sistemi a valle. La gestione dell'evoluzione degli schemi è, in pratica, uno degli aspetti operativi più complessi del CDC in produzione.

Debezium intercetta le Data Definition Language (DDL) statements dal transaction log e aggiorna lo schema interno del connettore, che utilizza per deserializzare correttamente i record successivi. Quando la serializzazione utilizza Apache Avro con il Confluent Schema Registry [8], ogni nuovo schema generato dal connettore viene registrato nel registry con un controllo di compatibilità. Il Schema Registry supporta quattro modalità di compatibilità: BACKWARD (i consumatori con lo schema nuovo possono leggere dati prodotti con lo schema precedente), FORWARD (i consumatori con lo schema precedente possono leggere dati prodotti con lo schema nuovo), FULL (entrambe le direzioni) e NONE (nessun controllo). La scelta della modalità ha implicazioni operative dirette: in modalità BACKWARD, l'aggiunta di una colonna con valore di default è un'operazione sicura, perché i consumatori aggiornati possono gestire record prodotti prima dell'aggiunta; la rimozione di una colonna richiede invece un rollout coordinato in cui tutti i consumatori vengono aggiornati prima che il produttore inizi a emettere record con il nuovo schema.

La complessità aumenta quando i consumatori downstream operano con cicli di deployment indipendenti, lo scenario tipico nei microservizi. In questo caso, la modalità FULL compatibility è la più conservativa: accetta solo modifiche che sono simultaneamente backward e forward compatible, ovvero l'aggiunta di campi con valore di default e la rimozione di campi che avevano un valore di default. Questa modalità limita lo spazio delle modifiche ammissibili, ma garantisce che produttori e consumatori possano essere aggiornati in qualsiasi ordine senza interruzione del servizio.

Un aspetto critico nella pratica è la gestione delle modifiche non backward-compatible: la rimozione di una colonna, il cambiamento di tipo di un campo o la modifica di una chiave primaria. Queste operazioni violano le regole di compatibilità nella maggior parte delle configurazioni del Schema Registry, e richiedono una strategia di migrazione deliberata: duplicazione temporanea del topic con il nuovo schema, migrazione graduale dei consumatori, e decommissioning del topic precedente. L'alternativa, ovvero disabilitare i controlli di compatibilità con modalità NONE, elimina la protezione automatica e trasferisce l'intero onere della validazione agli sviluppatori, un approccio fragile in team con molti servizi indipendenti.

Apache Flink CDC [6] adotta un approccio diverso alla schema evolution: il framework può propagare automaticamente le modifiche DDL dal database sorgente al sink, incluse operazioni come l'aggiunta di colonne e il rename di tabelle, mantenendo la pipeline attiva senza intervento manuale. Questo approccio riduce il carico operativo per pipeline point-to-point con un singolo sink, ma non elimina la complessità della compatibilità multi-consumatore che il Schema Registry affronta a livello di ecosistema.


Limiti e problemi aperti

L'impatto sullo storage del database sorgente è un vincolo operativo che viene frequentemente sottovalutato. In PostgreSQL, un replication slot impedisce la rimozione dei segmenti WAL fino al loro completo consumo: se il connettore CDC si arresta per un periodo prolungato, a causa di un guasto, un aggiornamento o un problema di rete, il WAL cresce indefinitamente, rischiando di esaurire lo spazio disco del database primario. Questo failure mode è particolarmente insidioso perché il problema non si manifesta nel sistema CDC ma nel database di produzione. Le mitigazioni includono il monitoraggio della dimensione del WAL e del ritardo del replication slot, l'impostazione di max_slot_wal_keep_size per limitare la crescita (a costo di perdere eventi), e l'implementazione di alert operativi che segnalino il superamento di soglie critiche [5]. In MySQL, il problema è strutturalmente diverso ma complementare: la retention del binlog è basata su tempo, e un'interruzione del connettore oltre il periodo di retention causa la perdita silenziosa degli eventi: un failure mode che richiede un re-snapshot completo per il recovery.

La gestione dello schema in presenza di transazioni DDL concorrenti resta un problema aperto. Se un'operazione ALTER TABLE avviene mentre il connettore sta processando eventi che fanno riferimento allo schema precedente, il connettore deve determinare il punto esatto del log in cui lo schema cambia e applicare la deserializzazione corretta a ciascun evento. Debezium gestisce questo scenario attraverso uno schema history topic interno, ma casi edge, come DDL all'interno di transazioni non ancora committate in database che supportano transactional DDL (PostgreSQL), possono generare inconsistenze che richiedono intervento manuale [5].

L'exactly-once end-to-end, come analizzato nella sezione dedicata, resta una proprietà difficile da ottenere e verificare in pratica. L'interazione tra il connettore CDC, il broker Kafka e i consumatori downstream introduce molteplici punti dove la deduplicazione può fallire: offset non committati, consumer rebalancing, sink non idempotenti. Il pattern effectively-once (at-least-once con idempotent consumer) è la strategia più pragmatica, ma richiede che ogni consumatore implementi correttamente la logica di deduplicazione: un requisito che scala linearmente con il numero di servizi downstream.

Le sfide del multi-tenancy e del CDC su larga scala meritano attenzione. In ambienti con centinaia di microservizi, ciascuno con il proprio database, il numero di connettori CDC può diventare significativo. Ogni connettore mantiene una connessione persistente al database sorgente e occupa risorse nel cluster Kafka Connect. Il capacity planning, ovvero quanti connettori per worker, quanti worker per cluster e quanta banda di rete, diventa un esercizio di ingegneria non banale. Debezium Server [5], una modalità di deployment lightweight che opera senza Kafka Connect, riduce l'overhead infrastrutturale per scenari con pochi connettori, ma sacrifica le funzionalità di scalabilità e fault tolerance del framework Kafka Connect.

L'integrazione del CDC con architetture event-driven introduce infine un problema di ordinamento che attraversa i confini dei singoli servizi. Il CDC garantisce l'ordinamento degli eventi all'interno di una singola tabella (e, in Kafka, all'interno di una singola partizione), ma non tra tabelle diverse né tra servizi diversi. Quando una logica di business richiede la correlazione di mutazioni su più tabelle, ad esempio un ordine e le sue righe, l'ordinamento relativo degli eventi dipende dall'ordine di commit delle transazioni e dalla configurazione del partizionamento Kafka. Il pattern aggregato, in cui le mutazioni correlate sono raggruppate in un singolo evento attraverso la tabella outbox, mitiga il problema ma richiede una progettazione deliberata del modello di dominio che non è sempre retrofittabile su schemi relazionali esistenti.


Riferimenti

[1] M. Kleppmann, Designing Data-Intensive Applications, O'Reilly Media, 2017.

[2] J. Kreps, "The Log: What every software engineer should know about real-time data's unifying abstraction," LinkedIn Engineering Blog, 2013. https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying

[3] S. Das et al., "All aboard the Databus! LinkedIn's scalable consistent change data capture platform," in Proc. ACM SOCC, 2012. https://www.researchgate.net/publication/262155021_All_aboard_the_Databus_Linkedin's_scalable_consistent_change_data_capture_platform

[4] A. Andreakis, I. Papapanagiotou, "DBLog: A Watermark Based Change-Data-Capture Framework," arXiv:2010.12597, 2020. https://arxiv.org/abs/2010.12597

[5] Red Hat, "Debezium Documentation," 2025. https://debezium.io/documentation/reference/stable/architecture.html

[6] Apache Software Foundation, "Apache Flink CDC Documentation," 2025. https://nightlies.apache.org/flink/flink-cdc-docs-stable/

[7] Apache Software Foundation, "Apache Kafka Connect Documentation," 2025. https://kafka.apache.org/documentation/#connect

[8] Confluent, "Schema Registry Documentation," 2025. https://docs.confluent.io/platform/current/schema-registry/fundamentals/schema-evolution.html

[9] C. Richardson, Microservices Patterns, Manning Publications, 2018.

[10] Debezium Blog, "Event Sourcing vs. Change Data Capture," 2020. https://debezium.io/blog/2020/02/10/event-sourcing-vs-cdc/

[11] Apache Software Foundation, "KIP-98: Exactly Once Delivery and Transactional Messaging," Apache Kafka Wiki. https://cwiki.apache.org/confluence/display/KAFKA/KIP-98+-+Exactly+Once+Delivery+and+Transactional+Messaging

Change Data Capture (CDC)

Raccontaci la situazione. Rispondiamo entro 24 ore nei giorni lavorativi.

Tweaks

Light mode
Atmospheric (glass)
Client logos
Terminal hero