
Ciao a tutti. In questo articolo ti racconterò perché noi di Avito abbiamo scelto Kafka nove mesi fa e di cosa si tratta. Condividerò uno dei casi d'uso: un broker di messaggi. E infine, parliamo dei vantaggi che abbiamo ottenuto dall’utilizzo dell’approccio Kafka as a Service.
Problema

Innanzitutto, un po’ di contesto. Qualche tempo fa abbiamo iniziato ad allontanarci dall'architettura monolitica e ora Avito dispone già di diverse centinaia di servizi diversi. Hanno i propri repository, il proprio stack tecnologico e sono responsabili della propria parte della logica aziendale.
Uno dei problemi con un gran numero di servizi è la comunicazione. Il servizio A spesso desidera conoscere le informazioni di cui dispone il servizio B. In questo caso, il servizio A accede al servizio B tramite un'API sincrona. Il servizio B vuole sapere cosa sta succedendo ai servizi D e D e loro, a loro volta, sono interessati ai servizi A e B. Quando ci sono molti servizi "curiosi", le connessioni tra loro si trasformano in un groviglio intricato.
Allo stesso tempo, il servizio A potrebbe non essere più disponibile in qualsiasi momento. E cosa dovrebbero fare in questo caso il servizio B e tutti gli altri servizi ad esso collegati? E se per completare un'operazione aziendale è necessaria una catena di chiamate sincrone sequenziali, la probabilità di fallimento dell'intera operazione diventa ancora più elevata (e più lunga è la catena, maggiore è).
Selezione della tecnologia

Ok, i problemi sono chiari. Possono essere eliminati creando un sistema di messaggistica centralizzato tra i servizi. Ora ciascuno dei servizi deve solo conoscere questo sistema di messaggistica. Inoltre il sistema stesso deve essere tollerante ai guasti e scalabile orizzontalmente e, in caso di incidenti, accumulare un buffer di accesso per la successiva elaborazione.
Selezioniamo ora la tecnologia su cui verrà implementata la consegna dei messaggi. Per fare ciò, vediamo innanzitutto cosa ci aspettiamo da esso:
- i messaggi tra i servizi non dovrebbero andare persi;
- i messaggi possono essere duplicati;
- i messaggi possono essere archiviati e letti per una profondità di diversi giorni (buffer persistente);
- i servizi possono iscriversi ai dati che li interessano;
- più servizi possono leggere gli stessi dati;
- i messaggi possono contenere un carico utile dettagliato e voluminoso (trasferimento di stato trasportato da eventi);
- A volte è necessario garantire l'ordine dei messaggi.
Per noi era anche di fondamentale importanza scegliere il sistema più scalabile e affidabile con un rendimento elevato (almeno 100 messaggi di diversi kilobyte al secondo).
A questo punto abbiamo detto addio a RabbitMQ (difficile da mantenere stabile ad alti rps), PGQ di SkyTools (non abbastanza veloce e non scala bene) e NSQ (non persistente). Utilizziamo tutte queste tecnologie nella nostra azienda, ma non erano adatte al problema da risolvere.
Successivamente, abbiamo iniziato a esaminare tecnologie che erano nuove per noi: Apache Kafka, Apache Pulsar e NATS Streaming.
Pulsar fu il primo ad essere scartato. Abbiamo deciso che Kafka e Pulsar sono soluzioni abbastanza simili. E nonostante Pulsar sia stato testato da grandi aziende, sia più recente e offra una latenza inferiore (in teoria), abbiamo deciso di lasciare Kafka tra questi due come standard de facto per tali compiti. Probabilmente torneremo ad Apache Pulsar in futuro.
E ora restano due candidati: NATS Streaming e Apache Kafka. Abbiamo studiato entrambe le soluzioni in dettaglio ed entrambe erano adatte al compito. Ma alla fine, avevamo paura della relativa giovinezza di NATS Streaming (e del fatto che uno dei principali sviluppatori, Tyler Treat, abbia deciso di abbandonare il progetto e avviarne uno proprio: Liftbridge). Allo stesso tempo, la modalità Clustering di NATS Streaming non prevedeva la possibilità di un forte ridimensionamento orizzontale (probabilmente questo non è più un problema dopo l'aggiunta della modalità di partizionamento nel 2017).
Tuttavia, NATS Streaming è una tecnologia interessante scritta in Go e supportata dalla Cloud Native Computing Foundation. A differenza di Apache Kafka, non ha bisogno di Zookeeper per funzionare (forse ), poiché implementa RAFT internamente. Allo stesso tempo, NATS Streaming è più facile da amministrare. Non escludiamo che si ritorni a questa tecnologia in futuro.
Eppure oggi il nostro vincitore è Apache Kafka. Nei nostri test si è rivelato abbastanza veloce (più di un milione di messaggi al secondo in lettura e scrittura con un volume di messaggi di 1 kilobyte), abbastanza affidabile, altamente scalabile e comprovato dall'esperienza nella produzione di grandi aziende. Inoltre, Kafka supporta almeno diverse grandi società commerciali (noi, ad esempio, utilizziamo la versione Confluent) e Kafka ha anche un ecosistema sviluppato.
Panoramica di Kafka
Prima di iniziare, vorrei consigliarvi subito un ottimo libro: "Kafka: la guida definitiva" (esiste anche una traduzione in russo, ma i termini sono un po' sconcertanti). Contiene le informazioni necessarie per acquisire una conoscenza di base di Kafka e anche qualcosa in più. Anche la documentazione di Apache e il blog di Confluent sono ben scritti e facili da leggere.
Quindi diamo uno sguardo d'insieme su come funziona Kafka. La topologia di base di Kafka è composta da produttore, consumatore, intermediario e guardiano dello zoo.
Broker

Il broker è responsabile della memorizzazione dei tuoi dati. Tutti i dati sono archiviati in formato binario e il broker sa poco di cosa siano e quale sia la loro struttura.
Ogni tipo di evento logico si trova solitamente in un argomento separato. Ad esempio, l'evento della creazione di un annuncio potrebbe rientrare nell'argomento item.created e l'evento della sua modifica potrebbe rientrare nell'argomento item.changed. Gli argomenti possono essere considerati come classificatori di eventi. A livello di argomento è possibile impostare parametri di configurazione quali:
- la quantità di dati archiviati e/o la loro età (retention.bytes,tention.ms);
- fattore di ridondanza dei dati (fattore di replica);
- dimensione massima di un messaggio (max.message.bytes);
- il numero minimo di repliche coerenti con cui i dati possono essere scritti su un argomento (min.insync.replicas);
- la possibilità di eseguire un failover su una replica ritardata non sincrona con potenziale perdita di dati (unclean.leader.election.enable);
- e molti altri ().
A sua volta, ogni argomento è suddiviso in una o più partizioni. È nei partiti che alla fine cadono gli eventi. Se è presente più di un broker nel cluster, le partizioni verranno distribuite equamente tra tutti i broker (per quanto possibile), il che consentirà di scalare il carico di scrittura e lettura in un argomento su più broker contemporaneamente.
Sul disco, i dati per ciascuna partizione vengono archiviati sotto forma di file di segmenti, per impostazione predefinita pari a un gigabyte (controllati tramite log.segment.bytes). Una caratteristica importante è che i dati vengono eliminati dalle partizioni (quando viene attivata la conservazione) in segmenti (non è possibile eliminare un evento da una partizione, è possibile eliminare solo un intero segmento e solo quello inattivo).
Zookeeper
Zookeeper funge da archivio e coordinatore di metadati. È lui che è in grado di dire se i broker sono vivi (puoi guardarlo attraverso gli occhi di zookeeper usando zookeeper-shell con il comando ls /brokers/ids), quale broker è il titolare del trattamento (get /controller), se le partizioni sono sincronizzate con le relative repliche (get /brokers/topics/topic_name/partitions/partition_number/state). Inoltre, è allo zookeeper che il produttore e il consumatore si recheranno prima per scoprire su quale broker sono archiviati quali argomenti e partizioni. Nei casi in cui per un argomento viene specificato un fattore di replica maggiore di 1, zookeeper indicherà quali partizioni sono i leader (verranno scritti e letti). In caso di guasto del broker, le informazioni sulle nuove partizioni leader verranno registrate in zookeeper (dalla versione 1.1.0 in modo asincrono, ).
Nelle versioni precedenti di Kafka, il guardiano dello zoo era anche responsabile della memorizzazione degli offset, ma ora sono archiviati in un argomento speciale __consumer_offsets sul broker (anche se puoi comunque utilizzare zookeeper per questi scopi).
Il modo più semplice per trasformare i tuoi dati in una zucca è perdere le informazioni dal guardiano dello zoo. In uno scenario del genere, sarà molto difficile capire cosa leggere e da dove.
Produttore
Producer è molto spesso un servizio che scrive direttamente i dati su Apache Kafka. Il produttore seleziona un argomento in cui memorizzare i messaggi dell'argomento e inizia a scrivervi le informazioni. Ad esempio, il produttore potrebbe essere un servizio pubblicitario. In questo caso, invierà eventi come “annuncio creato”, “annuncio aggiornato”, “annuncio eliminato”, ecc. ad argomenti tematici. Ogni evento è una coppia chiave-valore.
Per impostazione predefinita, tutti gli eventi vengono distribuiti tra le partizioni degli argomenti utilizzando il round robin se la chiave non è specificata (ordine perdente) e tramite MurmurHash (chiave) se la chiave è presente (ordine all'interno di una partizione).
Vale subito la pena notare che Kafka garantisce l'ordine degli eventi solo all'interno di un lotto. Ma in realtà questo spesso non è un problema. Ad esempio, puoi essere sicuro di aggiungere tutte le modifiche alla stessa dichiarazione in un'unica partizione (preservando così l'ordine di queste modifiche all'interno della dichiarazione). Puoi anche inviare un numero di sequenza in uno dei campi evento.
Consumatori

Il consumatore è responsabile del recupero dei dati da Apache Kafka. Se torniamo all'esempio precedente, il consumatore potrebbe essere un servizio di moderazione. Questo servizio verrà iscritto all'argomento del servizio annunci e, quando verrà visualizzato un nuovo annuncio, lo riceverà e lo analizzerà per verificarne la conformità con alcune politiche specificate.
Apache Kafka ricorda quali eventi recenti ha ricevuto il consumatore (a questo scopo viene utilizzato un argomento di servizio __consumer__offsets), garantendo così che, se la lettura ha esito positivo, il consumatore non riceva due volte lo stesso messaggio. Tuttavia, se utilizzi l'opzione Enable.auto.commit = true e deleghi completamente il lavoro di tracciamento della posizione del consumatore nell'argomento a Kafka, puoi . Nel codice di produzione, molto spesso la posizione del consumatore viene controllata manualmente (lo sviluppatore controlla il momento in cui deve verificarsi il commit dell'evento di lettura).
Nei casi in cui un consumatore non è sufficiente (ad esempio, il flusso di nuovi eventi è molto ampio), è possibile aggiungere molti altri consumatori collegandoli insieme in un gruppo di consumatori. Un gruppo di consumatori è logicamente identico a un consumatore, ma con i dati distribuiti tra i membri del gruppo. Ciò consente a ciascun partecipante di prendere la propria parte di messaggi, aumentando così la velocità di lettura.
Risultati dei test

Non scriverò molto testo esplicativo qui, condividerò solo i risultati ottenuti. I test sono stati effettuati su 3 macchine fisiche (12 CPU, 384 GB di RAM, 15k SAS DISK, 10 GBit/s Net), broker e zookeeper sono stati distribuiti in lxc.
Test delle prestazioni
Durante le prove si sono ottenuti i seguenti risultati.
- La velocità di registrazione simultanea di messaggi da 1 KB da parte di 9 produttori è di 1300000 eventi al secondo.
- La velocità di lettura simultanea di messaggi da 1 KB da parte di 9 consumatori è di 1500000 eventi al secondo.
Test di tolleranza ai guasti
Durante i test sono stati ottenuti i seguenti risultati (3 intermediari, 3 guardiani dello zoo).
- La chiusura anomala di uno dei broker non causa l'arresto o la non disponibilità del cluster. Il lavoro continua normalmente, ma i restanti broker hanno un carico di lavoro pesante.
- La terminazione anomala di due broker nel caso di un cluster di tre broker e min.isr = 2 fa sì che il cluster non sia disponibile per la scrittura, ma accessibile per la lettura. Se min.isr = 1, il cluster continua ad essere disponibile sia in lettura che in scrittura. Tuttavia, questa modalità contraddice i requisiti di elevata sicurezza dei dati.
- Un arresto anomalo di uno dei server Zookeeper non provoca l'arresto o la non disponibilità del cluster. Il lavoro continua normalmente.
- Un arresto anomalo di due server Zookeeper comporta la non disponibilità del cluster fino al ripristino di almeno uno dei server Zookeeper. Questa affermazione è vera per un cluster Zookeeper di 3 server. Di conseguenza, dopo la ricerca, si è deciso di aumentare il cluster Zookeeper a 5 server per aumentare la tolleranza agli errori.
Kafka come servizio

Siamo convinti che Kafka sia un'ottima tecnologia che ci permette di risolvere il compito assegnatoci (implementare un broker di messaggi). Tuttavia, abbiamo deciso di vietare ai servizi di accedere direttamente a Kafka e di chiuderlo con un servizio di bus dati. Perché lo abbiamo fatto? In effetti, ci sono parecchie ragioni.
Data-bus ha rilevato tutti i compiti relativi all'integrazione con Kafka (implementazione e configurazione di consumatori e produttori, monitoraggio, avvisi, registrazione, ridimensionamento, ecc.). Pertanto, l'integrazione con il broker di messaggi è il più semplice possibile.
Il bus dati ci ha permesso di allontanarci da un linguaggio o libreria specifica per lavorare con Kafka.
Il bus dati ha consentito ad altri servizi di astrarre il livello di archiviazione. Forse ad un certo punto cambieremo Kafka in Pulsar e nessuno si accorgerà di nulla (tutti i servizi conoscono solo l'API del bus dati).
Il bus dati ha assunto la validazione degli schemi di eventi.
L'autenticazione viene implementata utilizzando il bus dati.
Sotto la copertura del bus dati, possiamo aggiornare silenziosamente le versioni di Kafka senza tempi di inattività, gestire centralmente le configurazioni di produttori, consumatori, broker, ecc.
Il bus dati ci ha permesso di aggiungere le funzionalità di cui avevamo bisogno che non sono presenti in Kafka (come il controllo degli argomenti, il monitoraggio delle anomalie nel cluster, la creazione di DLQ, ecc.).
Il bus dati consente di implementare il failover centralmente per tutti i servizi.
Al momento, per iniziare a inviare eventi al broker di messaggi, devi solo connettere una piccola libreria al tuo codice di servizio. Questo è tutto. Hai la capacità di scrivere, leggere e scalare con una riga di codice. L'intera implementazione ti è nascosta, con solo poche maniglie delle dimensioni del batch che sporgono. Sotto il cofano, il servizio bus dati aumenta il numero richiesto di istanze produttore e consumatore in Kubernetes e fornisce loro la configurazione necessaria, ma tutto questo è trasparente per il tuo servizio.
Naturalmente non esiste una soluzione miracolosa e questo approccio ha i suoi limiti.
- Il bus dati deve essere supportato internamente, al contrario delle librerie di terze parti.
- Il bus dati aumenta il numero di interazioni tra i servizi e il broker di messaggi, il che si traduce in prestazioni inferiori rispetto al semplice Kafka.
- Non tutto può essere nascosto dai servizi così facilmente; non vogliamo duplicare la funzionalità di KSQL o Kafka Streams nel bus dati, quindi a volte dobbiamo consentire ai servizi di andare direttamente.
Nel nostro caso, i pro hanno superato i contro e la decisione di coprire il broker di messaggi con un servizio separato era giustificata. Durante l'anno di attività non abbiamo avuto incidenti o problemi gravi.
PS Grazie alla mia ragazza, Ekaterina Obalyaeva, per le belle foto di questo articolo. Se ti sono piaciuti, ci sono altre illustrazioni in arrivo.
Fonte: habr.com
