Nicht nur Verarbeitung: Wie wir aus Kafka Streams eine verteilte Datenbank gemacht haben und was daraus entstanden ist

Hallo, Habr!

Wir erinnern daran, dass wir nach dem Buch über Kafka ein ebenso interessantes Werk über die Bibliothek Kafka Streams API.

Nicht nur Verarbeitung: Wie wir aus Kafka Streams eine verteilte Datenbank gemacht haben und was daraus entstanden ist

Während die Community gerade die Grenzen der Möglichkeiten dieses leistungsstarken Werkzeugs entdeckt, ist kürzlich ein Artikel erschienen, mit dessen Übersetzung wir Sie bekannt machen möchten. Der Autor berichtet aus eigener Erfahrung, wie man aus Kafka Streams ein verteiltes Datenspeicher erstellen kann. Viel Spaß beim Lesen!

Die Apache-Bibliothek Kafka Streams wird weltweit in Unternehmen für die verteilte Echtzeitverarbeitung über Apache Kafka eingesetzt. Einer der oft unterschätzten Aspekte dieses Frameworks ist, dass es ermöglicht, einen lokalen Zustand zu speichern, der auf der Grundlage der Streamverarbeitung erzeugt wird.

In diesem Artikel erkläre ich, wie es unserem Unternehmen gelungen ist, diese Gelegenheit gewinnbringend zu nutzen, während wir ein Produkt zur Sicherheit von Cloud-Anwendungen entwickelt haben. Mithilfe von Kafka Streams haben wir Microservices mit gemeinsamem Zustand erstellt, die jeweils als fehlertolerante und hochverfügbare Quelle zuverlässiger Informationen über den Status der Objekte im System dienen. Dies ist für uns ein Schritt nach vorn in Bezug auf Zuverlässigkeit und Wartungsfreundlichkeit.

Falls Sie an einem alternativen Ansatz interessiert sind, der es ermöglicht, eine zentrale Datenbank zur Unterstützung des formalen Zustands Ihrer Objekte zu nutzen – lesen Sie weiter, es könnte interessant sein...

Warum wir der Meinung waren, dass es an der Zeit ist, unsere Ansätze zum Management des gemeinsamen Zustands zu überdenken

Wir mussten den Zustand verschiedener Objekte basierend auf den Berichten von Agenten aufrechterhalten (zum Beispiel: Wurde die Website angegriffen?). Vor der Umstellung auf Kafka Streams waren wir oft auf eine zentrale Datenbank (+ Service-API) angewiesen, um den Zustand zu verwalten. Dieser Ansatz hat jedoch seine Nachteile: in datenintensiven Situationen Die Unterstützung von Konsistenz und Synchronisierung wird zu einer echten Herausforderung. Die Datenbank kann zum Engpass werden oder sich in einem Zustand der Race Conditions befinden und unter Unberechenbarkeit leiden.

Nicht nur Verarbeitung: Wie wir aus Kafka Streams eine verteilte Datenbank gemacht haben und was daraus entstanden ist

Abbildung 1: Ein typisches Szenario mit Zustandsteilung, das vor dem Übergang zu
Kafka und Kafka Streams auftrat: Die Agenten berichten ihre Zustände über eine API, und der aktualisierte Zustand wird über eine zentrale Datenbank berechnet.

Lernen Sie Kafka Streams kennen – es ist jetzt einfach, Mikroservices mit gemeinsamem Zustand zu erstellen.

Vor etwa einem Jahr haben wir beschlossen, unsere Szenarien für die Arbeit mit gemeinsamem Zustand gründlich zu überdenken, um diese Probleme zu lösen. Wir haben uns sofort entschieden, Kafka Streams auszuprobieren – bekannt für seine Skalierbarkeit, hohe Verfügbarkeit und Fehlertoleranz sowie seinen umfangreichen Streaming-Funktionalitäten (einschließlich Statusbehalten). Genau das, was wir benötigten, ganz zu schweigen von der Reife und Zuverlässigkeit des Messaging-Systems, das in Kafka entstanden ist.

Jeder von uns erstellte zustandsbehaftete Mikroservice wurde auf der Grundlage einer Kafka Streams-Instanz mit einer relativ einfachen Topologie aufgebaut. Diese bestand aus 1) einer Quelle, 2) einem Prozessor mit persistentem Schlüssel-Wert-Speicher und 3) einem Strom:

Nicht nur Verarbeitung: Wie wir aus Kafka Streams eine verteilte Datenbank gemacht haben und was daraus entstanden ist

Abbildung 2: Die standardmäßige Topologie unserer Streaming-Instanzen für zustandsbehaftete Mikroservices. Beachten Sie, dass es hier auch einen Speicher gibt, der Metadaten zur Planung enthält.

Bei diesem neuen Ansatz erstellen die Agenten Nachrichten, die in das ursprüngliche Thema eingegeben werden, während die Verbraucher – beispielsweise der Benachrichtigungsdienst – den berechneten gemeinsamen Zustand über den Strom (das Ausgangsthema) empfangen.

Nicht nur Verarbeitung: Wie wir aus Kafka Streams eine verteilte Datenbank gemacht haben und was daraus entstanden ist

Abbildung 3: Ein neues Beispiel für einen Aufgabenfluss für ein Szenario mit gemeinsamen Mikroservices: 1) Der Agent erzeugt eine Nachricht, die in das ursprüngliche Kafka-Thema gelangt; 2) der zustandsbehaftete Mikroservice (der Kafka Streams verwendet) verarbeitet sie und schreibt den berechneten Zustand in das endgültige Kafka-Thema; danach 3) empfangen die Verbraucher den neuen Zustand.

Hey, und dieser eingebaute Schlüssel-Wert-Speicher ist tatsächlich sehr nützlich!

Wie bereits erwähnt, enthält unsere Topologie mit gemeinsamem Zustand einen Schlüssel-Wert-Speicher. Wir haben mehrere Verwendungsmöglichkeiten gefunden, von denen zwei im Folgenden beschrieben werden.

Option #1: Verwendung des Schlüssel-Wert-Speichers bei Berechnungen

Unser erster Schlüssel-Wert-Speicher enthielt Hilfsdaten, die wir für Berechnungen benötigten. Zum Beispiel wurde in einigen Fällen der gemeinsame Zustand nach dem Prinzip „Mehrheitsentscheid“ bestimmt. Im Speicher konnten alle neuesten Berichte der Agenten über den Zustand eines bestimmten Objekts aufbewahrt werden. Wenn wir dann einen neuen Bericht von einem Agenten erhielten, konnten wir ihn speichern, die Berichte aller anderen Agenten über den Zustand desselben Objekts aus dem Speicher abrufen und die Berechnung wiederholen.
In Abbildung 4 unten ist zu sehen, wie wir den Zugriff auf den Schlüssel-Wert-Speicher für die verarbeitende Methode des Prozessors eröffneten, sodass eine neue Nachricht anschließend verarbeitet werden konnte.

Nicht nur Verarbeitung: Wie wir aus Kafka Streams eine verteilte Datenbank gemacht haben und was daraus entstanden ist

Abbildung 4: Zugriff auf den Schlüssel-Wert-Speicher für die verarbeitende Methode des Prozessors gewähren (danach muss in jedem Szenario, das mit gemeinsam genutztem Zustand arbeitet, die Methode implementiert werden) doProcess)

Option #2: Erstellung eines CRUD-API über Kafka Streams

Nachdem wir unseren grundlegenden Aufgabenstrom eingerichtet hatten, versuchten wir, ein RESTful CRUD-API für unsere Mikrodienste mit gemeinsam genutztem Zustand zu schreiben. Wir wollten in der Lage sein, den Zustand bestimmter oder aller Objekte abzurufen sowie den Zustand eines Objekts festzulegen oder zu entfernen (dies ist nützlich für die Unterstützung der Server-Seite).

Um alle API Get State zu unterstützen, lagerten wir den Zustand jedes Mal, wenn wir ihn bei der Verarbeitung neu berechnen mussten, für längere Zeit im eingebetteten Schlüssel-Wert-Speicher. In diesem Fall wird es recht einfach, ein solches API mit einer einzigen Instanz von Kafka Streams zu implementieren, wie im unten stehenden Listing gezeigt:

Nicht nur Verarbeitung: Wie wir aus Kafka Streams eine verteilte Datenbank gemacht haben und was daraus entstanden ist

Abbildung 5: Nutzung des eingebetteten Schlüssel-Wert-Speichers zur Abfrage des vorab berechneten Objektzustands

Die Aktualisierung des Objektstatus über die API lässt sich ebenfalls unkompliziert umsetzen. Im Grunde genommen muss dafür lediglich ein Kafka-Producer erstellt werden, der dann eine Aufzeichnung vornimmt, die den neuen Status enthält. Auf diese Weise wird sichergestellt, dass alle über die API generierten Nachrichten genau wie die von anderen Produzenten (z.B. Agents) verarbeitet werden.

Nicht nur Verarbeitung: Wie wir aus Kafka Streams eine verteilte Datenbank gemacht haben und was daraus entstanden ist

Abbildung 6: Den Objektstatus können Sie mithilfe eines Kafka-Producers festlegen.

Eine kleine Komplikation: Kafka hat zahlreiche Partitionen.

Im weiteren Verlauf hatten wir das Ziel, die mit der Verarbeitung verbundene Last zu verteilen und die Verfügbarkeit zu verbessern, indem wir für jedes Szenario ein Cluster von Mikrodiensten mit gemeinsamem Status bereitstellen. Die Einrichtung war erstaunlich einfach: Nachdem wir alle Instanzen konfiguriert hatten, sodass sie mit derselben Anwendungs-ID (und denselben Boot-Servern) arbeiteten, geschah fast alles automatisch. Wir hatten zudem festgelegt, dass jedes ursprüngliche Thema aus mehreren Partitionen bestehen wird, damit jeder Instanz eine Teilmenge dieser Partitionen zugewiesen werden kann.

Ich möchte auch erwähnen, dass es hier üblich ist, ein Zustandsspeicher-Backup zu erstellen, um beispielsweise im Falle einer Wiederherstellung nach einem Ausfall diese Kopie auf eine andere Instanz zu übertragen. Für jeden Zustandsspeicher in Kafka Streams wird ein replizierbares Topic mit einem Änderungsprotokoll erstellt (in dem lokale Updates verfolgt werden). Dadurch sichert Kafka kontinuierlich den Zustandsspeicher ab. Daher kann im Falle eines Ausfalls einer bestimmten Kafka Streams-Instanz der Zustandsspeicher schnell auf eine andere Instanz wiederhergestellt werden, wo die entsprechenden Partitionen übertragen werden. Unsere Tests haben gezeigt, dass dies innerhalb von Sekunden erfolgt, selbst wenn Millionen von Einträgen im Zustandsspeicher vorhanden sind.

Der Übergang von einem Mikroservice mit gemeinsamem Zustand zu einem Cluster von Mikroservices macht die Implementierung der Get State API nicht trivial. In der neuen Situation enthält der Zustandsspeicher jedes Mikroservices nur einen Teil des Gesamtbildes (nämlich die Objekte, deren Schlüssel einer bestimmten Partition zugeordnet waren). Wir mussten feststellen, auf welchem Instanz der Zustand des benötigten Objekts gespeichert war, und das geschah anhand der Metadaten der Streams, wie unten gezeigt:

Nicht nur Verarbeitung: Wie wir aus Kafka Streams eine verteilte Datenbank gemacht haben und was daraus entstanden ist

Abbildung 7: Mithilfe von Metadaten der Streams bestimmen wir, von welcher Instanz wir den Zustand des benötigten Objekts abfragen; ein ähnlicher Ansatz wurde auch beim GET ALL API angewendet.

Wesentliche Erkenntnisse

Zustandsspeicher in Kafka Streams können de facto als verteilte Datenbank fungieren,

  • die kontinuierlich in Kafka repliziert wird.
  • Auf ein solches System lässt sich leicht ein CRUD API aufbauen.
  • Die Verarbeitung mehrerer Partitionen gestaltet sich etwas komplexer.
  • Es ist auch möglich, ein oder mehrere Zustandsspeicher in die Stream-Topologie aufzunehmen, um Hilfsdaten zu speichern. Diese Option kann verwendet werden für:
  • Langfristige Speicherung von Daten, die für Berechnungen während der Streamverarbeitung benötigt werden.
  • Langfristige Datenspeicherung, die bei der nächsten Initiierung einer Stream-Instanz von Nutzen sein kann.
  • und vieles mehr …

Dank dieser und weiterer Vorteile eignet sich Kafka Streams hervorragend zur Unterstützung des globalen Zustands in einem verteilten System wie unserem. Kafka Streams hat sich in der Produktion als sehr zuverlässig erwiesen (seit der Bereitstellung haben wir praktisch keine Nachrichten verloren), und wir sind uns sicher, dass sich ihre Möglichkeiten nicht darauf beschränken!

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