Ciao, Habr!
Ricordiamo che, a seguito del libro su abbiamo pubblicato un'opera altrettanto interessante sulla libreria .

Mentre la comunità sta ancora esplorando i confini delle possibilità di questo potente strumento. Recentemente è stato pubblicato un articolo, di cui vogliamo presentarvi la traduzione. L'autore, basandosi sulla propria esperienza, racconta come trasformare Kafka Streams in uno storage distribuito. Buona lettura!
La libreria Apache è utilizzata in tutto il mondo nelle aziende per l'elaborazione distribuita in tempo reale sopra Apache Kafka. Uno degli aspetti sottovalutati di questo framework è che consente di mantenere uno stato locale, prodotto sulla base dell'elaborazione dei flussi.
In questo articolo vi racconterò come la nostra azienda è riuscita a sfruttare questa opportunità nella progettazione di un prodotto per la sicurezza delle applicazioni cloud. Con Kafka Streams abbiamo creato microservizi con stato condiviso, ciascuno dei quali funge da fonte di informazioni affidabili e ad alta disponibilità sullo stato degli oggetti nel sistema. Per noi è un passo avanti sia in termini di affidabilità che di facilità di manutenzione.
Se siete interessati a un approccio alternativo che consenta di utilizzare un'unica centrale di dati per gestire lo stato formale dei vostri oggetti, leggete, sarà interessante…
Perché riteniamo che sia giunto il momento di cambiare il nostro approccio nella gestione dello stato condiviso
Dovevamo mantenere lo stato di vari oggetti, basandoci sui rapporti degli agenti (ad esempio: il sito ha subito un attacco?). Prima di passare a Kafka Streams, spesso ci affidavamo a un'unica centrale di dati (+ API di servizio) per gestire lo stato. Questo approccio ha i suoi svantaggi: in il mantenimento della coerenza e della sincronizzazione diventa una vera sfida. Il database può diventare un collo di bottiglia, oppure trovandosi in e soffrire di imprevedibilità.

Illustrazione 1: uno scenario tipico di separazione dello stato, riscontrato prima del passaggio a
Kafka e Kafka Streams: gli agenti riportano le loro visioni attraverso l'API, lo stato aggiornato viene calcolato tramite una centrale di dati
Incontrate Kafka Streams - ora è facile creare microservizi con stato condiviso
Circa un anno fa abbiamo deciso di rivedere approfonditamente i nostri scenari di lavoro con stati condivisi, per affrontare tali problematiche. Abbiamo subito deciso di provare Kafka Streams – noto per la sua scalabilità, alta disponibilità e robustezza, e per la ricca funzionalità di streaming che offre (trasformazioni, incluso il mantenimento dello stato). Proprio ciò di cui avevamo bisogno, senza contare quanto sia matura e affidabile sia diventata nel tempo la rete di messaggistica in Kafka.
Ognuno dei microservizi con mantenimento dello stato che abbiamo creato è stato costruito su un'istanza di Kafka Streams con una topologia piuttosto semplice. Essa consisteva in 1) una sorgente 2) un processore con un archivio permanente di chiavi e valori 3) un flusso:

Illustrazione 2: la topologia predefinita dei nostri istanze di flusso per i microservizi con mantenimento dello stato. Si prega di notare: qui c'è anche un archivio che contiene i metadati riguardanti la pianificazione.
Con questo nuovo approccio, gli agenti creano i messaggi inviati nel topic sorgente, mentre i consumatori – ad esempio, un servizio di notifica via email – ricevono lo stato condiviso calcolato attraverso il flusso (topic di uscita).

Illustrazione 3: un nuovo esempio del flusso di lavoro per uno scenario con microservizi condivisi: 1) l'agente genera un messaggio che arriva nel topic sorgente di Kafka; 2) il microservizio con stato condiviso (che utilizza Kafka Streams) lo elabora e scrive lo stato calcolato nel topic di destinazione di Kafka; quindi 3) i consumatori ricevono il nuovo stato.
Ehi, questo archivio integrato di chiavi e valori è davvero molto utile!
Come menzionato in precedenza, la nostra topologia con stato condiviso contiene un archivio di chiavi e valori. Abbiamo trovato diversi usi per esso, e due di questi sono descritti di seguito.
Opzione #1: utilizzo dell'archivio di chiavi e valori durante i calcoli.
Il nostro primo archivio di chiavi e valori conteneva dati ausiliari necessari per i nostri calcoli. Ad esempio, in alcuni casi, lo stato condiviso veniva determinato secondo il principio della "maggioranza dei voti". Nell'archivio si potevano mantenere tutti gli ultimi rapporti degli agenti sullo stato di un certo oggetto. Poi, ricevendo un nuovo rapporto da uno degli agenti, potevamo salvarlo, estrarre dall'archivio i rapporti di tutti gli altri agenti sullo stesso oggetto e ripetere il calcolo.
Di seguito, nell'illustrazione 4, si mostra come abbiamo aperto l'accesso all'archivio di chiavi e valori al metodo di elaborazione del processore, in modo da poter gestire un nuovo messaggio.

Illustrazione 4: apertura dell'accesso all'archivio di chiavi e valori per il metodo di elaborazione del processore (dopo questo, in ogni scenario che lavora con stato condiviso, è necessario implementare il metodo doProcess)
Opzione #2: creazione di un'API CRUD sopra Kafka Streams
Dopo aver configurato il nostro flusso di lavoro di base, abbiamo iniziato a sviluppare un'API RESTful CRUD per i nostri microservizi con stato condiviso. Volevamo poter estrarre lo stato di alcuni o tutti gli oggetti, così come impostare o rimuovere lo stato di un oggetto (utile per il supporto della parte server).
Per supportare tutte le API Get State, ogni volta che avevamo bisogno di ricalcolare lo stato durante l'elaborazione, lo memorizzavamo a lungo nello storage incorporato chiave-valore. In tal caso, è sufficiente implementare un'API usando un'unica istanza di Kafka Streams, come mostrato nel listing sottostante:

Illustrazione 5: utilizzo dello storage incorporato chiave-valore per ottenere lo stato pre-calcolato di un oggetto
Aggiornare lo stato di un oggetto tramite API non è difficile da realizzare. In sostanza, è sufficiente creare un produttore Kafka e utilizzarlo per registrare un nuovo stato. In questo modo si garantisce che tutti i messaggi generati tramite API vengano elaborati esattamente come quelli provenienti da altri produttori (ad esempio, agenti).

Illustrazione 6: è possibile definire lo stato dell'oggetto utilizzando il produttore Kafka
Una piccola complicazione: Kafka ha molte partizioni
In seguito volevamo distribuire il carico associato all'elaborazione e migliorare la disponibilità, fornendo a ciascun scenario un cluster di microservizi con stato condiviso. La configurazione è stata estremamente semplice: una volta configurati tutti gli istanze per lavorare con lo stesso ID dell'applicazione (e con gli stessi server di avvio), praticamente il resto è avvenuto automaticamente. Abbiamo anche specificato che ogni topic sorgente sarebbe composto da più partizioni, in modo che a ciascuna istanza potesse essere assegnato un sottoinsieme di queste partizioni.
Voglio anche menzionare che è comune qui eseguire il backup dello stato di archiviazione, in modo da poter trasferire questa copia su un altro istanza in caso di ripristino. Per ogni stato di archiviazione in Kafka Streams, viene creato un topic replicabile con un registro delle modifiche (che tiene traccia degli aggiornamenti locali). In questo modo, Kafka garantisce continuamente la sicurezza dello stato di archiviazione. Pertanto, in caso di guasto di un'istanza di Kafka Streams, lo stato di archiviazione può essere rapidamente ripristinato su un'altra istanza, dove andranno le relative partizioni. I nostri test hanno dimostrato che questo avviene in pochi secondi, anche se ci sono milioni di record nello stato di archiviazione.
Passando da un microservizio con stato condiviso a un cluster di microservizi, non è così banale implementare il Get State API. In questa nuova situazione, nel repository di stato di ogni microservizio è contenuta solo una parte del quadro generale (quegli oggetti i cui chiavi erano mappati su una specifica partizione). Era necessario determinare su quale istanza fosse contenuto lo stato dell'oggetto richiesto, e lo facevamo basandoci sui metadati dei flussi, come mostrato di seguito:

Illustrazione 7: utilizzando i metadati dei flussi, identifichiamo da quale istanza richiedere lo stato dell'oggetto desiderato; un approccio simile è stato utilizzato con il GET ALL API
Conclusioni principali
I repository di stato in Kafka Streams possono di fatto fungere da database distribuito,
- costantemente replicato in Kafka
- Sopra un tale sistema è facile costruire un CRUD API
- La gestione di più partizioni diventa un po' più complessa
- È anche possibile aggiungere uno o più repository di stato nella topologia del flusso per memorizzare dati ausiliari. Questa opzione può essere utilizzata per:
- Memorizzare a lungo termine i dati necessari per i calcoli durante l'elaborazione dei flussi
- Archiviazione a lungo termine dei dati, che possono essere utili durante la successiva inizializzazione di un'istanza di streaming
- molto altro…
Grazie a queste e ad altre qualità, Kafka Streams è particolarmente adatta per supportare uno stato globale in un sistema distribuito come il nostro. Kafka Streams si è dimostrata molto affidabile in produzione (da quando è stata implementata, praticamente non abbiamo perso messaggi) e siamo certi che le sue capacità non si limitano a questo!
Fonte: habr.com
