
Hallo an alle. In diesem Artikel werde ich Ihnen erklären, warum wir uns vor neun Monaten in Avito für Kafka entschieden haben und was es ist. Ich werde einen der Anwendungsfälle teilen - einen Nachrichtenbroker. Lassen Sie uns abschließend darüber sprechen, welche Vorteile sich aus der Anwendung des Kafka as a Service-Ansatzes ergeben.
Das Problem

Zunächst ein kleiner Kontext. Vor einiger Zeit haben wir begonnen, uns von der monolithischen Architektur zu entfernen, und jetzt gibt es in Avito bereits mehrere hundert verschiedene Dienstleistungen. Sie haben ihre eigenen Repositorys, ihren eigenen Technologie-Stack und sind für ihren Teil der Geschäftslogik verantwortlich.
Eines der Probleme bei einer großen Anzahl von Diensten ist die Kommunikation. Dienst A möchte häufig die Informationen wissen, über die Dienst B verfügt. In diesem Fall greift Dienst A über eine synchrone API auf Dienst B zu. Dienst B möchte wissen, was mit den Diensten G und D passiert, und diese wiederum sind an den Diensten A und B interessiert. Wenn es viele solcher „neugierigen“ Dienste gibt, verwandeln sich die Verbindungen zwischen ihnen in einen Wirrwarr.
Darüber hinaus kann Service A jederzeit nicht mehr verfügbar sein. Und was tun in diesem Fall, Service B und alle anderen damit verbundenen Services? Und wenn Sie eine Kette aufeinanderfolgender synchroner Aufrufe ausführen müssen, um einen Geschäftsvorgang abzuschließen, wird die Wahrscheinlichkeit eines Ausfalls des gesamten Vorgangs noch höher (und je höher, desto länger diese Kette).
Technologieauswahl

OK, die Probleme sind klar. Sie können sie beseitigen, indem Sie ein zentrales Nachrichtensystem zwischen den Diensten einrichten. Jetzt reicht jeder der Dienste aus, um nur über dieses Nachrichtensystem Bescheid zu wissen. Darüber hinaus muss das System selbst fehlertolerant und horizontal skalierbar sein und bei Unfällen einen Anrufpuffer für die anschließende Verarbeitung ansammeln.
Wählen wir nun die Technologie aus, auf der die Nachrichtenübermittlung implementiert werden soll. Um dies zu tun, verstehen Sie zuerst, was wir von ihr erwarten:
- Nachrichten zwischen Diensten sollten nicht verloren gehen.
- Nachrichten können dupliziert werden
- Nachrichten können bis zu einer Tiefe von mehreren Tagen gespeichert und gelesen werden (persistenter Puffer);
- Dienste können Daten abonnieren, die für sie von Interesse sind;
- Mehrere Dienste können dieselben Daten lesen.
- Nachrichten können detaillierte Massennutzdaten enthalten (ereignisgesteuerte Statusübertragung).
- Manchmal benötigen Sie eine Garantie für die Nachrichtenbestellung.
Für uns war es auch wichtig, das skalierbarste und zuverlässigste System mit hohem Durchsatz auszuwählen (mindestens 100.000 Nachrichten mit wenigen Kilobyte pro Sekunde).
Zu diesem Zeitpunkt haben wir uns von RabbitMQ (es ist schwierig, bei hohen Drehzahlen stabil zu bleiben), SkyTools PGQ (nicht schnell genug und schlecht skalierbar) und NSQ (nicht persistent) verabschiedet. Alle diese Technologien werden in unserem Unternehmen eingesetzt, aber sie passten nicht zur jeweiligen Aufgabe.
Dann suchten wir nach neuen Technologien für uns - Apache Kafka, Apache Pulsar und NATS Streaming.
Der erste, der Pulsar fallen ließ. Wir haben entschieden, dass Kafka und Pulsar ziemlich ähnliche Lösungen sind. Und trotz der Tatsache, dass Pulsar von großen Unternehmen getestet wird, neuer ist und (theoretisch) eine geringere Latenz bietet, haben wir beschlossen, Kafka als De-facto-Standard für solche Aufgaben aus den beiden herauszulassen. Wir werden wahrscheinlich in Zukunft zu Apache Pulsar zurückkehren.
Und es waren noch zwei Kandidaten übrig: NATS Streaming und Apache Kafka. Wir haben beide Lösungen eingehend untersucht und beide haben die Aufgabe erfüllt. Am Ende hatten wir jedoch Angst vor der relativen Jugend von NATS Streaming (und vor der Tatsache, dass einer der Hauptentwickler, Tyler Treat, beschlossen hat, das Projekt zu verlassen und ein eigenes zu gründen - Liftbridge). Gleichzeitig ermöglichte der Clustering-Modus von NATS Streaming keine starke horizontale Skalierung (dies ist wahrscheinlich kein Problem mehr, nachdem 2017 der Partitionierungsmodus hinzugefügt wurde).
NATS Streaming ist jedoch eine coole Technologie, die in Go geschrieben und von der Cloud Native Computing Foundation unterstützt wird. Im Gegensatz zu Apache Kafka muss Zookeeper nicht funktionieren ( möglicherweise kann man bald dasselbe über Kafka sagen ), da darin RAFT implementiert ist. Gleichzeitig ist NATS Streaming einfacher zu verwalten. Wir schließen nicht aus, dass wir in Zukunft auf diese Technologie zurückkommen werden.
Trotzdem ist Apache Kafka heute unser Gewinner geworden. In unseren Tests erwies es sich als recht schnell (mehr als eine Million Nachrichten pro Sekunde zum Lesen und Schreiben mit einem Nachrichtenvolumen von 1 Kilobyte), zuverlässig genug, gut skalierbar und nachgewiesene Erfahrung im Verkauf durch große Unternehmen. Darüber hinaus unterstützt Kafka mindestens mehrere große Handelsunternehmen (zum Beispiel verwenden wir die Confluent-Version), und Kafka verfügt über ein entwickeltes Ökosystem.
Bewertung Kafka
Bevor ich anfange, empfehle ich sofort ein ausgezeichnetes Buch - "Kafka: The Definitive Guide" (es ist auch in der russischen Übersetzung, aber die Begriffe brechen das Gehirn ein wenig). Darin finden Sie die Informationen, die für ein grundlegendes Verständnis von Kafka und noch ein wenig mehr erforderlich sind. Die Apache-Dokumentation selbst und der Confluent-Blog sind ebenfalls gut geschrieben und leicht zu lesen.
Schauen wir uns also an, wie Kafka aus der Vogelperspektive ist. Die grundlegende Kafka-Topologie besteht aus Produzenten, Verbrauchern, Maklern und Tierpflegern.
Makler

Ein Broker ist für die Speicherung Ihrer Daten verantwortlich. Alle Daten werden in binärer Form gespeichert, und der Broker weiß wenig darüber, was sie sind und wie sie aufgebaut sind.
Jeder logische Ereignistyp befindet sich normalerweise in einem eigenen Thema (Thema). Beispielsweise kann ein Ereignis zur Anzeigenerstellung in das Thema item.created fallen, und ein Ereignis seiner Änderung kann in item.changed fallen. Themen können als Klassifikatoren von Ereignissen betrachtet werden. Auf Themenebene können Sie folgende Konfigurationsparameter festlegen:
- gespeicherte Datenmenge und / oder deren Alter (Retention.bytes, Retention.ms);
- Datenredundanzfaktor (Replikationsfaktor);
- maximale Größe einer Nachricht (max.message.bytes);
- die Mindestanzahl konsistenter Replikate, mit denen Daten in das Thema geschrieben werden können (min.insync.replicas);
- die Möglichkeit eines Failovers auf ein nicht synchrones Replikat mit Verzögerung und potenziellem Datenverlust (unclean.leader.election.enable);
- und viele mehr ( https://kafka.apache.org/documentation/#topicconfigs ).
Jedes Thema ist wiederum in eine oder mehrere Partitionen (Partition) unterteilt. In der Partition fallen letztendlich Ereignisse. Wenn der Cluster mehr als einen Broker enthält, werden die Partitionen gleichmäßig auf alle Broker verteilt (so weit wie möglich), sodass Sie die Belastung beim Schreiben und Lesen in einem Thema auf mehrere Broker gleichzeitig skalieren können.
Auf der Festplatte werden die Daten für jede Partition als Segmentdateien gespeichert, die standardmäßig einem Gigabyte entsprechen (gesteuert über log.segment.bytes). Eine wichtige Funktion ist das Löschen von Daten aus Partitionen (wenn die Aufbewahrung ausgelöst wird) nur durch Segmente (Sie können nicht ein Ereignis aus einer Partition löschen, Sie können nur das gesamte Segment löschen und nur inaktiv).
Tierpfleger
Zookeeper fungiert als Metadaten-Repository und Koordinator. Er kann sagen, ob Broker am Leben sind (Sie können es mit den Augen eines Tierpflegers durch den Zookeeper-Shell-Befehl ls /brokers/ids
brokers ls /brokers/ids
), welcher der Makler der Controller ist ( get /controller
), ob die Partitionen mit ihren Replikaten synchron sind ( get /brokers/topics/topic_name/partitions/partition_number/state
). Außerdem gehen Produzent und Konsument zuerst zum Tierpfleger, um herauszufinden, auf welchem Broker welche Themen und Partitionen gespeichert sind. In Fällen, in denen ein Replikationsfaktor größer als 1 für das Thema angegeben ist, gibt der Tierpfleger an, welche Partitionen führend sind (sie werden beschrieben und von ihnen gelesen). Im Falle eines Broker-Absturzes werden im Zookeeper Informationen zu den neuen Leader-Partitionen aufgezeichnet (ab Version 1.1.0 asynchron, und dies ist wichtig ).
In älteren Versionen von Kafka war zookeeper auch für das Speichern von Offsets verantwortlich. Jetzt werden sie in einem speziellen Thema __consumer_offsets
auf dem Broker gespeichert (obwohl Sie zookeeper für diese Zwecke weiterhin verwenden können).
Der einfachste Weg, Ihre Daten in einen Kürbis zu verwandeln, ist der Informationsverlust mit zookeeper. In einem solchen Szenario ist es sehr schwierig zu verstehen, woraus und wo zu lesen ist.
Produzent
Der Produzent ist meistens ein Dienst, der Daten direkt in Apache Kafka schreibt. Der Produzent wählt ein Thema aus, in dem seine thematischen Nachrichten gespeichert werden, und beginnt, Informationen darauf zu schreiben. Ein Produzent könnte beispielsweise ein Werbedienst sein. In diesem Fall sendet er Ereignisse wie "Anzeige erstellt", "Anzeige aktualisiert", "Anzeige gelöscht" usw. an thematische Themen. Jedes Ereignis ist ein Schlüssel-Wert-Paar.
Standardmäßig werden alle Ereignisse von den Partitionspartitionen mit Round-Robin verteilt, wenn der Schlüssel nicht festgelegt ist (Verlust der Reihenfolge), und über MurmurHash (Schlüssel), wenn der Schlüssel vorhanden ist (Reihenfolge innerhalb derselben Partition).
Hier ist sofort anzumerken, dass Kafka die Reihenfolge der Ereignisse innerhalb nur einer Partition garantiert. Tatsächlich ist dies jedoch häufig kein Problem. Beispielsweise können Sie garantiert alle Änderungen derselben Ankündigung zu einer Partition hinzufügen (wodurch die Reihenfolge dieser Änderungen innerhalb der Ankündigung beibehalten wird). Sie können auch eine Sequenznummer in einem der Ereignisfelder übergeben.
Verbraucher

Der Verbraucher ist für das Abrufen von Daten von Apache Kafka verantwortlich. Wenn Sie zum obigen Beispiel zurückkehren, kann der Verbraucher ein Moderationsdienst sein. Dieser Dienst wird für das Thema des Ankündigungsdienstes abonniert. Wenn eine neue Anzeige erscheint, wird sie empfangen und auf Einhaltung bestimmter Richtlinien analysiert.
Apache Kafka merkt sich, welche Ereignisse der Verbraucher in letzter Zeit erhalten hat (hierfür wird das __consumer__offsets
verwendet), wodurch sichergestellt wird, dass der Verbraucher nach erfolgreichem Lesen nicht zweimal dieselbe Nachricht erhält. Wenn Sie jedoch die Option enable.auto.commit = true verwenden und Kafka die Aufgabe übertragen, die Position des Verbrauchers im Thema zu verfolgen, können Sie Daten verlieren . Im Produktionscode wird die Position des Verbrauchers meistens manuell gesteuert (der Entwickler steuert den Moment, in dem das Festschreiben des Leseereignisses erfolgen muss).
In Fällen, in denen ein Verbraucher nicht ausreicht (z. B. ist der Fluss neuer Ereignisse sehr groß), können Sie einige weitere Verbraucher hinzufügen, indem Sie sie in der Verbrauchergruppe miteinander verknüpfen. Die Verbrauchergruppe ist logischerweise genau derselbe Verbraucher, jedoch mit der Verteilung der Daten unter den Gruppenmitgliedern. Auf diese Weise kann jeder Teilnehmer seinen Anteil an Nachrichten übernehmen und so die Lesegeschwindigkeit erhöhen.
Testergebnisse

Hier werde ich nicht viel erklärenden Text schreiben, sondern nur die Ergebnisse teilen. Die Tests wurden auf 3 physischen Maschinen (12 CPU, 384 GB RAM, 15 KB SAS-Festplatte, 10 GBit / s Net) durchgeführt. Broker und Zookeeper wurden in lxc bereitgestellt.
Leistungstests
Während des Testens wurden die folgenden Ergebnisse erhalten.
- Die Geschwindigkeit der gleichzeitigen Aufzeichnung von Nachrichten mit einer Größe von 1 KB durch 9 Produzenten - 1300000 Ereignisse pro Sekunde.
- Lesegeschwindigkeit von 1 KB Nachrichten gleichzeitig von 9 Verbrauchern - 1.500.000 Ereignisse pro Sekunde.
Fehlertoleranzprüfung
Während des Tests wurden die folgenden Ergebnisse erhalten (3 Makler, 3 Tierpfleger).
- Eine abnormale Kündigung eines der Broker führt nicht zur Aussetzung oder Unzugänglichkeit des Clusters. Die Arbeit geht wie gewohnt weiter, aber die restlichen Makler haben eine große Last.
- Die abnormale Beendigung von zwei Brokern bei einem Cluster von drei Brokern und min.isr = 2 führt dazu, dass der Cluster nicht zum Schreiben, sondern zur Lesbarkeit zugänglich ist. Für den Fall min.isr = 1 steht der Cluster weiterhin zum Lesen und Schreiben zur Verfügung. Dieser Modus widerspricht jedoch der Forderung nach hoher Datensicherheit.
- Ein abnormales Beenden eines der Zookeeper-Server führt nicht zum Herunterfahren des Clusters oder zu Unzugänglichkeiten. Die Arbeit geht normal weiter.
- Eine abnormale Beendigung von zwei Zookeeper-Servern führt zu einer Unzugänglichkeit des Clusters, bis mindestens einer der Zookeeper-Server wiederhergestellt ist. Diese Aussage gilt für einen Zookeeper-Cluster mit 3 Servern. Infolgedessen wurde nach Recherchen beschlossen, den Zookeeper-Cluster auf 5 Server zu erhöhen, um die Fehlertoleranz zu erhöhen.
Kafka als Dienstleistung

Wir haben sichergestellt, dass Kafka eine hervorragende Technologie ist, mit der wir die für uns gestellten Aufgaben lösen können (Implementierung eines Nachrichtenbrokers). Trotzdem haben wir beschlossen, den Diensten den direkten Zugriff auf Kafka zu untersagen und es mit dem Datenbusdienst zu schließen. Warum haben wir das gemacht? Es gibt tatsächlich mehrere Gründe.
Der Datenbus übernahm alle Aufgaben im Zusammenhang mit der Integration in Kafka (Implementierung und Konfiguration von Verbrauchern und Herstellern, Überwachung, Alarmierung, Protokollierung, Skalierung usw.). Somit ist die Integration mit dem Nachrichtenbroker so einfach wie möglich.
Datenbus darf aus einer bestimmten Sprache oder Bibliothek abstrahieren, um mit Kafka zu arbeiten.
Der Datenbus ermöglichte es anderen Diensten, von der Speicherschicht zu abstrahieren. Vielleicht werden wir irgendwann Kafka in Pulsar ändern, und niemand wird etwas bemerken (alle Dienste wissen nur über die Datenbus-API Bescheid).
Der Datenbus übernahm die Validierung von Ereignisschemata.
Die Verwendung der Datenbusauthentifizierung ist implementiert.
Unter dem Deckmantel des Datenbusses können wir ohne Ausfallzeiten Kafka-Versionen diskret aktualisieren, Konfigurationen von Herstellern, Verbrauchern, Maklern usw. zentral durchführen.
Mit dem Datenbus konnten wir Funktionen hinzufügen, die nicht in Kafka enthalten sind (z. B. Themenprüfung, Überwachung von Anomalien im Cluster, Erstellen von DLQ usw.).
Mit dem Datenbus kann das Failover für alle Dienste zentral implementiert werden.
Um Ereignisse an den Nachrichtenbroker zu senden, verbinden Sie derzeit einfach eine kleine Bibliothek mit Ihrem Servicecode. Das ist alles. Sie haben die Möglichkeit, mit einer Codezeile zu schreiben, zu lesen und zu skalieren. Die gesamte Implementierung ist Ihnen verborgen, nur ein paar Sticks wie die Größe des Stapels ragen heraus. Unter der Haube erhöht der Datenbusdienst die erforderliche Anzahl von Produzenten- und Konsumenteninstanzen in Kubernetes und fügt ihnen die erforderliche Konfiguration hinzu, aber all dies ist für Ihren Dienst transparent.
Natürlich gibt es keine Silberkugel, und dieser Ansatz hat seine Grenzen.
- Der Datenbus muss im Gegensatz zu Bibliotheken von Drittanbietern eigenständig unterstützt werden.
- Der Datenbus erhöht die Anzahl der Interaktionen zwischen Diensten und dem Nachrichtenbroker, was zu einer geringeren Leistung im Vergleich zu nacktem Kafka führt.
- Nicht alles kann so einfach vor Diensten verborgen werden. Wir möchten nicht die Funktionalität von KSQL- oder Kafka-Streams im Datenbus duplizieren. Daher müssen Sie manchmal zulassen, dass Dienste direkt ausgeführt werden.
In unserem Fall überwogen die Vorteile die Nachteile, und die Entscheidung, den Nachrichtenbroker mit einem separaten Dienst abzudecken, war gerechtfertigt. Während des Betriebsjahres hatten wir keine ernsthaften Unfälle und Probleme.
PS Danke an meine Freundin Ekaterina Oblyalyaeva für die coolen Bilder zu diesem Artikel. Wenn Sie sie mochten, gibt es noch mehr Abbildungen.