Comprendere i broker di messaggi. Esplorando la meccanica dello scambio di messaggi tramite ActiveMQ e Kafka. Capitolo 3. Kafka

Continuazione della traduzione di un piccolo libro:
«Understanding Message Brokers»,
autore: Jakub Korab, editore: O’Reilly Media, Inc., data di pubblicazione: Giugno 2017, ISBN: 9781492049296.

Parte precedentemente tradotta: Comprendere i broker di messaggi. Esplorando la meccanica dello scambio di messaggi tramite ActiveMQ e Kafka. Capitolo 1. Introduzione

CAPITOLO 3

Kafka

Kafka è stata sviluppata in LinkedIn per superare alcune limitazioni dei tradizionali broker di messaggi e per evitare la necessità di configurare più broker di messaggi per diverse interazioni "point-to-point", come descritto in questo libro nella sezione "Scalabilità verticale e orizzontale" a pagina 28. Gli scenari d'uso in LinkedIn si basavano principalmente su un'assorbimento unidirezionale di enormi volumi di dati, come i clic sulle pagine e i log di accesso, consentendo allo stesso tempo a più sistemi di utilizzare questi dati senza impattare sulle prestazioni dei produttori o degli altri consumatori. Infatti, la ragione d'essere di Kafka è quella di fornire un'architettura di messaggistica come descritta nel Universal Data Pipeline.

Tenendo presente questo obiettivo finale, sono naturalmente emerse altre esigenze. Kafka deve:

  • Essere estremamente veloce
  • Fornire un'alta capacità di gestione dei messaggi
  • Supportare modelli "Publisher-Subscriber" e "Point-to-Point"
  • Non rallentare nell'aggiungere consumatori. Ad esempio, le prestazioni e le code e i topic in ActiveMQ peggiorano all'aumentare del numero di consumatori sul destinatario.
  • Essere orizzontalmente scalabile; se un broker che persiste i messaggi può farlo solo alla massima velocità del disco, allora vale la pena oltrepassare un singolo istanza di broker per aumentare le prestazioni.
  • Separare l'accesso allo storage e il recupero dei messaggi.

Per raggiungere tutto ciò, Kafka ha adottato un'architettura che ha ridefinito i ruoli e le responsabilità dei clienti e dei broker di messaggistica. Il modello JMS è molto orientato sul broker, dove quest'ultimo si occupa della distribuzione dei messaggi e i clienti devono preoccuparsi solo di inviare e ricevere messaggi. Kafka, al contrario, è cliente-centrico, con il cliente che assume molte delle funzioni tradizionali del broker, come la distribuzione equa dei messaggi pertinenti tra i consumatori, ricevendo in cambio un broker estremamente veloce e scalabile. Per coloro che hanno lavorato con sistemi di messaggistica tradizionali, l'utilizzo di Kafka richiede cambiamenti fondamentali nel modo di pensare.
Questa direzione ingegneristica ha portato alla creazione di un'infrastruttura di messaggistica capace di aumentare di molti ordini di grandezza la capacità rispetto a un broker convenzionale. Come vedremo, questo approccio comporta dei compromessi, il che significa che Kafka non è adatto per determinati tipi di carichi e software consolidato.

Modello unificato del destinatario

Per soddisfare i requisiti descritti sopra, Kafka ha combinato la messaggistica di tipo 'pubblica-sottoscrizione' e 'point-to-point' all'interno di un'unica forma di destinatario — topic. Questo può confondere le persone che hanno lavorato con sistemi di messaggistica dove la parola 'topic' si riferisce a un meccanismo di broadcasting, da cui (dal topic) la lettura non è affidabile. I topic di Kafka dovrebbero essere considerati un tipo ibrido di destinatario, secondo la definizione fornita nell'introduzione di questo libro.

Nella restante parte di questo capitolo, se non diversamente specificato, il termine 'topic' si riferirà al topic di Kafka.

Per comprendere appieno come si comportano i topic e quali garanzie offrono, dobbiamo prima esaminare come sono implementati in Kafka.
Ogni topic in Kafka ha il proprio log.
I produttori che inviano messaggi a Kafka scrivono in questo registro, mentre i consumatori leggono dal registro tramite puntatori che si spostano continuamente in avanti. Periodicamente, Kafka elimina le parti più vecchie del registro, indipendentemente dal fatto che i messaggi in quelle parti siano stati letti o meno. Una parte centrale del design di Kafka è che il broker non si preoccupa di sapere se i messaggi sono stati letti; questa responsabilità ricade sul cliente.

I termini "registro" e "puntatore" non compaiono nella documentazione di Kafka. Questi termini ben noti vengono utilizzati qui per facilitare la comprensione.

Questo modello è completamente diverso da ActiveMQ, dove i messaggi di tutte le code vengono memorizzati in un unico registro, e il broker contrassegna i messaggi come eliminati dopo che sono stati letti.
Ora approfondiamo un po' e consideriamo il registro del topic più nel dettaglio.
Il registro di Kafka è composto da più partizioni (Figura 3-1). Kafka garantisce un rigoroso ordinamento in ogni partizione. Ciò significa che i messaggi scritti in una partizione in un determinato ordine verranno letti nello stesso ordine. Ogni partizione è implementata come un file di registro circolare (rolling) che contiene un sottoinsieme di tutti i messaggi inviati al topic dai suoi produttori. Il topic creato contiene per impostazione predefinita una partizione. L'idea delle partizioni è un concetto centrale di Kafka per la scalabilità orizzontale.

Comprendere i broker di messaggi. Esplorando la meccanica dello scambio di messaggi tramite ActiveMQ e Kafka. Capitolo 3. Kafka
Figura 3-1. Partizioni Kafka

Quando un produttore invia un messaggio a un topic Kafka, decide in quale partizione inviare il messaggio. Esamineremo questo argomento più nel dettaglio in seguito.

Lettura dei messaggi

Il client che desidera leggere i messaggi gestisce un puntatore nominato, chiamato gruppo di consumatori (consumer group), che punta a un offset del messaggio nella partizione. L'offset è una posizione con un numero crescente, che inizia da 0 all'inizio della partizione. Questo gruppo di consumatori, a cui si riferisce l'API tramite un identificatore group_id definito dall'utente, corrisponde a un singolo consumatore logico o sistema.

La maggior parte dei sistemi di messaggistica legge i dati dal destinatario attraverso più istanze e flussi per l'elaborazione parallela dei messaggi. Di conseguenza, ci saranno generalmente molte istanze di consumatori che condividono lo stesso gruppo di consumatori.

Il problema della lettura può essere rappresentato nei seguenti termini:

  • Un topic ha diverse partizioni
  • Molti gruppi di consumatori possono utilizzare il topic contemporaneamente
  • Un gruppo di consumatori può avere diverse istanze distinte

Si tratta di un problema non banale di "molti a molti". Per capire come Kafka gestisce le relazioni tra gruppi di consumatori, istanze di consumatori e partizioni, consideriamo una serie di scenari di lettura che diventano progressivamente più complessi.

Consumatori e gruppi di consumatori

Prendiamo come punto di partenza un topic con una partizione (Figura 3-2).

Comprendere i broker di messaggi. Esplorando la meccanica dello scambio di messaggi tramite ActiveMQ e Kafka. Capitolo 3. Kafka
Figura 3-2. Un consumatore legge da una partizione

Quando un'istanza del consumatore si connette a questo argomento con il proprio group_id, viene assegnata una partizione per la lettura e uno spostamento in questa partizione. La posizione di questo spostamento viene configurata nel client come un puntatore alla posizione più recente (il messaggio più recente) o alla posizione più antica (il messaggio più vecchio). Il consumatore richiede (polls) i messaggi dall'argomento, il che porta alla loro lettura sequenziale dal registro.
La posizione dello spostamento viene regolarmente committata nuovamente in Kafka e salvata come messaggi nell'argomento interno _consumer_offsets. I messaggi letti non vengono comunque eliminati, a differenza di un broker normale, e il client può riavvolgere (rewind) lo spostamento per elaborare nuovamente i messaggi già visualizzati.

Quando si collega un secondo consumatore logico, utilizzando un altro group_id, gestisce un secondo puntatore, che non dipende dal primo (Figura 3-3). In questo modo, il topic Kafka funge da coda, in cui esiste un consumatore e, come un normale topic publish-subscribe (pub-sub), a cui sono iscritti più consumatori, con l'ulteriore vantaggio che tutti i messaggi vengono conservati e possono essere elaborati più volte.

Comprendere i broker di messaggi. Esplorando la meccanica dello scambio di messaggi tramite ActiveMQ e Kafka. Capitolo 3. Kafka
Figura 3-3. Due consumatori in gruppi di consumatori diversi leggono da una partizione

Consumatori nel gruppo di consumatori

Quando un'istanza del consumatore legge dati da una partizione, controlla completamente l'offset e elabora i messaggi come descritto nel precedente paragrafo.
Se più istanze di consumatori si sono collegate con lo stesso group_id a un topic con una partizione, all'istanza che si è collegata per ultima verrà assegnato il controllo dell'offset e da quel momento riceverà tutti i messaggi (Figura 3-4).

Comprendere i broker di messaggi. Esplorando la meccanica dello scambio di messaggi tramite ActiveMQ e Kafka. Capitolo 3. Kafka
Figura 3-4. Due consumatori nella stessa gruppo di consumatori leggono da una partizione

Questa modalità di elaborazione, in cui il numero di istanze dei consumatori supera il numero di partizioni, può essere considerata una forma di consumatore monopolistico. Questo può essere utile se hai bisogno di una clusterizzazione "attiva-passiva" (o "calda-tiepida") delle tue istanze dei consumatori, anche se l'esecuzione parallela di più consumatori ("attiva-attiva" o "calda-calda") è molto più comune rispetto ai consumatori in attesa.

Questo comportamento di distribuzione dei messaggi, descritto sopra, può sorprendere rispetto al comportamento di una normale coda JMS. In questo modello, i messaggi inviati alla coda saranno distribuiti uniformemente tra due consumatori.

Più spesso, quando creiamo più istanze di consumatori, lo facciamo o per l'elaborazione parallela dei messaggi, o per aumentare la velocità di lettura, o per migliorare la resilienza del processo di lettura. Poiché solo un'istanza del consumatore può leggere dati da una partizione contemporaneamente, come si raggiunge questo in Kafka?

Un modo per farlo è utilizzare un singolo consumer per leggere tutti i messaggi e passarli a un pool di thread. Sebbene questo approccio aumenti la capacità di elaborazione, complica la logica dei consumer e non migliora la resilienza del sistema di lettura. Se un'istanza del consumer si disconnette a causa di un guasto elettrico o di un evento simile, la lettura si interrompe.

Il modo canonico per affrontare questo problema in Kafka è utilizzare unmaggiore numero di partizioni.Partizionamento

Le partizioni sono il meccanismo principale per il parallelismo nella lettura e per scalare un topic oltre la capacità di un singolo broker. Per comprendere meglio questo, consideriamo una situazione in cui esiste un topic con due partizioni e un consumer si iscrive a questo topic (

Figure 3-5Figure 3-5. Un consumer legge da più partizioni).

Comprendere i broker di messaggi. Esplorando la meccanica dello scambio di messaggi tramite ActiveMQ e Kafka. Capitolo 3. Kafka
In questo scenario, al consumer viene data la possibilità di controllare i puntatori corrispondenti al suo group_id in entrambe le partizioni e inizia a leggere messaggi da entrambe le partizioni.

In questo scenario, al consumatore viene dato il controllo sui puntatori corrispondenti al suo group_id in entrambe le partizioni e inizia a leggere i messaggi da entrambe le partizioni.
Quando viene aggiunto un consumatore aggiuntivo a questo topic per lo stesso group_id, Kafka riassegna (reallocate) una delle partizioni dal primo al secondo consumatore. Da quel momento, ogni istanza del consumatore leggerà da una partizione del topic.Figura 3-6).

Per garantire l'elaborazione dei messaggi in parallelo su 20 thread, è necessario avere almeno 20 partizioni. Se ci sono meno partizioni, ci saranno consumatori senza lavoro, come descritto in precedenza nella discussione sui consumatori monopolisti.

Comprendere i broker di messaggi. Esplorando la meccanica dello scambio di messaggi tramite ActiveMQ e Kafka. Capitolo 3. Kafka
Figura 3-6. Due consumatori nella stessa gruppo di consumatori leggono da partizioni diverse

Questo schema riduce significativamente la complessità del lavoro del broker Kafka rispetto alla distribuzione dei messaggi necessaria per supportare le code JMS. Non è necessario preoccuparsi dei seguenti aspetti:

  • Quale consumatore dovrebbe ricevere il prossimo messaggio, basandosi sulla distribuzione ciclica (round-robin), la capacità attuale dei buffer di prelettura o sui messaggi precedenti (come per i gruppi di messaggi JMS).
  • Quali messaggi sono stati inviati a quali consumatori e se devono essere consegnati nuovamente in caso di errore.

L'unica cosa che deve fare il broker Kafka è inviare consecutivamente i messaggi al consumatore quando quest'ultimo li richiede.

Tuttavia, i requisiti per la parallelizzazione della lettura e la ritrasmissione dei messaggi falliti non svaniscono: la responsabilità per essi passa semplicemente dal broker al client. Ciò significa che devono essere considerati nel vostro codice.

Invio di messaggi

La responsabilità di decidere a quale partizione inviare il messaggio ricade sul produttore di quel messaggio. Per capire il meccanismo tramite il quale ciò avviene, dobbiamo prima considerare cosa stiamo realmente inviando.

Mentre in JMS utilizziamo una struttura di messaggio con metadati (intestazioni e proprietà) e un corpo contenente il payload, in Kafka il messaggio è una coppia 'chiave-valore'. Il payload del messaggio viene inviato come valore. La chiave, d'altra parte, è utilizzata principalmente per il partizionamento e deve contenere una chiave specifica per la logica di business, per collocare i messaggi correlati nella stessa partizione.

Nel Capitolo 2 abbiamo discusso il scenario delle scommesse online, in cui eventi correlati devono essere elaborati in sequenza da un singolo consumatore:

  1. L'account utente è impostato.
  2. I fondi sono accreditati sul conto.
  3. È fatta una scommessa, che preleva denaro dal conto.

Se ogni evento è un messaggio inviato a un argomento, in questo caso la chiave naturale sarà l'ID dell'account.
Quando un messaggio viene inviato utilizzando l'API Kafka Producer, esso viene passato alla funzione di partizionamento, che, considerando il messaggio e lo stato attuale del cluster Kafka, restituisce l'ID della partizione a cui il messaggio deve essere inviato. Questa funzione è implementata in Java attraverso l'interfaccia Partitioner.

Questa interfaccia appare come segue:

interface Partitioner {
    int partition(String topic,
        Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
}

L'implementazione del Partitioner per determinare la partizione utilizza per impostazione predefinita un algoritmo di hashing della chiave (general-purpose hashing algorithm over the key) o round-robin, se la chiave non è specificata. Questo valore predefinito funziona bene nella maggior parte dei casi. Tuttavia, in futuro potresti voler scrivere una tua strategia.

Scrivere una strategia di partizionamento personalizzata

Consideriamo un esempio in cui desideri inviare metadati insieme al payload di un messaggio. Il payload nel nostro esempio è una istruzione per effettuare un deposito su un conto di gioco. L'istruzione è qualcosa che vorremmo assicurare di non modificare durante la trasmissione e vogliamo essere certi che solo un sistema superiore fidato possa iniziare questa istruzione. In questo caso, i sistemi mittente e destinatario concordano sull'uso della firma per verificare l'autenticità del messaggio.
In una normale JMS definiamo semplicemente la proprietà "firma del messaggio" e la aggiungiamo al messaggio. Tuttavia, Kafka non ci fornisce un meccanismo per trasmettere metadati — solo una chiave e un valore.

Poiché il valore rappresenta il payload del bonifico bancario, di cui vogliamo preservare l'integrità, non abbiamo altra scelta se non definire una struttura dati da utilizzare nella chiave. Supponendo che sia necessario un identificatore dell'account per il partizionamento, poiché tutti i messaggi relativi all'account devono essere elaborati in ordine, elaboreremo la seguente struttura JSON:

{
  "signature": "541661622185851c248b41bf0cea7ad0",
  "accountId": "10007865234"
}

Poiché il valore della firma varierà a seconda del payload, la strategia di hashing predefinita dell'interfaccia Partitioner non raggrupperà in modo affidabile i messaggi correlati. Pertanto, dovremo scrivere una nostra strategia, che analizzi questa chiave e partizioni il valore accountId.

Kafka include checksum per rilevare la corruzione dei messaggi nello storage e ha un set completo di funzionalità di sicurezza. Anche in questo caso, a volte emergono requisiti specifici del settore, come quello riportato sopra.

La strategia di partizionamento utente deve garantire che tutti i messaggi correlati finiscano nella stessa partizione. Sebbene sembri semplice, questo requisito può essere complicato a causa dell'importanza di ordinare i messaggi correlati e del numero fisso di partizioni nel topic.

Il numero di partizioni in un topic può cambiare nel tempo, poiché possono essere aggiunte se il traffico supera le aspettative iniziali. Pertanto, le chiavi dei messaggi possono essere associate alla partizione in cui sono state inizialmente inviate, implicando parte dello stato che deve essere distribuito tra le istanze del produttore.

Un altro fattore da considerare è l'uniformità della distribuzione dei messaggi tra le partizioni. In generale, le chiavi non vengono distribuite equamente tra i messaggi, e le funzioni hash non garantiscono una distribuzione equa dei messaggi per un piccolo insieme di chiavi.
È importante notare che, qualunque sia il modo in cui si decide di suddividere i messaggi, il delimitatore stesso potrebbe dover essere riutilizzato.

Consideriamo la necessità di replicare i dati tra i cluster Kafka in diverse ubicazioni geografiche. A tal fine, Kafka fornisce uno strumento da riga di comando chiamato MirrorMaker, che viene utilizzato per leggere i messaggi da un cluster e inviarli a un altro.

MirrorMaker deve comprendere le chiavi del topic replicato per mantenere l'ordine relativo tra i messaggi durante la replica tra i cluster, poiché il numero di partizioni per questo topic potrebbe non corrispondere nei due cluster.

Le strategie di partizionamento personalizzate si incontrano relativamente raramente, poiché il partizionamento predefinito tramite hashing o round-robin funziona con successo nella maggior parte degli scenari. Tuttavia, se avete bisogno di garanzie rigorose di ordinamento o se è necessario estrarre metadati dai carichi utili, allora il partizionamento è qualcosa a cui dovreste prestare maggiore attenzione.

I vantaggi della scalabilità e delle prestazioni di Kafka sono dovuti al trasferimento di alcune responsabilità tradizionalmente gestite dal broker al cliente. In questo caso, si prende una decisione su come distribuire messaggi potenzialmente correlati tra più consumatori che operano in parallelo.

Anche i broker JMS devono affrontare tali requisiti. È interessante notare che il meccanismo di invio di messaggi correlati allo stesso consumatore, realizzato tramite le JMS Message Groups (una variante della strategia di bilanciamento del carico sticky load balancing (SLB)), richiede anche che il mittente contrassegni i messaggi come correlati. Nel caso di JMS, il broker è responsabile dell'invio di questo gruppo di messaggi correlati a un consumatore tra molti e del trasferimento della proprietà del gruppo se il consumatore si disconnette.

Accordi sul produttore

Il partizionamento non è l'unico aspetto da considerare quando si inviano messaggi. Esaminiamo i metodi send() della classe Producer nell'API Java:

Future  send(ProducerRecord  record);
Future  send(ProducerRecord  record, Callback callback);

È fondamentale notare che entrambi i metodi restituiscono un Future, il che indica che l'operazione di invio non viene eseguita immediatamente. Di conseguenza, il messaggio (ProducerRecord) viene scritto nel buffer di invio per ogni partizione attiva e inviato al broker in background attraverso la libreria client di Kafka. Sebbene questo renda il processo incredibilmente veloce, significa che un'applicazione scritta in modo inesperto può perdere messaggi se il suo processo viene interrotto.

Come sempre, esiste un modo per rendere l'operazione di invio più affidabile a scapito delle prestazioni. La dimensione di questo buffer può essere impostata su 0, costringendo il thread del'applicazione di invio ad attendere finché l'invio del messaggio al broker non è completato, come segue:

RecordMetadata metadata = producer.send(record).get();

Ancora una volta, sulla lettura dei messaggi

La lettura dei messaggi presenta ulteriori complessità che richiedono considerazione. A differenza dell'API JMS, che può avviare un ascoltatore di messaggi in risposta all'arrivo di un messaggio, l'interfaccia Consumer Kafka effettua solo polling. Esaminiamo più da vicino il metodo poll (), utilizzato a tale scopo:

ConsumerRecords  poll(long timeout);

Il valore restituito dal metodo è una struttura contenitore che contiene più oggetti ConsumerRecord da potenziali più partizioni. ConsumerRecord è esso stesso un oggetto holder per una coppia chiave-valore con i relativi metadati, come la partizione da cui è stato ottenuto.

Come discusso nel Capitolo 2, dobbiamo sempre tenere a mente cosa succede ai messaggi dopo il loro trattamento riuscito o non riuscito, ad esempio se il consumatore non riesce a elaborare un messaggio o se si interrompe. In JMS questo veniva gestito tramite la modalità di riconoscimento (acknowledgement mode). Il broker rimuoverà il messaggio elaborato con successo o lo riaffiderà se non elaborato o fallito (a condizione che siano state utilizzate delle transazioni).
Kafka funziona in modo completamente diverso. I messaggi non vengono rimossi dal broker dopo la lettura e la responsabilità di ciò che accade in caso di errore ricade sul codice di lettura stesso.

Come abbiamo già detto, un gruppo di consumatori è associato a un offset nel registro. La posizione nel registro associata a questo offset corrisponde al prossimo messaggio che sarà emesso in risposta a poll (). Il momento in cui questo offset aumenta è cruciale per la lettura.

Tornando al modello di lettura discusso in precedenza, il trattamento del messaggio consiste in tre fasi:

  1. Estrai il messaggio da leggere.
  2. Elabora il messaggio.
  3. Conferma il messaggio.

Il consumer Kafka viene fornito con l'opzione di configurazione enable.auto.commit. Questa è una delle impostazioni comunemente utilizzate di default, come accade di solito con le impostazioni che contengono la parola 'auto'.

Fino a Kafka 0.10, il client che utilizzava questo parametro inviava l'offset dell'ultimo messaggio letto al successivo chiamato poll () dopo l'elaborazione. Ciò significava che eventuali messaggi già estratti (fetched) potevano essere elaborati nuovamente, se il client li aveva già elaborati, ma era stato inaspettatamente distrutto prima della chiamata. poll (). Poiché il broker non conserva alcuno stato riguardo a quante volte è stato letto un messaggio, il successivo consumatore che estrae questo messaggio non saprà che è successo qualcosa di sbagliato. Questo comportamento era pseudo-trasazionale. L'offset veniva confermato solo se l'elaborazione del messaggio era stata completata con successo, ma se il client si interrompeva, il broker inviava nuovamente lo stesso messaggio a un altro client. Questo comportamento corrispondeva alla garanzia di consegna dei messaggi «almeno una volta«.

In Kafka 0.10, il codice del client è stato modificato in modo che la conferma venisse eseguita periodicamente dalla libreria del client, secondo l'impostazione auto.commit.interval.ms. Questo comportamento si colloca tra le modalità JMS AUTO_ACKNOWLEDGE e DUPS_OK_ACKNOWLEDGE. Quando si utilizza l'auto-committazione, i messaggi possono essere confermati indipendentemente dal fatto che siano stati effettivamente elaborati — ciò potrebbe accadere in caso di consumatore lento. Se il consumatore si interrompe, i messaggi vengono recuperati dal consumatore successivo, a partire dalla posizione di commit, il che potrebbe portare a saltare un messaggio. In questo caso, Kafka non perde i messaggi; semplicemente il codice lettore non li elabora.

Questa modalità ha le stesse prospettive di quella della versione 0.9: i messaggi possono essere elaborati, ma in caso di errore, l'offset potrebbe non essere stato committato, il che può potenzialmente portare a duplicati nella consegna. Più messaggi estraete durante l'esecuzione poll (), maggiore sarà questo problema.

Come discusso nella sezione 'Lettura dei messaggi dalla coda' a pagina 21, nel sistema di messaggistica non esiste un concetto di consegna unica dei messaggi, considerando le modalità di errore.

In Kafka, ci sono due modi per confermare (committare) l'offset: automaticamente e manualmente. In entrambi i casi, i messaggi possono essere elaborati più volte, nel caso in cui un messaggio sia stato elaborato, ma si sia verificato un errore prima della conferma. È anche possibile non elaborare affatto un messaggio se la conferma è avvenuta in background e il vostro codice è stato completato prima che iniziasse l'elaborazione (probabilmente nelle versioni Kafka 0.9 e precedenti).

Per gestire manualmente il processo di conferma dell'offset, è possibile utilizzare l'API del consumatore Kafka, impostando il parametro enable.auto.commit a false e richiamando esplicitamente uno dei seguenti metodi:

void commitSync();
void commitAsync();

Se desiderate elaborare un messaggio "almeno una volta", dovete committare manualmente l'offset con commitSync (), eseguendo questo comando immediatamente dopo l'elaborazione dei messaggi.

Questi metodi non consentono di confermare (acknowledged) i messaggi fino a quando non sono stati elaborati, ma non fanno nulla per eliminare il potenziale duplicato di elaborazione, creando nel contempo un'apparenza di transazionalità. In Kafka non esistono transazioni. Il client non ha la possibilità di fare quanto segue:

  • Ripristina automaticamente un messaggio fallito. I consumatori devono gestire le eccezioni che si verificano a causa di payload problematici e interruzioni del backend, poiché non possono fare affidamento sulla riconsegna dei messaggi da parte del broker.
  • Invia messaggi a più argomenti all'interno di un'unica operazione atomica. Come vedremo a breve, il controllo su diversi argomenti e partizioni può trovarsi su macchine diverse nel cluster Kafka, che non coordinano le transazioni durante l'invio. Al momento della scrittura di questo articolo, è stato fatto un certo lavoro per rendere questo possibile tramite KIP-98.
  • Collegare la lettura di un messaggio da un argomento all'invio di un altro messaggio in un altro argomento. Ancora una volta, l'architettura di Kafka dipende da molte macchine indipendenti che operano come un unico bus e non si fanno tentativi per nascondere questa realtà. Ad esempio, non esistono componenti API che consentano di colmare Consumatore e Produttore in una transazione. In JMS questo è garantito da un oggetto Sessione, da cui vengono creati MessageProducers e MessageConsumers.

Se non possiamo fare affidamento sulle transazioni, come possiamo garantire una semantica più vicina a quella fornita dai tradizionali sistemi di messaggistica?

Se esiste la possibilità che lo spostamento del consumer possa aumentare prima che il messaggio venga elaborato, ad esempio, durante un guasto del consumer, il consumer non ha modo di sapere se il suo gruppo di consumer ha perso messaggi quando gli viene assegnata la partizione. Pertanto, una delle strategie consiste nel riportare indietro (rewind) lo spostamento alla posizione precedente. L'API del consumer Kafka fornisce i seguenti metodi per questo:

void seek(TopicPartition partition, long offset);
void seekToBeginning(Collection  partitions);

Metodo seek () può essere utilizzato con il metodo
offsetsForTimes (Map timestampsToSearch) per tornare a uno stato in un momento specifico del passato.

L'uso di questo approccio implica che, con alta probabilità, alcuni messaggi già elaborati saranno letti e trattati di nuovo. Per evitare questo, possiamo utilizzare la lettura idempotente, come descritto nel Capitolo 4, per tenere traccia dei messaggi già visualizzati ed escludere i duplicati.

In alternativa, il codice del tuo consumatore può essere semplice se è consentita la perdita o la duplicazione dei messaggi. Quando consideriamo gli scenari in cui Kafka è comunemente utilizzato, come l'elaborazione di eventi di log, metriche, tracciamento dei clic, ecc., comprendiamo che la perdita di messaggi singoli avrà probabilmente un impatto minimo sulle applicazioni circostanti. In tali casi, i valori predefiniti sono perfettamente accettabili. D'altra parte, se la tua applicazione deve gestire pagamenti, devi prestare attenzione a ogni singolo messaggio. Tutto dipende dal contesto.

Osservazioni personali mostrano che con l'aumento dell'intensità dei messaggi, il valore di ogni singolo messaggio diminuisce. I messaggi di grande volume diventano generalmente preziosi se considerati in forma aggregata.

Alta disponibilità

L'approccio di Kafka all'alta disponibilità è notevolmente diverso da quello di ActiveMQ. Kafka è progettata su cluster orizzontalmente scalabili, in cui tutte le istanze del broker ricevono e distribuiscono i messaggi simultaneamente.

Un cluster Kafka è composto da più istanze di broker che lavorano su server diversi. Kafka è stata progettata per funzionare su hardware autonomo standard, dove ogni nodo ha il proprio storage dedicato. L'uso di storage di rete (SAN) non è raccomandato, poiché più nodi di calcolo possono competere per gli intervalli di tempo dello storage e creare conflitti.Iè

Kafka è sempre attivo sistema. Molti grandi utenti di Kafka non spengono mai i loro cluster, e il software effettua sempre aggiornamenti tramite un riavvio sequenziale. Ciò è possibile grazie alla garanzia di compatibilità con la versione precedente per i messaggi e le interazioni tra broker.

I broker sono connessi al cluster di server ZooKeeper, che funge da registro delle configurazioni e viene utilizzato per coordinare i ruoli di ciascun broker. ZooKeeper è a sua volta un sistema distribuito che garantisce alta disponibilità tramite la replicazione delle informazioni stabilendo un quorum.

In un caso base, un topic viene creato nel cluster Kafka con le seguenti proprietà:

  • Numero di partizioni. Come discusso in precedenza, il valore esatto utilizzato qui dipende dal livello desiderato di lettura parallela.
  • Il fattore di replica determina quante istanze del broker nel cluster devono contenere i log per questa partizione.

Utilizzando ZooKeepers per la coordinazione, Kafka cerca di distribuire equamente le nuove partizioni tra i broker nel cluster. Questo avviene attraverso un'istanza che funge da Controller.

nel runtime per ogni partizione del topic Controller assegna ai broker i ruoli di leader (leader, master, principale) e seguaci (followers, schiavi, subordinati). Il broker che agisce come leader per una data partizione è responsabile della ricezione di tutti i messaggi inviati dai produttori e della distribuzione dei messaggi ai consumatori. Quando si inviano messaggi a una partizione del topic, questi vengono replicati su tutti i nodi del broker che fungono da seguaci per quella partizione. Ogni nodo che contiene i log per la partizione è chiamato replica. Un broker può fungere da leader per alcune partizioni e da seguace per altre.

Un seguace che contiene tutti i messaggi conservati dal leader è chiamato replica sincronizzata (replica in stato sincronizzato, in-sync replica). Se il broker che funge da leader per una partizione si disconnette, qualsiasi broker che è in uno stato aggiornato o sincronizzato per quella partizione può assumere il ruolo di leader. Questo design è incredibilmente resiliente.

Una parte della configurazione del produttore è il parametro acks, che definisce quante repliche devono confermare (acknowledge) la ricezione di un messaggio prima che il flusso dell'applicazione continui a inviare: 0, 1 o tutte. Se è impostato il valore all, quando viene ricevuto il messaggio, il leader invierà una conferma (confirmation) al produttore non appena riceve le conferme (acknowledgements) della registrazione da diverse repliche (inclusa se stessa), definite dalla configurazione del topic min.insync.replicas (di default 1). Se il messaggio non può essere replicato con successo, il produttore lancerà un'eccezione per l'applicazione (NotEnoughReplicas o NotEnoughReplicasAfterAppend).

Nella configurazione tipica, viene creato un topic con un fattore di replica 3 (1 leader, 2 follower per ogni partizione) e il parametro min.insync.replicas è impostato su 2. In questo caso, il cluster permette che uno dei broker che gestiscono la partizione del topic possa disattivarsi senza influire sulle applicazioni client.

Questo ci riporta al compromesso già noto tra prestazioni e affidabilità. La replicazione richiede tempo aggiuntivo per l'attesa delle conferme (acknowledgments) dai follower. Tuttavia, poiché viene eseguita in parallelo, la replicazione, almeno su tre nodi, ha prestazioni pari a quella su due (ignorando l'aumento dell'utilizzo della larghezza di banda di rete).

Utilizzando questo schema di replicazione, Kafka evita abilmente la necessità di garantire la scrittura fisica di ogni messaggio su disco tramite l'operazione sync (). Ogni messaggio inviato dal produttore verrà registrato nel log della partizione, ma, come discusso nel Capitolo 2, la registrazione su disco viene inizialmente eseguita nel buffer del sistema operativo. Se questo messaggio è replicato su un'altra istanza di Kafka e risiede nella sua memoria, la perdita del leader non implica che il messaggio stesso sia andato perso: può essere preso da una replica sincronizzata.
Rinuncia alla necessità di eseguire l'operazione sync () significa che Kafka può accettare messaggi alla velocità con cui può registrarli in memoria. E viceversa, più a lungo si può evitare il flush della memoria su disco, meglio è. Per questo motivo, non è raro che ai broker Kafka venga assegnata una memoria di 64 GB o più. Tale utilizzo di memoria consente a un'istanza di Kafka di operare facilmente a velocità migliaia di volte superiori rispetto a un tradizionale broker di messaggi.

Kafka può anche essere configurato per applicare l'operazione sync () ai pacchetti di messaggi. Poiché tutto in Kafka è orientato al lavoro con pacchetti, questo funziona effettivamente piuttosto bene per molti scenari di utilizzo ed è uno strumento utile per gli utenti che richiedono garanzie molto forti. Gran parte delle prestazioni pure di Kafka è legata ai messaggi inviati al broker in forma di pacchetti e al fatto che questi messaggi vengano letti dal broker in blocchi sequenziali con zero-copy operazioni (operazioni in cui non si svolge il compito di copiare dati da un'area di memoria a un'altra). Quest'ultimo rappresenta un grande guadagno in termini di prestazioni e risorse ed è possibile solo grazie all'uso della struttura dati di registro sottostante che definisce lo schema di partizione.

Nel cluster Kafka, è possibile ottenere prestazioni molto più elevate rispetto all'uso di un singolo broker Kafka, poiché le partizioni del topic possono essere scalate orizzontalmente su più macchine separate.

Risultati

In questo capitolo abbiamo esaminato come l'architettura di Kafka ripensa le relazioni tra i client e i broker, per garantire una pipeline di messaggistica incredibilmente resiliente, con una capacità superiore di molte volte rispetto a un broker di messaggi tradizionale. Abbiamo discusso le funzionalità che utilizza per raggiungere questo obiettivo e abbiamo brevemente esaminato l'architettura delle applicazioni che forniscono queste funzionalità. Nel capitolo successivo affronteremo le sfide comuni che devono affrontare le applicazioni basate sulla messaggistica e discuteremo le strategie per risolverle. Concluderemo il capitolo delineando come pensare alle tecnologie di messaggistica in generale, in modo da poter valutare la loro idoneità per i vostri casi d'uso.

Parte precedentemente tradotta: Comprendere i broker di messaggi. Studio della meccanica dello scambio di messaggi tramite ActiveMQ e Kafka. Capitolo 1.

Tradotto da: tele.gg/middle_java

Continua...

Solo gli utenti registrati possono partecipare al sondaggio. Accedi, per favore.

Kafka è utilizzato nella vostra organizzazione?

  • No

  • In passato sì, ora no

  • Pianifichiamo di usarlo

Hanno votato 38 utenti. 8 utenti si sono astenuti.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster