Kafka e microservizi: panoramica

Kafka e microservizi: panoramica

Ciao a tutti. In questo articolo vi racconterò perché noi di Avito abbiamo scelto Kafka nove mesi fa e cosa rappresenta. Condividerò uno dei casi d'uso: il broker di messaggi. Infine, parleremo dei vantaggi che abbiamo ottenuto adottando l'approccio Kafka as a Service.

Problema

Kafka e microservizi: panoramica

Iniziamo con un po' di contesto. Qualche tempo fa abbiamo iniziato a allontanarci dall'architettura monolitica e ora in Avito ci sono già diverse centinaia di servizi diversi. Ciascuno ha il proprio storage, il proprio stack tecnologico e si occupa della propria parte di logica di business.

Uno dei problemi legati a un numero elevato di servizi è la comunicazione. Il servizio A spesso desidera conoscere informazioni che possiede il servizio B. In questo caso, il servizio A contatta il servizio B tramite un API sincrono. Il servizio C desidera sapere cosa accade ai servizi G e D, mentre questi ultimi sono a loro volta interessati ai servizi A e B. Quando ci sono molti servizi "curiosi", le connessioni tra di essi si trasformano in un groviglio complicato.

In qualsiasi momento, il servizio A può diventare non disponibile. E cosa devono fare in questo caso il servizio B e tutti gli altri servizi dipendenti da esso? E se, per completare un'operazione di business, è necessario effettuare una catena di chiamate sincrone in sequenza, la probabilità di fallimento dell'intera operazione aumenta (e aumenta tanto più lunga è questa catena).

Scelta della tecnologia

Kafka e microservizi: panoramica

Va bene, i problemi sono chiari. Possiamo eliminarli creando un sistema centralizzato di scambio di messaggi tra i servizi. Ora ciascuno dei servizi deve sapere solo di questo sistema di scambio di messaggi. Inoltre, il sistema stesso deve essere resistente ai guasti e scalabile orizzontalmente, e in caso di guasti deve accumulare un buffer di richieste per un successivo trattamento.

Ora scegliamo la tecnologia con cui implementeremo la consegna dei messaggi. Per farlo, iniziamo a capire cosa ci aspettiamo da essa:

  • i messaggi tra i servizi non devono perdersi;
  • i messaggi possono essere duplicati;
  • i messaggi possono essere memorizzati e letti anche con un ritardo di alcuni giorni (buffer persistente);
  • i servizi possono iscriversi ai dati di loro interesse;
  • più servizi possono leggere gli stessi dati;
  • i messaggi possono contenere payload dettagliato e voluminoso (event-carried state transfer);
  • a volte è necessaria la garanzia dell'ordine dei messaggi.

È stato inoltre fondamentale per noi scegliere un sistema il più scalabile e affidabile possibile, con un'alta capacità di elaborazione (non meno di 100k messaggi di qualche kilobyte al secondo).

In questa fase, abbiamo detto addio a RabbitMQ (difficile da mantenere stabile con alti rps), PGQ di SkyTools (non abbastanza veloce e scarsamente scalabile) e NSQ (non persistente). Tutte queste tecnologie sono utilizzate nella nostra azienda, ma non si adattavano al compito che dovevamo affrontare.

Successivamente abbiamo iniziato a esplorare nuove tecnologie per noi: Apache Kafka, Apache Pulsar e NATS Streaming.

Innanzitutto, abbiamo scartato Pulsar. Abbiamo ritenuto che Kafka e Pulsar fossero soluzioni piuttosto simili. E nonostante Pulsar sia stato testato da grandi aziende, sia più recente e offra una latenza più bassa (in teoria), abbiamo deciso di mantenere Kafka tra queste due come standard de facto per tali compiti. È probabile che in futuro torneremo a considerare Apache Pulsar.

E così sono rimasti due candidati: NATS Streaming e Apache Kafka. Abbiamo esaminato entrambi in dettaglio e entrambi si sono rivelati adatti al compito. Ma alla fine abbiamo esitato a causa della relativa giovinezza di NATS Streaming (e del fatto che uno dei principali sviluppatori, Tyler Treat, ha deciso di lasciare il progetto e avviare il proprio — Liftbridge). Inoltre, la modalità di clustering di NATS Streaming non offriva la possibilità di un forte scaling orizzontale (probabilmente non è più un problema dopo l'aggiunta della modalità di partizionamento nel 2017).

Tuttavia, NATS Streaming è una tecnologia notevole, scritta in Go e supportata dalla Cloud Native Computing Foundation. A differenza di Apache Kafka, non ha bisogno di Zookeeper per funzionare (forse, presto si potrà dire lo stesso di Kafka), dato che al suo interno implementa RAFT. Inoltre, NATS Streaming è più semplice da amministrare. Non escludiamo di tornare a questa tecnologia in futuro.

Eppure, al giorno d'oggi, il nostro vincitore è stato Apache Kafka. Nei nostri test ha dimostrato di essere abbastanza veloce (oltre un milione di messaggi al secondo in lettura e scrittura con una dimensione dei messaggi di 1 kilobyte), sufficientemente affidabile, ben scalabile e collaudata in produzione da grandi aziende. Inoltre, Kafka è supportata da almeno alcune grandi compagnie commerciali (noi, ad esempio, utilizziamo la versione di Confluent), e ha anche un ecosistema ben sviluppato.

Panoramica su Kafka

Prima di cominciare, raccomando subito un ottimo libro — «Kafka: The Definitive Guide» (c'è anche una traduzione in russo, ma i termini un po' disturbano). Qui puoi trovare informazioni necessarie per una comprensione di base di Kafka e anche qualcosa in più. La documentazione di Apache e il blog di Confluent sono anche ben scritti e facili da leggere.

Quindi, diamo un'occhiata a come è strutturato Kafka dall'alto. La topologia di base di Kafka è composta da producer, consumer, broker e zookeeper.

Broker

Kafka e microservizi: panoramica

Il broker è responsabile della memorizzazione dei tuoi dati. Tutti i dati vengono memorizzati in formato binario, e il broker sa poco di ciò che rappresentano e quale sia la loro struttura.

Ogni tipo logico di eventi si trova generalmente in un topic separato. Ad esempio, un evento di creazione di un annuncio può finire nel topic item.created, mentre un evento di modifica può andare in item.changed. I topic possono essere considerati come classificatori di eventi. A livello di topic, è possibile specificare parametri di configurazione come:

  • la quantità di dati memorizzati e/o la loro età (retention.bytes, retention.ms);
  • il fattore di ridondanza dei dati (replication factor);
  • la dimensione massima di un singolo messaggio (max.message.bytes);
  • il numero minimo di repliche concordate, al quale sarà possibile scrivere dati nel topic (min.insync.replicas);
  • la possibilità di effettuare failover su una replica non sincronizzata con una potenziale perdita di dati (unclean.leader.election.enable);
  • e molte altre (https://kafka.apache.org/documentation/#topicconfigs).

A sua volta, ogni topic è suddiviso in una o più partizioni. Sono proprio nelle partizioni che alla fine vanno a finire gli eventi. Se nel cluster ci sono più broker, le partizioni saranno distribuite su tutti i broker in modo uniforme (per quanto possibile), il che consente di scalare il carico di scrittura e lettura in un singolo topic su più broker.

Sulla disco, i dati per ogni partizione sono memorizzati come file segmenti, solitamente della dimensione di un gigabyte (controllato tramite log.segment.bytes). Una caratteristica importante è che l'eliminazione dei dati dalle partizioni (quando avviene il retention) avviene proprio per segmenti (non è possibile eliminare un singolo evento da una partizione, è possibile eliminare solo un intero segmento, e solo se non è attivo).

Zookeeper

Zookeeper funge da archivio di metadati e coordinatore. È in grado di dire se i broker sono vivi (puoi guardarlo dagli occhi di zookeeper tramite il comando zookeeper-shell ls /brokers/ids), quale broker è il controller (get /controller), le partizioni sono in uno stato di sincronizzazione con le loro repliche (get /brokers/topics/topic_name/partitions/partition_number/state). Inoltre, è proprio a zookeeper che i producer e i consumer si collegheranno per sapere su quale broker sono memorizzati quali topic e partizioni. Nei casi in cui il fattore di replica per un topic è impostato su un valore superiore a 1, zookeeper indicherà quali partizioni sono i leader (in esse verranno effettuate le scritture e da esse verranno letti i dati). In caso di crash di un broker, zookeeper registrerà le informazioni sulle nuove partizioni leader (a partire dalla versione 1.1.0 in modo asincrono, ed è importante).

Nelle versioni più vecchie di Kafka, zookeeper era responsabile anche della memorizzazione degli offset, ma ora sono conservati in un topic speciale __consumer_offsets sul broker (anche se è possibile continuare a utilizzare zookeeper per questi scopi).

Il modo più semplice per trasformare i tuoi dati in una zucca è proprio perdere informazioni con zookeeper. In uno scenario del genere, capire cosa e da dove leggere sarà molto difficile.

Produttore

Il Producer è solitamente un servizio che effettua direttamente la scrittura dei dati in Apache Kafka. Il Producer sceglie il topic in cui saranno memorizzati i suoi messaggi tematici e inizia a scriverci dentro informazioni. Ad esempio, un producer può essere un servizio di annunci. In tal caso, invierà nei topic tematici eventi come "annuncio creato", "annuncio aggiornato", "annuncio eliminato" e così via. Ogni evento rappresenta una coppia chiave-valore.

Per impostazione predefinita, tutti gli eventi vengono distribuiti tra le partizioni del topic secondo il principio round-robin, se la chiave non è impostata (perdendo l'ordinamento), e attraverso MurmurHash (chiave), se la chiave è presente (ordinamento all'interno di una singola partizione).

Qui vale la pena notare che Kafka garantisce l'ordine degli eventi solo all'interno di una singola partizione. Ma in realtà spesso questo non è un problema. Ad esempio, è possibile aggiungere in modo garantito tutte le modifiche dello stesso annuncio in una partizione (mantenendo così l'ordine di queste modifiche nell'ambito dell'annuncio). È inoltre possibile comunicare un numero di sequenza in uno dei campi dell'evento.

Consumer

Kafka e microservizi: panoramica

Il consumer è responsabile per la ricezione dei dati da Apache Kafka. Tornando all'esempio precedente, il consumer può essere un servizio di moderazione. Questo servizio si iscriverà al topic del servizio di annunci e, quando appare un nuovo annuncio, lo riceverà e lo analizzerà per verificarne la conformità a determinate politiche prefissate.

Apache Kafka memorizza quali sono gli ultimi eventi ricevuti dal consumer (per questo si utilizza il topic di servizio __consumer__offsets), garantendo così che, dopo una lettura riuscita, il consumer non riceva lo stesso messaggio due volte. Tuttavia, se si utilizza l'opzione enable.auto.commit = true e si affida completamente a Kafka il monitoraggio della posizione del consumer nel topic, è possibile perdere dati. Nel codice di produzione, la posizione del consumer viene di solito gestita manualmente (lo sviluppatore controlla quando deve necessariamente avvenire il commit dell'evento letto).

Nei casi in cui un solo consumer non sia sufficiente (ad esempio, se il flusso di nuovi eventi è molto elevato), si possono aggiungere ulteriori consumer, collegandoli insieme in un consumer group. Il consumer group rappresenta logicamente un unico consumer, ma con la distribuzione dei dati tra i membri del gruppo. Questo consente a ciascun membro di prendere la propria parte di messaggi, aumentando così la velocità di lettura.

Risultati dei test

Kafka e microservizi: panoramica

Qui non scriverò molto testo esplicativo, semplicemente condividerò i risultati ottenuti. I test sono stati condotti su 3 macchine fisiche (12 CPU, 384GB RAM, 15k SAS DISK, 10GBit/s Net), i broker e lo zookeeper sono stati distribuiti in lxc.

Test di prestazioni

Durante i test sono stati ottenuti i seguenti risultati.

  • La velocità di scrittura dei messaggi di dimensioni 1KB con 9 producer contemporaneamente è di 1.300.000 eventi al secondo.
  • La velocità di lettura dei messaggi di dimensioni 1KB con 9 consumer contemporaneamente è di 1.500.000 eventi al secondo.

Testing della resilienza

Durante i test sono stati ottenuti i seguenti risultati (3 broker, 3 zookeeper).

  • Il crash anomalo di uno dei broker non provoca l'interruzione o l'inaccessibilità del cluster. Il lavoro continua normalmente, ma i broker rimanenti sono sottoposti a un carico maggiore.
  • La chiusura anomala di due broker in un cluster di tre broker con min.isr = 2 porta all'inaccessibilità del cluster in scrittura, ma è accessibile in lettura. Se min.isr = 1, il cluster rimane accessibile sia in lettura che in scrittura. Tuttavia, questa modalità contraddice il requisito di elevata integrità dei dati.
  • La chiusura anomala di uno dei server Zookeeper non comporta l'arresto o l'inaccessibilità del cluster. Il funzionamento continua normalmente.
  • La chiusura anomala di due server Zookeeper comporta l'inaccessibilità del cluster fino al ripristino dell'operatività di almeno uno dei server Zookeeper. Questa affermazione è valida per un cluster Zookeeper di 3 server. Di conseguenza, dopo alcune ricerche, è stato deciso di aumentare il cluster Zookeeper a 5 server per migliorare la resilienza.

Kafka come servizio

Kafka e microservizi: panoramica

Abbiamo constatato che Kafka è una tecnologia eccellente che ci permette di risolvere il compito che ci era stato assegnato (l'implementazione di un broker di messaggi). Tuttavia, abbiamo deciso di impedire ai servizi di accedere direttamente a Kafka e l'abbiamo chiusa tramite un servizio data-bus. Perché abbiamo fatto questo? In realtà ci sono diverse ragioni.

  • Il data-bus ha assorbito tutte le attività relative all'integrazione con Kafka (implementazione e configurazione di consumer e producer, monitoraggio, alerting, logging, scalabilità, ecc.). Così, l'integrazione con il broker di messaggi avviene nel modo più semplice possibile.

  • Il data-bus ha permesso di astrarsi dal linguaggio o dalla libreria specifica per lavorare con Kafka.

  • Il data-bus ha consentito ad altri servizi di astrarsi dallo strato di archiviazione. Potrebbe darsi che, in un certo momento, cambiamo Kafka con Pulsar, senza che nessuno se ne accorga (tutti i servizi conoscono solo l'API del data-bus).

  • Il data-bus si è occupato della validazione degli schemi degli eventi.

  • Con il data-bus è stata implementata l'autenticazione.

  • Sotto la copertura del data-bus possiamo aggiornare le versioni di Kafka senza downtime, in modo invisibile, gestendo centralmente le configurazioni di producer, consumer, broker, ecc.

  • Il data-bus ha permesso di aggiungere le funzionalità necessarie che non sono presenti in Kafka (come l'audit dei topic, il controllo delle anomalie nel cluster, la creazione di DLQ, ecc.).

  • Il data-bus consente di implementare failover in modo centralizzato per tutti i servizi.

Attualmente, per iniziare a inviare eventi al broker di messaggi, è sufficiente collegare una piccola libreria al codice del proprio servizio. È tutto. Avete la possibilità di scrivere, leggere e scalare con una sola riga di codice. Tutta l'implementazione è nascosta da voi, ciò che appare all'esterno sono solo alcune manopole per il formato del batch. Sotto il cofano, il servizio data-bus avvia nel Kubernetes il numero necessario di istanze di producer e consumer, fornendo loro la configurazione necessaria, ma tutto questo è trasparente per il vostro servizio.

Certo, non esiste una soluzione miracolosa e questo approccio ha le sue limitazioni.

  • Il data-bus deve essere mantenuto con le proprie forze, a differenza delle librerie di terze parti.
  • Il data-bus aumenta il numero di interazioni tra i servizi e il broker di messaggi, il che porta a una diminuzione delle prestazioni rispetto a Kafka nudo.
  • Non tutto può essere così facilmente nascosto dai servizi, non vogliamo duplicare la funzionalità di KSQL o Kafka Streams nel data-bus, perciò a volte è necessario consentire ai servizi di accedere direttamente.

Nel nostro caso, i vantaggi hanno superato gli svantaggi e la decisione di nascondere il broker di messaggi dietro a un servizio separato si è rivelata utile. In un anno di utilizzo non abbiamo avuto seri guasti né problemi.

P.S. Grazie alla mia ragazza, Ekaterina Obalayaeva, per le fantastiche immagini di questo articolo. Se vi sono piaciute, qui ce ne saranno ancora di più.

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