Apache Kafka per Data Engineer: Streaming, Partizioni e Domande di Colloquio

Apache Kafka per il data engineering: architettura KRaft, strategie di partizionamento, consumer group, CDC con Debezium, exactly-once semantics e domande di colloquio con esempi Kafka 4.x.

Architettura Apache Kafka Streaming per Data Engineering

Apache Kafka rappresenta il componente infrastrutturale cardine delle architetture dati moderne. Nato in LinkedIn per gestire i flussi di attività degli utenti, il sistema si è affermato come lo standard de facto per lo streaming distribuito nelle piattaforme data di ogni dimensione. Nel 2026, Kafka elabora quotidianamente migliaia di miliardi di eventi presso aziende come Netflix, Uber e Airbnb. Per qualsiasi data engineer, padroneggiare i meccanismi interni di Kafka -- partizioni, consumer group, garanzie di consegna e semantiche transazionali -- non è più facoltativo: costituisce un requisito imprescindibile per progettare pipeline affidabili e scalabili. In questa guida vengono analizzate le architetture fondamentali, le strategie di partizionamento, il Change Data Capture e le domande più frequenti nei colloqui di data engineering.

Contenuto dell'articolo

Architettura Kafka 4.x con KRaft, strategie di partizionamento, consumer group con gestione manuale degli offset, CDC con Debezium e Kafka Connect, semantica exactly-once, Share Groups (KIP-932) e domande di colloquio per data engineer.

Architettura Kafka 4.x: il modo KRaft e la fine di ZooKeeper

Kafka 4.0, rilasciato nel marzo 2025, ha eliminato definitivamente la dipendenza da ZooKeeper. Il modo KRaft (Kafka Raft) gestisce i metadati del cluster direttamente all'interno dei broker, utilizzando un protocollo di consenso basato su Raft. Questa semplificazione architetturale riduce il numero di componenti da amministrare, accelera il recovery in caso di guasto e consente la scalabilità fino a milioni di partizioni per cluster.

Per il data engineering, l'eliminazione di ZooKeeper si traduce in deployment più semplici. Un cluster di sviluppo locale si avvia con un singolo file Docker Compose.

yaml
# docker-compose.yml — Kafka 4.x KRaft cluster (no ZooKeeper)
services:
  kafka-1:
    image: apache/kafka:4.2.0
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093
      KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_LOG_DIRS: /var/lib/kafka/data
      KAFKA_NUM_PARTITIONS: 6
      KAFKA_DEFAULT_REPLICATION_FACTOR: 3
      KAFKA_MIN_INSYNC_REPLICAS: 2
    ports:
      - "9092:9092"

Il parametro KAFKA_PROCESS_ROLES: broker,controller indica che ogni nodo svolge entrambi i ruoli. In produzione, è pratica comune separare controller e broker su nodi dedicati per isolare il carico di gestione dei metadati dall'elaborazione dei messaggi. La variabile KAFKA_CONTROLLER_QUORUM_VOTERS definisce i nodi partecipanti al quorum Raft. Con un fattore di replica pari a 3 e un minimo di 2 repliche sincrone (MIN_INSYNC_REPLICAS), il cluster tollera la perdita di un nodo senza interruzione del servizio né perdita di dati.

Replication Factor e Min ISR

Un replication_factor di 3 con min.insync.replicas di 2 garantisce che qualsiasi messaggio confermato con acks=all sopravviva alla caduta di un broker. Questa configurazione offre un equilibrio solido tra durabilità e disponibilità per la maggior parte dei pipeline di dati.

Strategie di partizionamento: il cuore del parallelismo

Le partizioni rappresentano il meccanismo fondamentale di parallelismo in Kafka. Ogni topic viene suddiviso in una o più partizioni, ciascuna delle quali costituisce un log ordinato e immutabile. I messaggi all'interno di una stessa partizione mantengono un ordine rigoroso e sequenziale, ma nessun ordine globale è garantito tra partizioni diverse.

La scelta della chiave di partizionamento determina quali messaggi finiscono in quale partizione. Tale scelta impatta direttamente tre aspetti critici: l'ordine di elaborazione, la distribuzione del carico e la capacità di scalare orizzontalmente.

Esistono tre strategie principali per la scelta delle chiavi di partizione nel contesto del data engineering:

Partizionamento per entità di business: si utilizza l'identificativo dell'entità principale (customer_id, account_id) come chiave. Tutti gli eventi di uno stesso cliente confluiscono nella stessa partizione, garantendo l'ordine cronologico. Rappresenta la strategia più diffusa per pipeline di ordini, transazioni e attività utente.

Partizionamento per sessione: per dati di clickstream o telemetria, la chiave session_id raggruppa tutti gli eventi di una sessione nella stessa partizione. Utilizzare user_id sarebbe scorretto se un utente può avere sessioni simultanee multiple, poiché si perderebbe l'ordine all'interno di ciascuna sessione.

Partizionamento round-robin: quando l'ordine non è rilevante (log, metriche generali), l'omissione della chiave distribuisce i messaggi in modo uniforme, massimizzando il parallelismo a scapito di qualsiasi garanzia di ordine.

python
# partition_strategy.py — Choosing the right partition key
from confluent_kafka import Producer
import json

def create_producer():
    return Producer({
        'bootstrap.servers': 'kafka-1:9092',
        'acks': 'all',
        'enable.idempotence': True,
        'max.in.flight.requests.per.connection': 5
    })

def publish_order_event(producer, event):
    # Key by customer_id: all events for one customer stay ordered
    key = str(event['customer_id']).encode('utf-8')
    value = json.dumps(event).encode('utf-8')
    producer.produce(
        topic='order-events',
        key=key,
        value=value,
        headers=[('source', b'order-service'), ('version', b'2')]
    )
    producer.flush()

def publish_clickstream(producer, event):
    # Key by session_id: ordering within a session matters
    # NOT by user_id — one user may have multiple sessions
    key = str(event['session_id']).encode('utf-8')
    value = json.dumps(event).encode('utf-8')
    producer.produce(
        topic='clickstream',
        key=key,
        value=value
    )

La configurazione enable.idempotence: True in combinazione con acks: all abilita la produzione idempotente, prevenendo duplicati quando il producer ritenta un invio a causa di timeout di rete. Il parametro max.in.flight.requests.per.connection: 5 preserva l'ordine anche con retry attivi, un miglioramento introdotto in Kafka 2.0 che ha eliminato la precedente restrizione di limitare le richieste in volo a 1.

Hot partition: la trappola più comune

Se una chiave di partizionamento è mal distribuita -- ad esempio un identificativo di merchant in un sistema dove un singolo merchant genera l'80% del traffico -- una partizione riceverà la maggior parte dei messaggi. Questo squilibrio, noto come hot partition, provoca un collo di bottiglia sul broker responsabile e annulla i benefici del parallelismo. La regola fondamentale: scegliere una chiave i cui valori siano distribuiti in modo uniforme.

Quante partizioni configurare?

Il numero di partizioni di un topic determina il parallelismo massimo lato consumer. Se un topic possiede 12 partizioni, al massimo 12 consumer di uno stesso consumer group possono elaborare il topic in parallelo. Un tredicesimo consumer rimarrebbe inattivo.

In pratica, la regola seguente funziona come punto di partenza: dividere il throughput target (in MB/s) per il throughput massimo che un singolo consumer riesce a gestire. Per un topic che riceve 120 MB/s e consumer capaci di elaborare 10 MB/s ciascuno, 12 partizioni costituiscono un minimo ragionevole. Conviene prevedere un margine, poiché aumentare il numero di partizioni di un topic esistente redistribuisce le chiavi e interrompe temporaneamente le garanzie di ordine.

Consumer group e gestione manuale degli offset

I consumer group rappresentano il meccanismo primario di parallelismo nel consumo dei dati in Kafka. Ogni consumer all'interno di un gruppo riceve un sottoinsieme esclusivo di partizioni. Quando un consumer si disconnette o ne viene aggiunto uno nuovo, Kafka esegue un rebalancing che ridistribuisce le partizioni tra i membri attivi del gruppo.

Per i pipeline di dati, il pattern raccomandato prevede la disattivazione dell'auto-commit degli offset e la conferma manuale dopo l'elaborazione di ogni batch. Questo approccio garantisce la semantica at-least-once: se il consumer si interrompe prima della conferma, i messaggi vengono rielaborati al riavvio. Il corollario è che la logica di elaborazione deve essere idempotente per gestire correttamente eventuali duplicati.

L'elaborazione in micro-batch migliora significativamente il throughput, ammortizzando le operazioni di scrittura verso il data warehouse. Invece di scrivere record per record, i messaggi vengono accumulati in un buffer e scaricati con un'unica operazione di scrittura per batch.

python
# consumer_pipeline.py — Consumer group with manual offset management
from confluent_kafka import Consumer, KafkaError
import json

def create_consumer(group_id: str):
    return Consumer({
        'bootstrap.servers': 'kafka-1:9092',
        'group.id': group_id,
        'auto.offset.reset': 'earliest',
        'enable.auto.commit': False,  # Manual commit for at-least-once
        'max.poll.interval.ms': 300000,
        'session.timeout.ms': 45000,
        'isolation.level': 'read_committed'  # Only read committed transactions
    })

def process_batch(consumer: Consumer, batch_size: int = 500):
    """Process records in micro-batches for throughput."""
    consumer.subscribe(['order-events'])
    buffer = []

    while True:
        msg = consumer.poll(timeout=1.0)
        if msg is None:
            continue
        if msg.error():
            if msg.error().code() == KafkaError._PARTITION_EOF:
                continue
            raise Exception(msg.error())

        record = json.loads(msg.value().decode('utf-8'))
        buffer.append(record)

        if len(buffer) >= batch_size:
            # Process the full batch (write to warehouse, etc.)
            write_to_warehouse(buffer)
            # Commit AFTER successful processing
            consumer.commit(asynchronous=False)
            buffer.clear()

def write_to_warehouse(records: list):
    # Insert batch into data warehouse (BigQuery, Snowflake, etc.)
    pass

Il parametro isolation.level: read_committed risulta essenziale quando i producer utilizzano transazioni. Con questa configurazione, il consumer legge esclusivamente messaggi confermati da transazioni completate con successo, evitando di elaborare dati provenienti da transazioni abortite. La configurazione di max.poll.interval.ms: 300000 (5 minuti) definisce il tempo massimo consentito tra chiamate successive a poll(). Se l'elaborazione di un batch richiede più tempo, Kafka considera il consumer inattivo e avvia un rebalancing.

Change Data Capture con Debezium e Kafka Connect

Il Change Data Capture (CDC) rappresenta uno dei casi d'uso più potenti di Kafka nell'ambito del data engineering. Invece di interrogazioni periodiche sulla base dati sorgente (polling), Debezium legge il journal delle transazioni (WAL) di PostgreSQL, MySQL o MongoDB e pubblica ogni modifica come evento in un topic Kafka. Per approfondire i pattern di integrazione dati, consultare la guida sui pattern ETL/ELT.

Questo approccio offre tre vantaggi decisivi: latenza quasi nulla tra la modifica in database e la sua propagazione, nessun carico aggiuntivo sulla base dati sorgente (la lettura del WAL è passiva) e la cattura esaustiva di tutte le operazioni, comprese le eliminazioni (DELETE).

json
{
  "name": "postgres-cdc-source",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "database.hostname": "postgres-primary",
    "database.port": "5432",
    "database.user": "replication_user",
    "database.dbname": "production",
    "topic.prefix": "cdc",
    "table.include.list": "public.orders,public.customers,public.products",
    "slot.name": "debezium_slot",
    "plugin.name": "pgoutput",
    "publication.name": "dbz_publication",
    "snapshot.mode": "initial",
    "transforms": "route",
    "transforms.route.type": "org.apache.kafka.connect.transforms.RegexRouter",
    "transforms.route.regex": "cdc\\.public\\.(.*)",
    "transforms.route.replacement": "warehouse.$1"
  }
}

Il campo snapshot.mode: initial indica che Debezium esegue prima un'istantanea completa delle tabelle configurate, poi passa allo streaming delle modifiche incrementali. Il transform RegexRouter rinomina i topic di output (ad esempio, cdc.public.orders diventa warehouse.orders), facilitando l'organizzazione logica dei topic per destinazione. Il replication slot (debezium_slot) garantisce che PostgreSQL non elimini segmenti WAL non ancora consumati.

Monitoraggio dei replication slot

Se Debezium si arresta o si disconnette per un periodo prolungato, il replication slot accumula segmenti WAL non elaborati. Si raccomanda di configurare alert sulla dimensione dello slot e di stabilire un limite massimo di retention in PostgreSQL (max_slot_wal_keep_size) per evitare la saturazione dello spazio su disco del server database.

Pronto a superare i tuoi colloqui su Data Engineering?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Pipeline exactly-once: transazioni Kafka end-to-end

La semantica exactly-once rappresenta l'obiettivo più ambizioso dei pipeline di streaming. Senza di essa, un pipeline rischia di produrre duplicati (at-least-once) o di perdere messaggi (at-most-once). Kafka implementa l'exactly-once tramite transazioni producer combinate con l'isolamento read_committed lato consumer.

Il pattern consume-transform-produce illustra questo meccanismo: il consumer legge un messaggio, lo trasforma, produce il risultato in un topic di output e conferma l'offset del messaggio sorgente -- il tutto all'interno di un'unica transazione atomica.

python
# exactly_once_pipeline.py — Transactional consume-transform-produce
from confluent_kafka import Consumer, Producer
import json

def transactional_pipeline():
    consumer = Consumer({
        'bootstrap.servers': 'kafka-1:9092',
        'group.id': 'etl-transformer',
        'enable.auto.commit': False,
        'isolation.level': 'read_committed'
    })

    producer = Producer({
        'bootstrap.servers': 'kafka-1:9092',
        'transactional.id': 'etl-transformer-001',
        'acks': 'all',
        'enable.idempotence': True
    })

    producer.init_transactions()
    consumer.subscribe(['raw-events'])

    while True:
        msg = consumer.poll(1.0)
        if msg is None or msg.error():
            continue

        # Begin atomic transaction
        producer.begin_transaction()
        try:
            raw = json.loads(msg.value())
            enriched = transform(raw)

            # Write transformed record to output topic
            producer.produce(
                'enriched-events',
                key=msg.key(),
                value=json.dumps(enriched).encode('utf-8')
            )

            # Commit consumer offset within the same transaction
            producer.send_offsets_to_transaction(
                consumer.position(consumer.assignment()),
                consumer.consumer_group_metadata()
            )
            producer.commit_transaction()
        except Exception:
            producer.abort_transaction()

def transform(record: dict) -> dict:
    # Apply business logic transformations
    record['processed_at'] = '2026-04-20T00:00:00Z'
    record['pipeline_version'] = '2.1'
    return record

Il transactional.id identifica in modo univoco l'istanza del pipeline transazionale. In caso di riavvio, Kafka utilizza tale identificativo per annullare qualsiasi transazione incompiuta dell'istanza precedente, impedendo la produzione di duplicati. La chiamata a send_offsets_to_transaction lega il commit dell'offset consumer alla stessa transazione della produzione del messaggio arricchito: o entrambe le operazioni vengono confermate, o nessuna delle due lo è.

Quando utilizzare exactly-once

Le transazioni Kafka garantiscono exactly-once all'interno dell'ecosistema Kafka (consume-transform-produce). Per ottenere exactly-once end-to-end verso un sistema esterno (database, data warehouse), è necessario combinare le transazioni Kafka con scritture idempotenti sulla destinazione. In molti scenari, la semantica at-least-once con logica idempotente lato consumer risulta sufficiente e presenta un overhead inferiore.

Share Groups: il nuovo paradigma di consumo (KIP-932)

Kafka 4.0 introduce gli Share Groups, un'evoluzione significativa del modello di consumo. A differenza dei consumer group classici, dove ogni partizione viene assegnata esclusivamente a un singolo consumer, gli Share Groups consentono a più consumer di leggere dalla stessa partizione simultaneamente, con un meccanismo di acknowledgement per singolo messaggio.

Questo modello si ispira alle code di messaggi tradizionali (RabbitMQ, SQS), pur conservando i vantaggi dell'infrastruttura Kafka. Risulta particolarmente adatto ai workload in cui l'ordine di elaborazione non è critico ma la distribuzione uniforme del carico tra i consumer è essenziale.

| Feature | Consumer Groups | Share Groups | |---|---|---| | Partition assignment | Exclusive (one consumer per partition) | Shared (multiple consumers per partition) | | Ordering guarantee | Per-partition ordering preserved | No ordering guarantee | | Use case | Ordered event streams | Task distribution, work queues | | Acknowledgement | Offset-based (commit position) | Per-record (acknowledge individually) | | Max parallelism | Limited by partition count | Limited by consumer count |

Gli Share Groups non sostituiscono i consumer group tradizionali. I due modelli coesistono e rispondono a esigenze differenti. Per un pipeline ETL dove l'ordine degli eventi è critico, i consumer group classici rimangono la scelta appropriata. Per distribuire task di calcolo (generazione di report, invio di notifiche, validazioni indipendenti), gli Share Groups offrono una scalabilità superiore senza il vincolo del numero di partizioni.

Domande di colloquio Kafka per data engineer

I colloqui tecnici per posizioni di data engineering includono frequentemente domande approfondite su Kafka. Le seguenti coprono i temi più valutati nel 2026 e rappresentano un complemento alla preparazione sulle domande di colloquio Data Engineering.

Come garantisce Kafka l'ordine dei messaggi? L'ordine è garantito esclusivamente all'interno di una partizione. Tutti i messaggi che condividono la stessa chiave di partizionamento vengono indirizzati alla medesima partizione e quindi elaborati nell'ordine di produzione. Non esiste alcuna garanzia di ordine tra partizioni diverse. Per un ordine globale rigoroso, un topic con una sola partizione è necessario, ma ciò elimina ogni parallelismo.

Qual è la differenza tra at-least-once, at-most-once e exactly-once? At-most-once: il consumer conferma l'offset prima dell'elaborazione; in caso di guasto, il messaggio viene perso. At-least-once: il consumer conferma dopo l'elaborazione; in caso di guasto e riavvio, il messaggio viene rielaborato. Exactly-once: la combinazione di transazioni producer, idempotenza e isolamento read_committed garantisce che ogni messaggio venga elaborato esattamente una volta, anche in caso di failure.

Come funziona il rebalancing in un consumer group? Quando un consumer si unisce o abbandona un consumer group, Kafka avvia un rebalancing. Durante questa fase, tutte le partizioni vengono temporaneamente rilasciate e riassegnate. Il rebalancing provoca una pausa nell'elaborazione. Le strategie cooperative (Cooperative Sticky Assignor) minimizzano l'impatto riassegnando solo le partizioni interessate.

Cosa accade quando un broker va in failure? Se il broker ospita partizioni leader, Kafka elegge nuovi leader tra le repliche sincrone (ISR). I producer e i consumer vengono automaticamente reindirizzati verso i nuovi leader. Se min.insync.replicas non è più soddisfatto dopo il guasto, le produzioni con acks=all falliscono fino a quando un numero sufficiente di repliche torna sincronizzato.

Quando scegliere Kafka Streams piuttosto che Apache Flink? Kafka Streams è una libreria integrata nell'applicazione. Non richiede alcun cluster di elaborazione separato e risulta adeguata per trasformazioni di complessità media con volumi fino a qualche centinaio di migliaia di messaggi al secondo. Flink è un motore di elaborazione distribuita autonomo, adatto a trattamenti complessi (windowing avanzato, CEP, join multi-stream) e volumi estremi. La scelta dipende dalla complessità dell'elaborazione e dalla volontà di gestire un cluster aggiuntivo.

Come gestire l'evoluzione degli schema in Kafka? Lo Schema Registry (Confluent o Apicurio) archivia e versiona gli schema Avro, Protobuf o JSON Schema. I producer registrano lo schema prima della produzione, e i consumer verificano la compatibilità al momento del consumo. Le policy di compatibilità (BACKWARD, FORWARD, FULL) impediscono che modifiche distruttive allo schema compromettano i consumer esistenti. Questo concetto è strettamente collegato alla modellazione dati nel contesto del data engineering.

Kafka e il Data Lakehouse: convergenza delle architetture

L'emergere dei formati di tabella aperti (Apache Iceberg, Delta Lake, Apache Hudi) sta trasformando la relazione tra Kafka e lo storage analitico. Invece di scrivere i dati Kafka in un data warehouse proprietario, i pipeline moderni depositano gli eventi direttamente in un data lakehouse in formato Iceberg o Delta.

Questa convergenza offre molteplici vantaggi: lo storage in formato aperto previene il vendor lock-in, l'evoluzione dello schema viene gestita nativamente dal formato di tabella, e i motori di query (Spark, Trino, DuckDB) possono interrogare i dati direttamente senza copie intermedie. Kafka Connect propone connettori nativi per Iceberg e Delta Lake, semplificando l'implementazione di questo pattern.

Kafka + Iceberg nel 2026

Il connettore Kafka Connect per Apache Iceberg, ufficialmente in incubazione nel progetto Apache, è diventato lo standard per l'ingestione streaming verso il lakehouse. Supporta la deduplicazione at-rest, la compattazione automatica e i commit transazionali. Per i team che costruiscono nuove piattaforme data, la combinazione Kafka + Iceberg + un motore di query (Trino o Spark) rappresenta un'architettura di riferimento.

Conclusione

Apache Kafka si è affermato come l'infrastruttura di riferimento per lo streaming di dati nel data engineering. I punti essenziali da ricordare:

  • Il modo KRaft (Kafka 4.x) elimina ZooKeeper e semplifica considerevolmente il deployment e l'amministrazione dei cluster
  • La scelta della chiave di partizionamento determina l'ordine di elaborazione, la distribuzione del carico e la scalabilità -- una chiave mal scelta provoca hot partition che degradano le prestazioni
  • Il commit manuale degli offset con enable.auto.commit: False e l'isolamento read_committed costituiscono la base di un pipeline at-least-once affidabile
  • Il CDC tramite Debezium cattura le modifiche in tempo reale dal WAL della base dati sorgente, senza impatto sulle prestazioni di quest'ultima
  • Le transazioni Kafka (exactly-once) garantiscono l'atomicità del pattern consume-transform-produce, al costo di un overhead da calibrare tramite la dimensione dei batch
  • Gli Share Groups (KIP-932), introdotti in Kafka 4.0, aprono un nuovo modello di consumo adatto alle code di lavoro senza vincoli di ordine
  • Il monitoraggio del consumer lag, delle partizioni sotto-replicate e della latenza delle richieste è indispensabile per prevenire incidenti silenziosi in produzione
  • La convergenza Kafka + formati di tabella aperti (Iceberg, Delta Lake) delinea l'architettura di riferimento delle piattaforme data moderne

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Tag

#kafka
#data-engineering
#streaming
#partizioni
#cdc
#debezium
#colloquio

Condividi

Articoli correlati