Kafka und Mikrodienste: Eine Übersicht

Kafka und Mikrodienste: Eine Übersicht

Hallo zusammen. In diesem Artikel erkläre ich, warum wir bei Avito vor neun Monaten Kafka gewählt haben und was es genau ist. Ich teile einen Anwendungsfall — den Nachrichtenbroker. Zum Schluss sprechen wir über die Vorteile, die wir durch den Einsatz von Kafka als Dienstleistung erhalten haben.

Das Problem

Kafka und Mikrodienste: Eine Übersicht

Zunächst ein wenig Kontext. Vor einiger Zeit haben wir begonnen, von einer monolithischen Architektur abzurücken, und derzeit gibt es bei Avito mehrere Hundert verschiedene Services. Diese verfügen über eigene Datenbanken, eigene Technologiestacks und sind für ihren Teil der Geschäftslogik verantwortlich.

Ein Problem bei einer großen Anzahl von Services sind die Kommunikationswege. Service A möchte oft Informationen von Service B erhalten. In diesem Fall kontaktiert Service A Service B über eine synchrone API. Service C möchte wissen, was bei den Services G und D passiert, während diese wiederum an den Services A und B interessiert sind. Wenn die Zahl solcher „neugierigen“ Services wächst, verwandeln sich die Verbindungen zwischen ihnen in ein kompliziertes Netz.

In diesem Zusammenhang kann der Dienst A jederzeit ausfallen. Was soll der Dienst B und all die anderen, die von ihm abhängen, in diesem Fall tun? Wenn für die Durchführung einer Geschäftsoperation eine Kette aufeinanderfolgender synchroner Aufrufe erforderlich ist, steigt die Wahrscheinlichkeit, dass die gesamte Operation fehlschlägt, noch mehr (und je länger diese Kette ist, desto höher ist sie).

Technologieauswahl

Kafka und Mikrodienste: Eine Übersicht

Okay, die Probleme sind klar. Wir können sie beheben, indem wir ein zentrales Nachrichtenaustauschsystem zwischen den Diensten erstellen. Jetzt muss jeder Dienst nur noch von diesem Nachrichtenaustauschsystem wissen. Darüber hinaus sollte das System ausfallsicher und horizontal skalierbar sein und im Falle eines Ausfalls einen Puffer für Anfragen sammeln, um diese später zu verarbeiten.

Lassen Sie uns nun die Technologie auswählen, mit der die Nachrichtenübermittlung realisiert wird. Zuerst klären wir, was wir von ihr erwarten:

  • Nachrichten zwischen den Diensten dürfen nicht verloren gehen;
  • Nachrichten können sich duplizieren;
  • Nachrichten können über mehrere Tage hinweg gespeichert und gelesen werden (persistenter Puffer);
  • Dienste können sich für relevante Daten anmelden;
  • Einige Dienste können die gleichen Daten lesen;
  • Nachrichten können einen detaillierten, umfangreichen Payload (event-carried state transfer) enthalten;
  • Manchmal ist eine Garantierung der Reihenfolge von Nachrichten erforderlich.

In dieser Phase war es uns auch kritisch wichtig, das maximal skalierbare und zuverlässige System mit hoher Durchsatzrate (mindestens 100.000 Nachrichten von mehreren Kilobyte pro Sekunde) auszuwählen.

An diesem Punkt haben wir uns von RabbitMQ verabschiedet (schwierig, bei hohen RPS stabil zu halten), PGQ von SkyTools (nicht schnell genug und schlecht skalierbar) und NSQ (nicht persistent). All diese Technologien werden bei uns im Unternehmen eingesetzt, aber für die zu lösende Aufgabe waren sie nicht geeignet.

Dann begannen wir, neue Technologien für uns zu betrachten – Apache Kafka, Apache Pulsar und NATS Streaming.

Pulsar haben wir als erstes ausgeschlossen. Wir entschieden, dass Kafka und Pulsar recht ähnliche Lösungen sind. Und obwohl Pulsar von großen Unternehmen getestet, neuer und theoretisch mit niedrigerer Latenz bietet, entschieden wir uns, von diesen beiden Kafka als de facto Standard für solche Aufgaben zu belassen. Wahrscheinlich werden wir in Zukunft zu Apache Pulsar zurückkehren.

Und so blieben zwei Kandidaten: NATS Streaming und Apache Kafka. Wir haben beide Lösungen ausführlich untersucht, und beide erfüllten die Anforderungen. Doch letztendlich hatten wir Bedenken bezüglich der relativen Jugend von NATS Streaming (insbesondere da einer der Hauptentwickler, Tyler Treat, beschlossen hat, das Projekt zu verlassen und sein eigenes - Liftbridge - zu starten). Zudem bot der Clustering-Modus von NATS Streaming keine starken horizontalen Skalierungsmöglichkeiten (wahrscheinlich ist das jetzt kein Problem mehr, seit der Partitionierungsmodus 2017 hinzugefügt wurde).

Dennoch ist NATS Streaming eine beeindruckende Technologie, die in Go geschrieben wurde und Unterstützung von der Cloud Native Computing Foundation erhält. Im Gegensatz zu Apache Kafka benötigt sie keinen Zookeeper für den Betrieb (möglicherweise wird man bald dasselbe über Kafka sagen können), da sie intern RAFT implementiert. Zudem ist NATS Streaming einfacher zu administrieren. Wir schließen nicht aus, dass wir in Zukunft zu dieser Technologie zurückkehren.

Und dennoch ist Apache Kafka unser Gewinner bis heute. In unseren Tests hat sie sich als ziemlich schnell erwiesen (über eine Million Nachrichten pro Sekunde beim Lesen und Schreiben bei einer Nachrichtengröße von 1 Kilobyte), ausreichend zuverlässig, gut skalierbar und hat sich in der Produktion bei großen Unternehmen bewährt. Darüber hinaus unterstützt Kafka mindestens einige große kommerzielle Unternehmen (wir verwenden beispielsweise die Confluent-Version) und hat ein umfangreiches Ökosystem.

Überblick über Kafka

Bevor wir beginnen, möchte ich gleich ein großartiges Buch empfehlen — „Kafka: The Definitive Guide“ (es gibt auch eine russische Übersetzung, aber die Begriffe sind etwas verwirrend). Darin finden sich Informationen, die für ein grundlegendes Verständnis von Kafka erforderlich sind, und sogar noch etwas mehr. Die Dokumentation von Apache selbst und der Blog von Confluent sind ebenfalls hervorragend geschrieben und leicht verständlich.

Schauen wir uns also an, wie Kafka aus der Vogelperspektive strukturiert ist. Die grundlegende Topologie von Kafka besteht aus Producer, Consumer, Broker und Zookeeper.

Broker

Kafka und Mikrodienste: Eine Übersicht

Für die Speicherung Ihrer Daten ist der Broker verantwortlich. Alle Daten werden binär gespeichert, und der Broker weiß wenig darüber, was sie sind und wie ihre Struktur aussieht.

Jeder logische Ereignistyp befindet sich in der Regel in einem separaten Topic. Zum Beispiel kann das Ereignis der Erstellung eines Elements in das Topic item.created gelangen, während das Ereignis einer Änderung im item.changed landet. Topics können als Klassifizierer für Ereignisse betrachtet werden. Auf der Ebene des Topics können Konfigurationsparameter wie folgende festgelegt werden:

  • das Volumen der gespeicherten Daten und/oder deren Alter (retention.bytes, retention.ms);
  • der Datenredundanzfaktor (replication factor);
  • die maximale Größe einer Nachricht (max.message.bytes);
  • die minimale Anzahl von übereinstimmenden Repliken, bei der Daten in das Topic geschrieben werden können (min.insync.replicas);
  • die Möglichkeit, einen Failover auf eine unsynchronisierte, nachgelagerte Replik mit potenziellem Datenverlust durchzuführen (unclean.leader.election.enable);
  • und viele weitere (https://kafka.apache.org/documentation/#topicconfigs).

Jedes Topic wird wiederum in eine oder mehrere Partitionen unterteilt. Tatsächlich gelangen die Ereignisse in die Partitionen. Wenn es im Cluster mehr als einen Broker gibt, werden die Partitionen gleichmäßig auf alle Broker verteilt (so weit wie möglich), was eine Skalierung der Schreib- und Lesevorgänge in einem Topic auf mehrere Broker gleichzeitig ermöglicht.

Auf der Festplatte werden die Daten für jede Partition in Form von Segmentdateien gespeichert, die standardmäßig eine Größe von einem Gigabyte haben (steuert über log.segment.bytes). Eine wichtige Eigenschaft ist, dass die Daten aus Partitionen (bei Auslösung der Aufbewahrung) genau in Segmenten gelöscht werden (man kann nicht ein einzelnes Ereignis aus einer Partition löschen, sondern nur ein ganzes Segment, und das zudem nur, wenn es nicht aktiv ist).

Zookeeper

Zookeeper fungiert als Metadaten-Repository und Koordinator. Er kann genau sagen, ob die Broker aktiv sind (das kann man aus der Sicht des Zookeepers mit dem Befehl ls /brokers/ids), welcher Broker der Controller ist (get /controller), ob sich die Partitionen synchron mit ihren Replikaten befinden (get /brokers/topics/topic_name/partitions/partition_number/state). Ebenso werden zuerst Producer und Consumer zu Zookeeper gehen, um herauszufinden, auf welchem Broker welche Themen und Partitionen gespeichert sind. In Fällen, in denen für ein Thema ein Replikationsfaktor von mehr als 1 festgelegt ist, wird Zookeeper angeben, welche Partitionen die Führer sind (in ihnen erfolgt das Schreiben und auch das Lesen). Im Falle eines Broker-Ausfalls wird die Information über neue Führungs-Partitionen ebenfalls in Zookeeper gespeichert (seit Version 1.1.0 asynchron, und das ist wichtig).

In älteren Versionen von Kafka war Zookeeper auch für die Speicherung von Offsets zuständig, aber heute werden sie in einem speziellen Topic gespeichert. __consumer_offsets auf dem Broker (obwohl Sie Zookeeper weiterhin für diese Zwecke verwenden können).

Der einfachste Weg, Ihre Daten zu "verderben", besteht darin, Informationen durch Zookeeper zu verlieren. In diesem Szenario wird es sehr schwierig sein zu verstehen, was und wo gelesen werden muss.

Producer

Ein Producer ist in der Regel ein Dienst, der Daten direkt in Apache Kafka schreibt. Der Producer wählt das Topic aus, in dem seine Nachrichten gespeichert werden, und beginnt, Informationen hineinzuschreiben. Beispielsweise könnte ein Producer ein Anzeigendienst sein. In diesem Fall sendet er in entsprechende Topics Ereignisse wie "Anzeige erstellt", "Anzeige aktualisiert", "Anzeige gelöscht" usw. Jedes Ereignis besteht aus einem Schlüssel-Wert-Paar.

Standardmäßig werden alle Ereignisse rundenweise über die Partitionen des Topics verteilt, wenn kein Schlüssel angegeben ist (was die Reihenfolge verliert), und über MurmurHash (schlüsselbasiert), wenn ein Schlüssel vorhanden ist (Reihenfolge innerhalb einer Partition).

Es ist wichtig zu betonen, dass Kafka die Reihenfolge der Ereignisse nur innerhalb einer Partition garantiert. In der Praxis stellt dies jedoch oft kein Problem dar. Zum Beispiel können alle Änderungen zu einem bestimmten Angebot zuverlässig in eine Partition eingefügt werden, wodurch die Reihenfolge dieser Änderungen innerhalb des Angebots erhalten bleibt. Zudem kann eine fortlaufende Nummer in einem der Felder des Ereignisses übergeben werden.

Consumer

Kafka und Mikrodienste: Eine Übersicht

Der Consumer ist verantwortlich für den Empfang von Daten aus Apache Kafka. Wenn wir zum obigen Beispiel zurückkehren, könnte der Consumer ein Moderationsdienst sein. Dieser Dienst wird auf das Topic des Angebotsdienstes abonniert und empfängt neue Angebote, um sie auf Übereinstimmung mit bestimmten festgelegten Richtlinien zu analysieren.

Apache Kafka merkt sich, welche letzten Ereignisse der Consumer erhalten hat (dafür wird ein spezielles Topic verwendet, __consumer__offsets), wodurch sichergestellt wird, dass der Consumer beim erfolgreichen Lesen nicht dieselbe Nachricht zweimal erhält. Wenn jedoch die Option enable.auto.commit = true verwendet wird und die Verantwortung für die Verfolgung der Position des Consumers im Topic Kafka überlassen wird, kann man Daten verlieren.. Im Produktionscode wird die Position des Consumers häufig manuell kontrolliert (der Entwickler steuert den Moment, wann der Commit des gelesenen Ereignisses erfolgen muss).

In Fällen, in denen ein Consumer nicht ausreicht (z. B. wenn der Strom neuer Ereignisse sehr groß ist), kann man mehrere Consumer hinzufügen und sie in einer Consumer-Gruppe verbinden. Eine Consumer-Gruppe stellt logisch einen einzelnen Consumer dar, jedoch mit einer Verteilung der Daten zwischen den Gruppenmitgliedern. Dies ermöglicht es jedem Teilnehmer, seinen Anteil an Nachrichten zu erhalten, wodurch die Lesegeschwindigkeit skaliert wird.

Testergebnisse

Kafka und Mikrodienste: Eine Übersicht

Hier werde ich nicht viel erläuternden Text schreiben, sondern einfach die erhaltenen Ergebnisse teilen. Die Tests wurden auf 3 physischen Maschinen durchgeführt (12 CPU, 384GB RAM, 15k SAS DISK, 10GBit/s Net), Broker und Zookeeper wurden in LXC bereitgestellt.

Leistungstest

Im Verlauf der Tests wurden folgende Ergebnisse erzielt.

  • Die Schreibgeschwindigkeit von 1KB großen Nachrichten bei 9 Produzenten gleichzeitig – 1.300.000 Ereignisse pro Sekunde.
  • Die Lesegeschwindigkeit von 1KB großen Nachrichten bei 9 Consumern gleichzeitig – 1.500.000 Ereignisse pro Sekunde.

Tests zur Ausfallsicherheit

Im Verlauf der Tests wurden folgende Ergebnisse erzielt (3 Broker, 3 Zookeeper).

  • Ein unerwartetes Aussetzen eines Brokers führt nicht zur Stilllegung oder Nichtverfügbarkeit des Clusters. Der Betrieb läuft weiterhin planmäßig, jedoch wird die Last auf die verbleibenden Broker größer.
  • Ein unerwartetes Aussetzen von zwei Brokern in einem Cluster aus drei Brokern bei min.isr = 2 führt zur Nichtverfügbarkeit des Clusters für Schreibvorgänge, aber zur Verfügbarkeit für Lesevorgänge. Wenn min.isr = 1 ist, bleibt das Cluster sowohl für Lese- als auch Schreibvorgänge verfügbar. Dennoch widerspricht dieser Modus den Anforderungen an hohe Datensicherheit.
  • Ein unerwartetes Aussetzen eines Zookeeper-Servers führt nicht zur Stilllegung oder Nichtverfügbarkeit des Clusters. Der Betrieb läuft weiterhin planmäßig.
  • Ein unerwartetes Aussetzen von zwei Zookeeper-Servern führt zur Nichtverfügbarkeit des Clusters, bis mindestens einer der Zookeeper-Server wieder verfügbar ist. Diese Aussage trifft für einen Zookeeper-Cluster aus 3 Servern zu. Infolge von Untersuchungen wurde beschlossen, den Zookeeper-Cluster auf 5 Server zu erweitern, um die Fehlertoleranz zu erhöhen.

Kafka als Dienst

Kafka und Mikrodienste: Eine Übersicht

Wir haben festgestellt, dass Kafka eine hervorragende Technologie ist, die uns ermöglicht, die gestellte Aufgabe (die Implementierung eines Messaging-Brokers) zu lösen. Dennoch haben wir beschlossen, den direkten Zugriff auf Kafka für die Services zu verbieten und es mit einem Data-Bus zu überlagern. Warum haben wir das getan? Tatsächlich gibt es dafür mehrere Gründe.

  • Der Data-Bus übernimmt alle Aufgaben im Zusammenhang mit der Integration von Kafka (Implementierung und Konfiguration von Consumer- und Producer-Instanzen, Monitoring, Alarming, Logging, Skalierung usw.). Dadurch gestaltet sich die Integration mit dem Messaging-Broker besonders einfach.

  • Der Data-Bus ermöglicht es, sich von einer bestimmten Sprache oder Bibliothek für die Arbeit mit Kafka zu abstrahieren.

  • Der Data-Bus erlaubt es anderen Services, sich von der Speicherschicht zu abstrahieren. könnten wir irgendwann Kafka durch Pulsar ersetzen, und niemand würde es bemerken (alle Services kennen nur die API des Data-Bus).

  • Der Data-Bus übernimmt die Validierung von Ereignisschemas.

  • Die Authentifizierung wird über den Data-Bus umgesetzt.

  • Mit dem Data-Bus können wir die Versionen von Kafka ohne Downtime und unbemerkt aktualisieren und zentralisierte Konfigurationen von Producer- und Consumer-Instanzen sowie Brokern verwalten.

  • Der Data-Bus ermöglicht es, notwendige Funktionen hinzuzufügen, die in Kafka nicht vorhanden sind (wie z. B. Audit von Topics, Überwachung von Anomalien im Cluster, Erstellung von DLQ usw.).

  • Der Data-Bus ermöglicht ein zentrales Failover für alle Dienste.

Aktuell reicht es aus, eine kleine Bibliothek in den Code Ihres Dienstes einzufügen, um mit dem Senden von Ereignissen an den Nachrichtenbroker zu beginnen. Das ist alles. Sie haben die Möglichkeit, zu schreiben, zu lesen und zu skalieren – und das mit nur einer Zeile Code. Die gesamte Implementierung bleibt Ihnen verborgen, nur einige Parameter wie die Batch-Größe sind sichtbar. Im Hintergrund startet der Data-Bus in Kubernetes die erforderliche Anzahl an Producer- und Consumer-Instanzen und versorgt diese mit der notwendigen Konfiguration, aber all das geschieht transparent für Ihren Dienst.

Natürlich gibt es keine silberne Kugel, und dieser Ansatz hat seine eigenen Einschränkungen.

  • Den Data-Bus müssen Sie eigenständig unterstützen, im Gegensatz zu externen Bibliotheken.
  • Der Data-Bus erhöht die Anzahl der Interaktionen zwischen den Diensten und dem Nachrichtenbroker, was im Vergleich zu reiner Kafka zu einer geringeren Leistung führt.
  • Nicht alles lässt sich so einfach vor Services verstecken, und wir möchten die Funktionalität von KSQL oder Kafka Streams nicht in unserem Datenbus duplizieren. Daher müssen wir manchmal zulassen, dass Services direkt zugreifen.

In unserem Fall überwogen die Vorteile die Nachteile, und die Entscheidung, den Nachrichtenbroker als separaten Service zu betreiben, hat sich bewährt. In einem Jahr Betrieb hatten wir keine ernsthaften Ausfälle oder Probleme.

P.S. Vielen Dank an meine Freundin, Ekaterina Obalaya, für die großartigen Bilder zu diesem Artikel. Wenn sie Ihnen gefallen haben, hier werden Sie noch mehr Illustrationen finden.

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster