Parliamone
// tecnologie.message-brokers

Message Brokers

Architetture, garanzie di consegna, partizionamento e consumer group a confronto: Kafka, RabbitMQ, Pulsar e NATS nel panorama dei sistemi di messaggistica distribuita.

Software ArchitectureData Engineering

Executive summary

Quando un sistema informativo distribuito deve far comunicare decine o centinaia di componenti in modo affidabile, trasferendo dati tra servizi, alimentando catene di elaborazione in tempo reale, coordinando flussi di lavoro asincroni, la scelta della piattaforma di messaggistica diventa una decisione che condiziona le prestazioni, l'affidabilità e la capacità di evoluzione dell'intero sistema. Questo articolo analizza le quattro piattaforme di messaggistica distribuita più adottate, confrontandone le architetture interne, i meccanismi con cui garantiscono che i messaggi arrivino a destinazione senza perdite né duplicazioni, le strategie di suddivisione del carico tra più consumatori e i compromessi operativi che ciascuna impone. L'analisi mostra che nessuna piattaforma è universalmente superiore: ciascuna incarna scelte progettuali profondamente diverse, dal registro immutabile che conserva la storia completa dei messaggi, alla coda tradizionale che elimina ogni messaggio dopo la consegna, fino a sistemi che separano il calcolo dalla memorizzazione, e la scelta ottimale dipende dalla corrispondenza tra queste proprietà e le esigenze specifiche del contesto di utilizzo.


Background

La messaggistica asincrona tra componenti distribuiti è un problema di ingegneria del software che precede l'era dei microservizi di almeno tre decenni. I primi message-oriented middleware (MOM) commerciali, IBM MQSeries (1993), TIBCO Rendezvous, implementavano il disaccoppiamento tra produttori e consumatori attraverso code persistenti e protocolli proprietari, in contesti prevalentemente enterprise e mainframe [1]. La standardizzazione del protocollo AMQP (Advanced Message Queuing Protocol), avviata nel 2003 da JPMorgan Chase e ratificata come standard OASIS e ISO/IEC 19464 nella versione 1.0, ha rappresentato il primo tentativo di definire un protocollo wire-level aperto per la messaggistica, separando la specifica del trasporto dall'implementazione del broker [2]. RabbitMQ, rilasciato nel 2007, ha adottato la versione 0-9-1 del protocollo, architetturalmente distinta dalla 1.0 e incompatibile a livello wire, consolidando un modello basato su exchange, binding e code che resta il riferimento per la messaggistica a coda tradizionale.

Il contributo concettuale che ha ridefinito il campo è la formalizzazione del log distribuito come primitiva di integrazione dati. Kreps, Narkhede e Rao [3], nel paper presentato al workshop NetDB nel 2011, hanno descritto Apache Kafka come un sistema di messaggistica per il processamento di log su larga scala, progettato internamente a LinkedIn per gestire i flussi di dati tra i sistemi operazionali e quelli analitici. L'intuizione fondamentale, trattare il flusso di messaggi come un log append-only partizionato e replicato, anziché come una coda da cui i messaggi vengono consumati e rimossi, ha inaugurato una categoria architetturale distinta dal message queuing tradizionale. Kleppmann [1] ha successivamente generalizzato questa visione, argomentando che ogni database è una materializzazione derivata di un log di eventi, e che il log distribuito è la primitiva unificante su cui costruire pipeline di integrazione dati.

Il panorama contemporaneo dei message broker riflette una tensione irrisolta tra modelli architetturali fondamentalmente diversi. Da un lato, il modello log-based (Kafka, Redpanda) mantiene i messaggi in un log ordinato e immutabile, supportando il replay e l'event sourcing. Dall'altro, il modello queue-based (RabbitMQ) implementa una semantica di consegna e rimozione ottimizzata per la distribuzione del lavoro. Architetture più recenti, Apache Pulsar [4] e NATS JetStream [5], hanno tentato sintesi diverse: Pulsar separa compute e storage per ottenere scalabilità indipendente, mentre NATS persegue la minimalità operativa con un sistema embeddabile e un protocollo testuale. Questa diversità non è un difetto del mercato, ma il riflesso di trade-off architetturali irriducibili: come stabilito dal teorema CAP [6], un sistema distribuito non può garantire simultaneamente consistenza forte, disponibilità e tolleranza alle partizioni di rete, e ogni broker incarna una scelta diversa lungo questi assi.


Criteri di confronto

L'analisi comparativa che segue è strutturata su cinque dimensioni architetturali che determinano l'idoneità di un message broker per un dato contesto applicativo. Ciascuna dimensione non è un parametro isolato, ma interagisce con le altre in modi che rendono insufficiente una valutazione per singolo criterio.

Architettura di storage e distribuzione. Il modello con cui il broker persiste e replica i messaggi determina le proprietà fondamentali del sistema: la durabilità dei dati, la capacità di replay, la scalabilità orizzontale e il comportamento in caso di failure di un nodo. La distinzione primaria è tra sistemi che mantengono un log distribuito immutabile e sistemi che implementano code con semantica di consegna e rimozione.

Garanzie di consegna. Le tre semantiche classiche, at-most-once, at-least-once, exactly-once, definiscono il contratto tra broker e applicazione rispetto alla perdita e alla duplicazione dei messaggi. La semantica exactly-once, a lungo considerata irrealizzabile nei sistemi distribuiti, è stata implementata con approcci diversi da ciascun broker, con trade-off significativi in termini di throughput e latenza.

Partizionamento e ordinamento. La strategia di suddivisione dei messaggi tra partizioni, shard o stream determina il parallelismo massimo raggiungibile e le garanzie di ordinamento disponibili. Alcuni broker garantiscono l'ordinamento solo all'interno di una partizione, altri offrono ordinamento per chiave con distribuzione round-robin, altri ancora delegano il partizionamento a livello applicativo.

Consumer group e bilanciamento del carico. Il meccanismo con cui i consumatori si coordinano per processare i messaggi in parallelo, e il comportamento del sistema quando un consumatore si aggiunge, si rimuove o diventa non disponibile, ha impatto diretto sulla latenza di processamento e sulla disponibilità del servizio durante le fasi di ribilanciamento.

Operabilità e maturità in produzione. Le caratteristiche operative, complessità del deployment, dipendenze esterne, strumenti di monitoraggio, curva di apprendimento, ecosistema di integrazione, determinano il costo totale di gestione del sistema nel tempo, un fattore spesso sottovalutato nelle valutazioni architetturali.


Analisi comparativa

Apache Kafka

Apache Kafka implementa il modello del log distribuito con partizionamento statico e replicazione sincrona. Ogni topic è suddiviso in un numero configurabile di partizioni, ciascuna delle quali è un segmento di log append-only ordinato e immutabile. I messaggi all'interno di una partizione sono identificati da un offset monotonicamente crescente, e l'ordinamento è garantito esclusivamente a livello di singola partizione [3]. La scelta del numero di partizioni è una decisione di design anticipata e difficilmente reversibile: aumentare le partizioni di un topic richiede un ribilanciamento dei dati, e ridurle non è supportato senza ricreare il topic.

Replicazione e ISR. Ogni partizione è replicata su un numero configurabile di broker (replication factor), con un leader che gestisce tutte le operazioni di lettura e scrittura e un insieme di follower che replicano il log. Il meccanismo di In-Sync Replicas (ISR) definisce il sottoinsieme di repliche che sono allineate al leader entro un intervallo configurabile: una replica esce dall'ISR se il suo ritardo supera la soglia replica.lag.time.max.ms, e il sistema garantisce la durabilità solo per i messaggi confermati da tutte le repliche ISR quando il produttore configura acks=all [7]. Questo design offre un compromesso esplicito tra durabilità e latenza: la configurazione acks=1 (solo il leader) riduce la latenza ma espone al rischio di perdita dati in caso di failure del leader prima della replicazione; la configurazione acks=all garantisce la durabilità al costo di una latenza proporzionale al tempo di replicazione verso il follower più lento nell'ISR.

Da ZooKeeper a KRaft. L'architettura originale di Kafka dipendeva da Apache ZooKeeper per la gestione dei metadati del cluster, l'elezione del controller e la registrazione dei broker. Il KIP-500 [8] ha introdotto KRaft, un protocollo di consenso basato su una variante event-based di Raft, che elimina la dipendenza da ZooKeeper. A partire da Kafka 4.0 (marzo 2025), il supporto per ZooKeeper è stato completamente rimosso [8]. KRaft implementa un quorum di controller dedicati che mantengono i metadati in un topic interno, riducendo il numero di componenti da operare e migliorando la scalabilità del piano di metadati. Il collo di bottiglia introdotto da ZooKeeper nella gestione dei metadati, che nella pratica industriale limitava il numero di partizioni gestibili per cluster [7, 8], è stato eliminato, consentendo cluster di dimensioni significativamente maggiori.

Exactly-once semantics. Kafka implementa l'exactly-once semantics (EOS) attraverso due meccanismi complementari [9]. Il producer idempotente assegna un Producer ID (PID) e un sequence number a ogni batch di messaggi per partizione: il broker rileva e scarta i duplicati confrontando i sequence number, garantendo l'idempotenza a livello di singola partizione. Le transazioni estendono questa garanzia a scritture atomiche su partizioni e topic multipli: un producer transazionale può scrivere su più partizioni e committare o abortire l'intera operazione atomicamente, consentendo pattern come il read-process-write in Kafka Streams con la configurazione processing.guarantee=exactly_once_v2 [9]. Il costo dell'EOS non è trascurabile: l'overhead del protocollo transazionale e il meccanismo di fencing dei producer zombie introducono latenza aggiuntiva, particolarmente visibile in scenari di failure e recovery.

Consumer group. In Kafka, un consumer group è un insieme di consumatori che condividono un group.id e si spartiscono le partizioni di un topic: ogni partizione è assegnata a esattamente un consumatore del gruppo, garantendo che ogni messaggio sia processato da un solo consumatore. Il numero massimo di consumatori attivi in un gruppo è uguale al numero di partizioni del topic. Il protocollo di rebalancing, il processo di riassegnazione delle partizioni quando un consumatore si aggiunge o si rimuove, è passato attraverso tre generazioni. Il protocollo eager originale causava uno stop-the-world in cui tutti i consumatori rilasciavano tutte le partizioni prima della riassegnazione. Il KIP-429 ha introdotto il protocollo cooperative incremental, implementato dal CooperativeStickyAssignor, che revoca solo le partizioni che devono essere spostate, consentendo ai consumatori non coinvolti di continuare il processamento durante il ribilanciamento [10]. A partire da Kafka 3.x, il protocollo cooperativo è il default raccomandato, riducendo i tempi di ribilanciamento fino a un ordine di grandezza rispetto al protocollo eager.

RabbitMQ

RabbitMQ implementa il modello a coda di messaggi con routing flessibile basato sul protocollo AMQP 0-9-1. L'architettura si fonda su tre primitive: exchange, queue e binding. I produttori pubblicano messaggi verso un exchange, mai direttamente verso una coda, e l'exchange instrada i messaggi verso una o più code sulla base delle regole di binding e della routing key associata al messaggio [2]. Questa separazione tra pubblicazione e instradamento è la caratteristica architetturale distintiva di RabbitMQ: consente pattern di distribuzione complessi (fanout, topic-based routing, header-based routing) senza che il produttore debba conoscere la topologia dei consumatori.

Tipologie di exchange. RabbitMQ definisce quattro tipologie di exchange [11]. Il direct exchange instrada i messaggi verso le code il cui binding key corrisponde esattamente alla routing key del messaggio, implementando una distribuzione point-to-point. Il fanout exchange replica il messaggio verso tutte le code associate, indipendentemente dalla routing key, implementando un broadcast. Il topic exchange supporta pattern matching con wildcard (* per una parola, # per zero o più parole) sulla routing key, consentendo un routing gerarchico. L'headers exchange instrada sulla base degli header del messaggio anziché della routing key, offrendo la massima flessibilità a costo di una configurazione più complessa. Questa tassonomia rende RabbitMQ particolarmente adatto a scenari in cui la logica di routing è complessa e variabile nel tempo, poiché nuove code possono essere associate a un exchange esistente senza modificare i produttori.

Quorum queue e Raft. Le quorum queue, introdotte in RabbitMQ 3.8 e divenute il tipo di coda replicata raccomandato con la rimozione delle classic mirrored queue in RabbitMQ 4.0 (2024), implementano la replicazione tramite il protocollo di consenso Raft [12]. Ogni quorum queue ha un leader e un insieme configurabile di follower. Quando il leader riceve un messaggio, registra l'operazione nel Write-Ahead Log (WAL), persiste il messaggio nel Raft log locale e invia comandi di replicazione ai follower in parallelo. Un messaggio è confermato al produttore solo quando la maggioranza dei nodi (quorum) ha persistito l'entry, garantendo durabilità anche in caso di failure della minoranza. A differenza delle classic mirrored queue, le quorum queue supportano il delivery limit, un contatore che traccia il numero di tentativi di consegna falliti, con reindirizzamento automatico dei messaggi "velenosi" (poison message) verso una dead-letter queue dopo il superamento della soglia configurata [12].

Stream queue. RabbitMQ 3.9 ha introdotto le stream queue, un'implementazione di log append-only all'interno dell'ecosistema RabbitMQ che avvicina le capability del broker a quelle di Kafka. Le stream queue utilizzano un'architettura a due livelli: un cluster Raft per la gestione dei metadati e il lifecycle dello stream, e il motore Osiris per lo storage append-only con replicazione [12]. Le stream queue supportano il replay dei messaggi tramite offset, il consumo da una posizione arbitraria e la retention time-based o size-based. Tuttavia, le stream queue non supportano il routing tramite exchange nella stessa modalità delle code tradizionali, e il loro modello di consumo è più vicino a quello di Kafka che a quello AMQP tradizionale.

Garanzie di consegna. RabbitMQ implementa nativamente la semantica at-least-once attraverso il meccanismo di publisher confirm (il broker conferma la ricezione del messaggio) e consumer acknowledgment (il consumatore conferma il processamento). La semantica at-most-once si ottiene disabilitando le conferme. La semantica exactly-once non è implementata a livello di broker: RabbitMQ non fornisce un meccanismo nativo di deduplicazione o transazioni cross-queue, delegando la garanzia di idempotenza al livello applicativo [11]. Questa scelta progettuale riflette una filosofia diversa da quella di Kafka: RabbitMQ privilegia la semplicità del broker e la flessibilità del routing, affidando la complessità della semantica exactly-once all'applicazione.

Consumer e bilanciamento. In RabbitMQ, i consumatori si registrano su una coda specifica e il broker distribuisce i messaggi in modalità round-robin tra i consumatori registrati. Il prefetch count (basic.qos) controlla il numero di messaggi non confermati che il broker invia a ciascun consumatore, fungendo da meccanismo di flow control e di bilanciamento del carico implicito: un consumatore lento accumula messaggi non confermati e riceve meno nuovi messaggi. Questo modello è più semplice del consumer group di Kafka, non richiede un protocollo di rebalancing, ma non offre garanzie di ordinamento: messaggi consecutivi nella stessa coda possono essere processati in parallelo da consumatori diversi, con completamento in ordine arbitrario.

Apache Pulsar

Apache Pulsar è stato sviluppato internamente a Yahoo nel 2012 per gestire la messaggistica geo-replicata tra i data center dell'azienda, ed è stato donato alla Apache Software Foundation nel 2016 [4]. L'architettura di Pulsar si distingue per la separazione strutturale tra il livello di servizio (broker) e il livello di storage (Apache BookKeeper), una scelta che consente il scaling indipendente dei due livelli e rende i broker completamente stateless.

Architettura a due livelli. I broker Pulsar gestiscono le connessioni dei client, il dispatching dei messaggi e il coordinamento dei topic, ma non persistono dati: ogni messaggio ricevuto viene scritto in un ledger su Apache BookKeeper, un sistema di storage distribuito basato su Write-Ahead Log progettato originariamente a Yahoo per il journaling di HDFS NameNode [13]. BookKeeper organizza i dati in ledger, segmenti di log append-only con un singolo writer, replicati su un sottoinsieme configurabile di bookie (i nodi di storage). Ogni ledger è distribuito su un ensemble di bookie con un write quorum $Q_w$ e un ack quorum $Q_a$: una scrittura è confermata quando $Q_a$ bookie hanno persistito l'entry, e il sistema tollera fino a $Q_a - 1$ failure simultanei senza perdita dati [13]. Questa architettura segment-based differisce fondamentalmente dal modello partition-based di Kafka: mentre in Kafka una partizione è un'entità indivisibile assegnata a un singolo broker, in Pulsar un topic è composto da segmenti (ledger) che possono essere distribuiti su bookie diversi, consentendo un ribilanciamento dello storage senza spostare intere partizioni.

Modello di sottoscrizione. Pulsar offre quattro modalità di sottoscrizione che coprono sia la semantica pub/sub sia quella di coda [4]. La sottoscrizione exclusive assegna il topic a un singolo consumatore, equivalente a una coda FIFO non condivisa. La sottoscrizione failover designa un consumatore attivo e uno o più standby, con failover automatico. La sottoscrizione shared distribuisce i messaggi round-robin tra i consumatori, senza garanzia di ordinamento, analoga al modello di RabbitMQ. La sottoscrizione key_shared distribuisce i messaggi sulla base di una chiave, garantendo che tutti i messaggi con la stessa chiave siano consegnati allo stesso consumatore, combinando parallelismo e ordinamento per chiave. Questa flessibilità a livello di sottoscrizione, configurabile per topic e per gruppo di consumatori, è una delle differenze architetturali più significative rispetto a Kafka, dove il modello consumer group è l'unica modalità di consumo parallelo.

Multi-tenancy e geo-replicazione. Pulsar implementa il multi-tenancy come primitiva di primo livello dell'architettura [4]. I topic seguono la convenzione persistent://tenant/namespace/topic, dove tenant e namespace definiscono i confini di isolamento per autenticazione, autorizzazione, quote di storage e politiche di retention. La geo-replicazione è configurabile a livello di namespace: i broker mantengono replicatori che consumano i messaggi pubblicati nel cluster locale e li ripubblicano nei cluster remoti, con deduplicazione basata su message ID per evitare loop. Questa architettura consente topologie di replicazione flessibili (active-active, active-standby, mesh) senza intervento applicativo [4].

Tiered storage. L'architettura segment-based di Pulsar abilita nativamente il tiered storage: i segmenti più vecchi possono essere migrati da BookKeeper a object storage (S3, GCS, Azure Blob) mantenendo la possibilità di lettura trasparente per il consumatore [4]. Questo meccanismo consente retention potenzialmente illimitata a costi di storage ridotti, una proprietà particolarmente rilevante per scenari di event sourcing e audit log.

Garanzie di consegna. Pulsar supporta la semantica at-least-once di default e implementa la deduplicazione dei messaggi lato broker per avvicinarsi alla semantica exactly-once nella produzione. Il producer assegna un sequence number a ogni messaggio, e il broker mantiene una finestra di sequence number per rilevare e scartare i duplicati in caso di retry. Per il processamento end-to-end exactly-once, Pulsar supporta le transazioni a partire dalla versione 2.8, consentendo scritture atomiche su topic multipli e commit/abort coordinati con il consumo dei messaggi sorgente [4].

NATS e JetStream

NATS è un sistema di messaggistica progettato per la semplicità operativa e la bassa latenza, con un protocollo testuale minimalista e un'architettura che privilegia la leggerezza rispetto alla ricchezza funzionale. Il core NATS, senza persistenza, implementa un modello pub/sub fire-and-forget con semantica at-most-once: i messaggi sono consegnati ai sottoscrittori connessi al momento della pubblicazione, senza buffering né retry [5]. JetStream, introdotto come layer di persistenza integrato nel server NATS, aggiunge le garanzie necessarie per scenari che richiedono durabilità e replay.

Architettura di JetStream. JetStream è integrato nel binario del server NATS ma abilitato su base per-server, consentendo un deployment ibrido in cui alcuni nodi gestiscono solo messaggistica effimera e altri forniscono persistenza [5]. Gli stream sono la primitiva di storage: ogni stream cattura i messaggi pubblicati su uno o più subject NATS e li persiste su disco o in memoria con replicazione Raft configurabile ($R = 1, 3, 5$). La replicazione è implementata tramite il protocollo Raft, lo stesso utilizzato dalle quorum queue di RabbitMQ, con un leader che gestisce le scritture e i follower che replicano il log. A differenza di Kafka, dove le partizioni sono la primitiva di parallelismo, in NATS il parallelismo di consumo è gestito dai consumer group (implementati come consumer con lo stesso nome) che si spartiscono i messaggi dello stream tramite pull-based fetching.

Delivery semantics. JetStream supporta tre livelli di garanzia [5]. La semantica at-least-once è il default: i messaggi non confermati vengono ri-consegnati dopo un timeout configurabile. La policy di acknowledgment è configurabile per consumer: AckNone (nessun ack, at-most-once), AckAll (l'ack di un messaggio conferma implicitamente tutti i precedenti) e AckExplicit (ogni messaggio richiede un ack individuale). La semantica exactly-once è implementata attraverso la combinazione di deduplicazione lato produttore (basata su un header Nats-Msg-Id che il broker utilizza per scartare duplicati entro una finestra temporale configurabile) e double acknowledgment lato consumatore (AckSync), dove il consumer richiede che il server confermi di aver ricevuto l'ack, eliminando l'ambiguità dello stato in caso di failure durante la fase di acknowledgment [5].

Key-Value store e Object store. JetStream estende le primitive di messaggistica con un Key-Value store distribuito e un Object store, entrambi costruiti sopra gli stream [5]. Il KV store implementa un'interfaccia get/put/delete con history e watch (notifiche di cambiamento), utilizzando internamente un subject per chiave e lo stream come log delle mutazioni. L'Object store consente lo storage di blob di dimensioni arbitrarie, segmentati in chunk e distribuiti su stream. Queste astrazioni posizionano NATS come un sistema convergente che integra messaggistica, storage chiave-valore e object storage in un singolo runtime, riducendo le dipendenze esterne a costo di una minore specializzazione in ciascun dominio.

Operabilità. NATS si distingue per la semplicità operativa: un singolo binario senza dipendenze esterne, configurazione minimale, protocollo testuale ispezionabile con strumenti standard (telnet, netcat). Il cluster si forma tramite un meccanismo di gossip e non richiede ZooKeeper, etcd o BookKeeper. Il footprint di memoria è significativamente inferiore a quello di Kafka o Pulsar, rendendo NATS adatto a scenari edge e IoT dove le risorse computazionali sono limitate [5].


Discussione e raccomandazioni

Confronto strutturale delle architetture

Le quattro piattaforme analizzate incarnano tre modelli architetturali distinti. Kafka implementa un log distribuito monolitico in cui broker e storage sono co-locati: ogni broker è responsabile sia del servizio ai client sia della persistenza dei dati sulle partizioni assegnate. Pulsar separa i due livelli, rendendo i broker stateless e delegando la persistenza a BookKeeper, al costo di una complessità operativa aggiuntiva (un cluster BookKeeper più un cluster di broker più, fino a Pulsar 3.x, un cluster ZooKeeper). RabbitMQ implementa un modello a coda con routing programmabile, dove la persistenza è gestita dal broker stesso con replicazione Raft nelle quorum queue. NATS JetStream integra la persistenza nel server di messaggistica con un approccio minimalista, sacrificando alcune funzionalità avanzate, come le transazioni cross-stream, in favore della semplicità operativa.

La tabella seguente sintetizza le differenze strutturali lungo le cinque dimensioni di confronto:

Dimensione Kafka RabbitMQ Pulsar NATS JetStream
Modello di storage Log partizionato co-locato Code con routing (Raft) Segment-based su BookKeeper Stream su Raft integrato
Delivery semantics At-least-once, exactly-once (EOS) At-most-once, at-least-once At-least-once, exactly-once (txn) At-most-once (core), at-least-once, exactly-once (dedup + double ack)
Ordinamento Per partizione Nessuna garanzia con consumatori paralleli Per partizione; per chiave con key_shared Per stream; per chiave con consumer filtering
Consumer group Partition assignment con rebalancing cooperativo Round-robin su coda con prefetch Quattro modalità di sottoscrizione Consumer con pull-based fetching
Dipendenze esterne Nessuna (KRaft, da Kafka 4.0) Nessuna BookKeeper, (ZooKeeper rimosso in Pulsar 4.0) Nessuna

Trade-off nelle garanzie di consegna

La semantica exactly-once è il punto su cui le differenze architetturali si manifestano con maggiore chiarezza. Kafka offre l'implementazione più matura, basata su producer idempotenti e transazioni con fencing dei producer zombie, ma il costo in termini di latenza e complessità operativa è significativo: il protocollo transazionale introduce overhead su ogni batch di scrittura e richiede un coordinator dedicato [9]. Pulsar raggiunge un risultato funzionalmente equivalente attraverso deduplicazione lato broker e transazioni, ma con un'architettura di storage diversa che distribuisce il costo della durabilità su BookKeeper anziché sul broker [4]. NATS adotta un approccio più leggero, basato su deduplicazione a finestra temporale e double ack, che copre la maggior parte degli scenari pratici ma non offre la stessa garanzia formale delle transazioni Kafka per scritture multi-stream atomiche [5]. RabbitMQ delega esplicitamente l'idempotenza al livello applicativo, una scelta coerente con la sua filosofia di broker semplice con routing ricco [11].

Come argomentato da Kleppmann [1], la distinzione tra at-least-once con deduplicazione applicativa ed exactly-once nativa è in parte semantica: in entrambi i casi, l'effetto osservabile è che ogni messaggio viene processato esattamente una volta, ma il livello al quale la garanzia è implementata, broker o applicazione, determina la complessità del codice applicativo e la portabilità tra piattaforme diverse.

Partizionamento e scalabilità

Il partizionamento è il meccanismo fondamentale attraverso cui i message broker raggiungono la scalabilità orizzontale, ma le implementazioni divergono in modi che hanno impatto diretto sulla flessibilità operativa. In Kafka, il numero di partizioni di un topic è una decisione anticipata che vincola il parallelismo massimo e non è riducibile senza ricreare il topic: un topic con 12 partizioni può avere al massimo 12 consumatori attivi nel gruppo, e l'aumento delle partizioni richiede un ribilanciamento dei dati [3, 7]. Il KIP-429 e il protocollo cooperativo hanno mitigato l'impatto del ribilanciamento sul servizio, ma non hanno eliminato il vincolo strutturale [10].

Pulsar offre maggiore elasticità grazie all'architettura segment-based: poiché i segmenti di un topic sono distribuiti su bookie indipendenti, l'aggiunta di capacità di storage non richiede lo spostamento di partizioni intere, e il broker può ribilanciare i topic tra i nodi senza downtime [4, 13]. Tuttavia, il numero di partizioni di un topic Pulsar determina comunque il parallelismo massimo nelle sottoscrizioni exclusive e failover, analogamente a Kafka.

NATS JetStream non utilizza il concetto di partizione: il parallelismo è gestito a livello di consumer, con il server che distribuisce i messaggi dello stream tra i consumer di un gruppo. Questo modello elimina il vincolo di dimensionamento anticipato delle partizioni, ma sposta la responsabilità del bilanciamento del carico sul protocollo di distribuzione del server, che può risultare meno prevedibile sotto carichi asimmetrici [5].

Considerazioni operative

La complessità operativa è una dimensione spesso sottovalutata nelle valutazioni architetturali, ma determinante nel costo totale di ownership di un sistema di messaggistica. La transizione di Kafka a KRaft ha eliminato la dipendenza da ZooKeeper, storicamente la fonte principale di complessità operativa, semplificando significativamente il deployment e riducendo il numero di componenti da monitorare [8]. Tuttavia, Kafka rimane un sistema che richiede competenze specifiche per il tuning delle partizioni, la gestione dell'ISR e l'ottimizzazione dei parametri di produttore e consumatore.

Pulsar presenta la complessità operativa più elevata tra le piattaforme analizzate: un deployment in produzione richiede un cluster di broker, un cluster BookKeeper e, fino a Pulsar 3.x, un cluster ZooKeeper, per un totale di tre (ora due, con l'adozione di un metadata store basato su etcd o la propria implementazione) componenti distribuiti da gestire [4, 13]. La roadmap di Pulsar include la rimozione della dipendenza da BookKeeper a favore di un layer di storage nativo, ma al momento della stesura di questo articolo il componente resta necessario per i deployment in produzione.

RabbitMQ offre un deployment relativamente semplice con un singolo componente, ma la gestione delle quorum queue richiede attenzione al dimensionamento del cluster (numero dispari di nodi per il quorum Raft) e al monitoraggio del lag di replicazione [12]. NATS rappresenta l'estremo della semplicità operativa: un singolo binario, nessuna dipendenza esterna, configurazione minimale, con il trade-off di un ecosistema di strumenti e integrazioni meno maturo rispetto a Kafka.

Scenari di adozione

L'allineamento tra le proprietà architetturali del broker e i requisiti del contesto applicativo guida la scelta. I log distribuiti (Kafka) sono la scelta naturale per scenari di event streaming ad alto throughput in cui il replay, l'event sourcing e la retention a lungo termine sono requisiti, pipeline di data integration, change data capture, architetture CQRS [1, 3]. Le code con routing flessibile (RabbitMQ) sono ottimali per scenari di task distribution, request-reply e workflow orchestration in cui la logica di instradamento è complessa e il throughput richiesto è nell'ordine delle decine di migliaia di messaggi al secondo [11]. L'architettura disaggregata di Pulsar è vantaggiosa in contesti multi-tenant con requisiti di geo-replicazione e retention differenziata per namespace, tipici di piattaforme SaaS e organizzazioni con deployment multi-regione [4]. NATS JetStream è adatto a scenari edge, IoT e microservizi leggeri in cui la semplicità operativa e la bassa latenza prevalgono sulla ricchezza funzionale del broker [5].

Queste raccomandazioni non sono mutuamente esclusive: in architetture complesse, è comune trovare Kafka come backbone di event streaming e RabbitMQ come broker per la distribuzione di task all'interno dei singoli servizi, o NATS come layer di comunicazione a bassa latenza tra microservizi con Pulsar come sistema di persistenza a lungo termine.


Riferimenti

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

[2] AMQP Working Group, "AMQP 0-9-1 Protocol Specification," 2008. https://www.amqp.org/specification/0-9-1/amqp-org-download

[3] J. Kreps, N. Narkhede, J. Rao, "Kafka: A Distributed Messaging System for Log Processing," in Proc. NetDB Workshop, 2011. https://notes.stephenholiday.com/Kafka.pdf

[4] Apache Software Foundation, "Apache Pulsar Documentation — Architecture Overview," 2025. https://pulsar.apache.org/docs/4.0.x/concepts-architecture-overview/

[5] Synadia / NATS.io, "NATS Documentation — JetStream," 2025. https://docs.nats.io/nats-concepts/jetstream

[6] S. Gilbert, N. Lynch, "Brewer's Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services," ACM SIGACT News, vol. 33, no. 2, 2002. https://dl.acm.org/doi/10.1145/564585.564601

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

[8] Apache Software Foundation, "KIP-500: Replace ZooKeeper with a Self-Managed Metadata Quorum," 2019. https://cwiki.apache.org/confluence/display/KAFKA/KIP-500:+Replace+ZooKeeper+with+a+Self-Managed+Metadata+Quorum

[9] Confluent, "Exactly-Once Semantics Are Possible: Here's How Apache Kafka Does It," 2017. https://www.confluent.io/blog/exactly-once-semantics-are-possible-heres-how-apache-kafka-does-it/

[10] Apache Software Foundation, "KIP-429: Kafka Consumer Incremental Rebalance Protocol," 2019. https://cwiki.apache.org/confluence/x/vAclBg

[11] RabbitMQ, "AMQP 0-9-1 Model Explained," 2025. https://www.rabbitmq.com/tutorials/amqp-concepts

[12] RabbitMQ, "Quorum Queues," 2025. https://www.rabbitmq.com/docs/quorum-queues

[13] Apache Software Foundation, "Apache BookKeeper — Concepts and Architecture," 2024. https://bookkeeper.apache.org/docs/4.5.1/getting-started/concepts/

Message Brokers

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

Tweaks

Light mode
Atmospheric (glass)
Client logos
Terminal hero