Erfahrung in der Entwicklung des Refund Tool-Dienstes mit asynchroner API auf Kafka

Was könnte ein so großes Unternehmen wie Lamoda, mit einem optimierten Prozess und Dutzenden miteinander verbundener Dienste, dazu bewegen, seinen Ansatz erheblich zu ändern? Die Motivation kann ganz unterschiedlich sein: von gesetzlichen Vorgaben bis hin zum typischen Wunsch von Programmierern, Neues auszuprobieren.

Das bedeutet jedoch nicht, dass man nicht mit zusätzlichen Vorteilen rechnen kann. Was konkret gewonnen werden kann, wenn ein events-driven API auf Kafka implementiert wird, erklärt Sergey Zaika (fewald). Auch über Fehler und interessante Entdeckungen wird es unbedingt gehen – Experimente kommen nicht ohne sie aus.

Erfahrung in der Entwicklung des Refund Tool-Dienstes mit asynchroner API auf Kafka

Haftungsausschluss: Dieser Artikel basiert auf Materialien des Meetups, das Sergey im November 2018 auf HighLoad++ gehalten hat. Die praktischen Erfahrungen von Lamoda mit Kafka zogen die Zuhörer nicht weniger an als andere Vorträge im Programm. Wir glauben, dass dies ein hervorragendes Beispiel dafür ist, wie wichtig es ist, Gleichgesinnte zu finden, und dass die Organisatoren von HighLoad++ auch weiterhin bestrebt sein werden, eine einladende Atmosphäre dafür zu schaffen.

Über den Prozess

Lamoda — eine große E-Commerce-Plattform mit eigenem Kontaktzentrum, Lieferservice (und zahlreichen Partnern), Fotostudio und einem riesigen Lager, alles betrieben mit eigener Software. Es gibt Dutzende von Zahlungsmethoden, B2B-Partner, die einen Teil oder alle diese Dienstleistungen nutzen können und aktuelle Informationen zu ihren Produkten benötigen. Darüber hinaus ist Lamoda in drei Ländern neben Russland aktiv, und dort läuft nicht alles ganz nach dem gleichen Muster. Insgesamt gibt es wahrscheinlich mehr als hundert Möglichkeiten, eine neue Bestellung zu konfigurieren, die auf verschiedene Weise bearbeitet werden muss. All dies funktioniert mithilfe von Dutzenden von Diensten, die sich manchmal auf nicht offensichtliche Weise austauschen. Es gibt auch ein zentrales System, dessen Hauptaufgabe die Bestellstatusverwaltung ist. Wir nennen es BOB, und ich arbeite damit.

Refund Tool mit einem ereignisgesteuerten API

Der Begriff ereignisgesteuert ist etwas abgedroschen; weiter unten werden wir genauer definieren, was damit gemeint ist. Ich beginne mit dem Kontext, in dem wir beschlossen haben, den Ansatz der ereignisgesteuerten API in Kafka auszuprobieren.

Erfahrung in der Entwicklung des Refund Tool-Dienstes mit asynchroner API auf Kafka

In jedem Geschäft gibt es neben den Käufen, für die Kunden bezahlen, Momente, in denen das Geschäft Rückerstattungen vornehmen muss, weil das Produkt nicht den Erwartungen des Kunden entspricht. Dieser vergleichsweise kurze Prozess umfasst die Klärung von Informationen, falls notwendig, und die Durchführung der Rücküberweisung.

Aber die Rückerstattung wurde aufgrund von Gesetzesänderungen komplizierter, und wir mussten dafür einen separaten Mikrodienst implementieren.

Erfahrung in der Entwicklung des Refund Tool-Dienstes mit asynchroner API auf Kafka

Unsere Motivation:

  1. Gesetz FZ-54 – kurz gesagt, das Gesetz verlangt, dass jede Geldtransaktion, sei es eine Rückerstattung oder ein Zahlungseingang, innerhalb eines relativ kurzen Zeitrahmens von wenigen Minuten dem Finanzamt gemeldet wird. Als E-Commerce-Unternehmen führen wir eine Vielzahl von Transaktionen durch. Technisch bedeutet das eine neue Verantwortung (und somit einen neuen Dienst) sowie Anpassungen in allen beteiligten Systemen.
  2. BOB Split – ein internes Projekt des Unternehmens, um BOB von einer großen Anzahl nicht relevanter Verantwortlichkeiten zu befreien und die Komplexität insgesamt zu reduzieren.

Erfahrung in der Entwicklung des Refund Tool-Dienstes mit asynchroner API auf Kafka

In diesem Diagramm sind die Hauptsysteme von Lamoda dargestellt. Momentan besteht der Großteil davon eher aus einem Sternenbild von 5-10 Mikrodiensten um einen schrumpfenden Monolithen.. Sie wachsen langsam, aber wir bemühen uns, sie kleiner zu halten, da das Deployment eines zentralen Elements riskant ist – wir dürfen nicht zulassen, dass es ausfällt. Alle Exchanges (Pfeile) müssen wir reservieren und uns darauf einstellen, dass einer von ihnen möglicherweise nicht verfügbar ist.

Auch in BOB gibt es eine Menge Exchanges: Zahlungssysteme, Versand, Benachrichtigungen usw.

Technisch gesehen ist BOB:

  • ~150k Codezeilen + ~100k Testzeilen;
  • php7.2 + Zend 1 & Symfony Components 3;
  • >100 APIs & ~50 ausgehende Integrationen;
  • 4 Länder mit ihrer eigenen Geschäftslogik.

Das Deployment von BOB ist teuer und kompliziert. Die Menge an Code und die Probleme, die er löst, sind so groß, dass niemand ihn im Ganzen im Kopf haben kann. Es gibt also viele Gründe, ihn zu vereinfachen.

Rückerstattungsprozess

Ursprünglich sind zwei Systeme am Prozess beteiligt: BOB und Payment. Jetzt kommen noch zwei hinzu:

  • Der Fiscalization Service, der sich um die Probleme der Fiskalisierung und die Kommunikation mit externen Diensten kümmert.
  • Das Refund Tool, in das einfach neue Exchanges eingebracht werden, um BOB nicht unnötig aufzublähen.

Jetzt sieht der Prozess so aus:

Erfahrung in der Entwicklung des Refund Tool-Dienstes mit asynchroner API auf Kafka

  1. BOB erhält eine Anfrage zur Rückerstattung von Geld.
  2. BOB informiert darüber das Refund Tool.
  3. Das Refund Tool sagt Payment: „Gib das Geld zurück.“
  4. Payment erstattet das Geld.
  5. Das Refund Tool und BOB synchronisieren ihre Status, weil sie derzeit beide darauf angewiesen sind. Wir sind noch nicht bereit, vollständig auf das Refund Tool umzusteigen, da BOB eine Benutzeroberfläche, Berichte für die Buchhaltung und viele Daten bietet, die sich nicht einfach übertragen lassen. So müssen wir auf zwei Stühlen sitzen.
  6. Es wird eine Anfrage zur Fiskalisierung gesendet.

Letztendlich haben wir eine Art von Ereignisbus auf Kafka erstellt, auf dem alles basiert. Hurra, jetzt haben wir einen einzigen Ausfallpunkt (Sarkasmus).

Erfahrung in der Entwicklung des Refund Tool-Dienstes mit asynchroner API auf Kafka

Die Vor- und Nachteile sind ziemlich offensichtlich. Wir haben den Bus gebaut, was bedeutet, dass jetzt alle Dienste davon abhängen. Das vereinfacht das Design, bringt aber einen einzigen Ausfallpunkt ins System. Wenn Kafka ausfällt, steht der Prozess still.

Was ist ein ereignisgesteuertes API?

Eine gute Antwort auf diese Frage finden Sie im Vortrag von Martin Fowler (GOTO 2017). „Die vielen Bedeutungen der ereignisgesteuerten Architektur“.

Kurz gesagt, was wir gemacht haben:

  1. Wir haben alle asynchronen Austauschprozesse über Events Storage. Anstatt jedem interessierten Verbraucher im Netzwerk über eine Statusänderung zu informieren, schreiben wir ein Ereignis über den Statuswechsel in einen zentralen Speicher, und interessierte Verbraucher lesen alles, was dort erscheint.
  2. Ein Ereignis (event) ist in diesem Fall eine Benachrichtigung (Benachrichtigungen) darüber, dass sich etwas irgendwo geändert hat. Zum Beispiel könnte sich der Status einer Bestellung geändert haben. Der Verbraucher, der an bestimmten ergänzenden Informationen zum Statuswechsel interessiert ist und die in der Benachrichtigung nicht enthalten sind, kann den Status selbst überprüfen.
  3. Die umfangreichste Variante ist vollständiges Event Sourcing, State Transfer, bei dem das Ereignis alle Informationen enthält, die für die Verarbeitung erforderlich sind: von wo und in welchen Status gewechselt wurde, wie sich die Daten genau verändert haben usw. Die Frage ist nur die Zweckmäßigkeit und der Umfang der Informationen, die Sie speichern können.

Im Rahmen der Einführung des Refund Tools haben wir die dritte Variante verwendet. Dies erleichterte die Verarbeitung von Ereignissen, da keine detaillierten Informationen abgerufen werden mussten, und es wurde das Szenario ausgeschlossen, in dem jedes neue Ereignis eine Welle von anfragenden GET-Anfragen von Verbrauchern auslöst.

Der Refund Tool-Service ist nicht belastet, daher ist Kafka hier eher ein Test als eine Notwendigkeit. Ich denke nicht, dass, wenn der Rückerstattungsdienst zu einem Highload-Projekt werden würde, das Unternehmen glücklich wäre.

Async Exchange AS IS

Für asynchrone Austauschprozesse nutzt die PHP-Abteilung in der Regel RabbitMQ. Wir haben die Daten für die Anfrage gesammelt, in die Warteschlange gelegt, und der Verbraucher dieses Dienstes hat sie gelesen und gesendet (oder auch nicht gesendet). Für die API nutzt Lamoda aktiv Swagger. Wir entwerfen die API, beschreiben sie in Swagger und generieren den Client- und Server-Code. Außerdem verwenden wir eine leicht erweiterte JSON RPC 2.0.

An einigen Stellen werden ESB-Busse verwendet, manche arbeiten mit ActiveMQ, aber insgesamt ist RabbitMQ der Standard..

Async exchange TO BE

Beim Entwerfen des Austauschs über den Events-Bus zeigt sich eine Analogie. Wir beschreiben den zukünftigen Datenaustausch ähnlich durch die Beschreibung der Struktur des Events. Das Format ist YAML, die Codegenerierung mussten wir selbst durchführen, der Generator erstellt DTOs gemäß der Spezifikation und lehrt die Clients und Server, mit ihnen zu arbeiten. Die Generierung erfolgt in zwei Sprachen - Golang und PHP.Das ermöglicht es, die Bibliotheken konsistent zu halten. Der Generator ist in Golang geschrieben und trägt den Namen gogi.

Event-Sourcing mit Kafka ist ein typisches Vorgehen. Es gibt eine Lösung von der Hauptunternehmensversion Kafka Confluent, es gibt Nakadi,eine Lösung von unseren "Brüdern" im Domain-Bereich Zalando. Unsere Motivation, mit Vanilla Kafka zu beginnen. — das ist, um die Lösung kostenlos zu lassen, bis wir endgültig entschieden haben, ob wir sie weit verbreitet nutzen werden, und um uns Spielraum für Anpassungen zu lassen: wir möchten Unterstützung für unser JSON RPC 2.0, Generatoren für zwei Sprachen, und wir werden sehen, was sonst noch.

Ironischerweise können wir, selbst wenn es ein ähnliches Geschäft wie Zalando gibt, das eine ähnliche Lösung entwickelt hat, diese nicht effektiv nutzen.

Architektonisch ist das Muster beim Start so: Wir lesen direkt aus Kafka, aber schreiben nur über den Events-Bus. Für das Lesen in Kafka gibt es viel Fertiges: Broker, Load-Balancer und es ist mehr oder weniger bereit für horizontale Skalierung, das wollten wir beibehalten. Das Schreiben wollten wir jedoch über einen Gateway, auch bekannt als Events-Bus, umsetzen, und das ist der Grund.

Events-Bus

Oder Event-Bus. Das ist einfach ein stateless HTTP-Gateway, das mehrere wichtige Rollen übernimmt:

  • Validierung des Produzierens — wir überprüfen, ob die Ereignisse unserer Spezifikation entsprechen.
  • Master-System für Ereignisse, also dies ist das Haupt- und einzige System im Unternehmen, das beantwortet, welche Events mit welchen Strukturen als gültig gelten. Die Validierung umfasst einfach Datentypen und Enums zur strengen Spezifikation des Inhalts.
  • Hash-Funktion für Sharding – die Struktur der Kafka-Nachricht ist key-value und basierend auf dem Hash des Keys wird berechnet, wo dies abgelegt werden soll.

Warum

Wir arbeiten in einem großen Unternehmen mit einem etablierten Prozess. Warum etwas ändern? Dies ist ein Experiment, und wir rechnen damit, mehrere Vorteile zu erzielen.

1:n+1-Austausch (ein zu vielen)

Mit Kafka ist es sehr einfach, neue Verbraucher an die API anzuschließen.

Angenommen, Sie haben ein Verzeichnis, das in mehreren Systemen gleichzeitig aktuell gehalten werden muss (und in einigen neuen). Früher haben wir ein Bundle erfunden, das ein Set-API implementierte, und der Master-System sagte die Adressen der Verbraucher. Jetzt sendet das Master-System Updates in das Topic, und alle, die interessiert sind, lesen. Es gibt ein neues System – wir haben es auf das Topic angemeldet. Ja, ebenfalls ein Bundle, aber einfacher.

Im Fall des Refund-Tools, das einen Teil von BOB darstellt, ist es für uns praktisch, diese über Kafka synchronisiert zu halten. Die Zahlungsabwicklung teilt mit, dass das Geld zurückerstattet wurde: BOB und RT erfahren darüber, ändern ihre Status, und der Fiscalization Service wird informiert und erzeugt den Beleg.

Erfahrung in der Entwicklung des Refund Tool-Dienstes mit asynchroner API auf Kafka

Wir planen, einen einheitlichen Notifications Service zu schaffen, der die Kunden über Neuigkeiten zu ihren Bestellungen/Rückgaben informiert. Momentan ist diese Verantwortung auf mehrere Systeme verteilt. Es reicht aus, den Notifications Service zu schulen, damit er relevante Informationen aus Kafka abgreift und darauf reagiert (und diese Benachrichtigungen in anderen Systemen zu deaktivieren). Es sind keine neuen direkten Austauschmechanismen erforderlich.

Datengetrieben

Die Informationen zwischen den Systemen werden transparent – egal welches „blutige Unternehmen“ Sie haben oder wie umfangreich Ihr Backlog ist. Bei Lamoda gibt es eine Abteilung für Datenanalyse, die Daten aus den Systemen sammelt und sie in wiederverwendbare Formate für sowohl das Geschäft als auch die intelligenten Systeme bringt. Kafka ermöglicht es, ihnen schnell eine große Menge an Daten bereitzustellen und diesen Informationsfluss aktuell zu halten.

Replikationsprotokoll

Nach dem Lesen verschwinden Nachrichten nicht, wie bei RabbitMQ. Wenn ein Ereignis genügend Informationen zur Verarbeitung enthält, haben wir eine Historie der letzten Änderungen am Objekt und, falls gewünscht, die Möglichkeit, diese Änderungen anzuwenden.

Die Aufbewahrungsfrist für das Replikationsprotokoll hängt von der Schreibintensität in diesem Thema ab. Kafka ermöglicht eine flexible Konfiguration der Speicherkapazitäten sowohl zeitlich als auch hinsichtlich des Datenvolumens. Für stark frequentierte Themen ist es wichtig, dass alle Verbraucher die Informationen rechtzeitig lesen, bevor sie verschwinden, selbst im Falle einer kurzfristigen Ausfallzeit. In der Regel können Daten über einige Tagegespeichert werden, was für den Support völlig ausreichend ist.

Erfahrung in der Entwicklung des Refund Tool-Dienstes mit asynchroner API auf Kafka

Nun folgt eine kurze Zusammenfassung der Dokumentation für diejenigen, die mit Kafka nicht vertraut sind (das Bild stammt ebenfalls aus der Dokumentation).

In AMQP gibt es Warteschlangen: Wir schreiben Nachrichten in eine Warteschlange für den Verbraucher. In der Regel verarbeitet ein System mit derselben Geschäftslogik eine Warteschlange. Wenn mehrere Systeme benachrichtigt werden müssen, kann die Anwendung so konfiguriert werden, dass sie in mehrere Warteschlangen schreibt oder einen Exchange mit einem Fanout-Mechanismus einrichtet, der sie selbst klont.

In Kafka gibt es eine ähnliche Abstraktion Thema, in die Sie Nachrichten schreiben, die jedoch nach dem Lesen nicht verschwinden. Standardmäßig erhalten Sie beim Anschluss an Kafka alle Nachrichten und haben die Möglichkeit, den Punkt zu speichern, an dem Sie aufgehört haben. Das heißt, Sie lesen sequenziell und können eine Nachricht ungesehen lassen, aber die ID speichern, von der aus Sie dann weiterlesen. Die ID, an der Sie aufgehört haben, wird als Offset bezeichnet, und der Mechanismus ist das Commit-Offset.

Dementsprechend können verschiedene Logiken implementiert werden. Beispielsweise existiert BOB in 4 Instanzen für verschiedene Länder – Lamoda ist in Russland, Kasachstan, der Ukraine und Weißrussland verfügbar. Da sie getrennt bereitgestellt werden, haben sie leicht unterschiedliche Konfigurationen und ihre eigene Geschäftslogik. Wir geben in der Nachricht an, auf welches Land sie sich bezieht. Jeder BOB-Consumer in jedem Land liest mit unterschiedlichen groupIds, und wenn eine Nachricht nicht für ihn relevant ist, wird sie übersprungen, d.h. der Offset wird sofort um +1 committed. Wenn dasselbe Thema von unserem Zahlungsdienst gelesen wird, geschieht dies mit einer separaten Gruppe, sodass sich die Offsets nicht überschneiden.

Anforderungen an Ereignisse:

  • Vollständigkeit der Daten. Ich wünschte, dass das Ereignis genügend Daten enthält, um verarbeitet werden zu können.

  • Integrität. Wir delegieren die Überprüfung der Konsistenz des Ereignisses an den Events-Bus, der es verarbeiten kann.
  • Die Reihenfolge ist wichtig. Im Falle von Rücksendungen müssen wir mit der Historie arbeiten. Bei Benachrichtigungen ist die Reihenfolge nicht wichtig, solange es sich um einheitliche Benachrichtigungen handelt; die E-Mail bleibt gleich, unabhängig davon, welcher Auftrag zuerst kommt. Bei Rücksendungen gibt es einen klaren Prozess, und wenn man die Reihenfolge ändert, können Ausnahmen auftreten, und die Rückerstattung wird möglicherweise nicht erstellt oder verarbeitet – wir gelangen in einen anderen Status.
  • Konsistenz. Wir haben ein Repository und erstellen jetzt anstelle einer API Ereignisse. Wir benötigen eine Möglichkeit, schnell und kostengünstig Informationen über neue Ereignisse und Änderungen an bestehenden in unsere Dienste zu übertragen. Dies wird durch eine gemeinsame Spezifikation in einem separaten Git-Repository und Code-Generatoren erreicht. Dadurch sind unsere Kunden- und Serverdienste konsistent.

Kafka bei Lamoda

Wir haben drei Kafka-Installationen:

  1. Logs;
  2. Forschung und Entwicklung;
  3. Events-Bus.

Heute sprechen wir ausschließlich über den letzten Punkt. In unserem Events-Bus haben wir keine großen Installationen – 3 Broker (Server) und insgesamt 27 Topics. In der Regel entspricht ein Topic einem Prozess. Aber das ist ein feiner Punkt, und darauf werden wir jetzt eingehen.

Erfahrung in der Entwicklung des Refund Tool-Dienstes mit asynchroner API auf Kafka

Oben sehen Sie das rps-Diagramm. Der Refund-Prozess ist durch eine türkisfarbene Linie markiert (ja, die, die auf der X-Achse liegt), und der Inhaltsaktualisierungsprozess ist durch eine rosafarbene Linie angezeigt.

Der Katalog von Lamoda enthält Millionen von Artikeln, und die Daten werden ständig aktualisiert. Einige Kollektionen geraten aus der Mode, während neue herausgebracht werden, und im Katalog tauchen ständig neue Modelle auf. Wir versuchen vorherzusagen, was unseren Kunden morgen gefallen wird, weshalb wir ständig neue Artikel einkaufen, diese fotografieren und das Schaufenster aktualisieren.

Die rosafarbenen Spitzen repräsentieren Produktupdates, also Änderungen an den Artikeln. Man sieht, dass die Leute fotografiert haben, fotografiert haben, und dann plötzlich! – eine Flut von Ereignissen hochgeladen wurde.

Lamoda Events Anwendungsfälle

Die aufgebaute Architektur verwenden wir für folgende Operationen:

  • Verfolgung des Rückgabestatus: Call-to-Action und Statusverfolgung aller beteiligten Systeme. Zahlungen, Status, Fiscalitätsprüfung, Benachrichtigungen. Hier haben wir einen Ansatz ausprobiert, Werkzeuge entwickelt, alle Fehler gesammelt, Dokumentation geschrieben und unseren Kollegen erklärt, wie man sie verwendet.
  • Aktualisierung der Produktkarten: Konfiguration, Metadaten, Spezifikationen. Eine System liest (das angezeigt wird), mehrere schreiben.
  • E-Mail, Push und SMS.: Bestellung zusammengestellt, Bestellung angekommen, Rücksendung akzeptiert usw., viele davon.
  • Lagerbestand, Aktualisierung des Bestands. ‐ quantitative Aktualisierung von Artikeln, einfache Zahlen: Eingang im Lager, Rücksendung. Es ist wichtig, dass alle Systeme, die mit der Reservierung von Produkten verbunden sind, mit den aktuellsten Daten arbeiten. Momentan ist das System zur Bestandsaktualisierung recht komplex, Kafka wird helfen, es zu vereinfachen.
  • Datenanalyse (F&E-Abteilung), ML-Tools, Analytik, Statistik. Wir möchten, dass Informationen transparent sind – dafür eignet sich Kafka gut.

Jetzt der spannendere Teil über die gemachten Fehler und interessanten Entdeckungen, die in den letzten sechs Monaten gemacht wurden.

Entwurfsprobleme

Angenommen, wir möchten etwas Neues machen – zum Beispiel den gesamten Lieferprozess auf Kafka umzustellen. Momentan wird ein Teil des Prozesses im Order Processing in BOB umgesetzt. Hinter der Auftragsübergabe an den Lieferservice, dem Transport zum Zwischenlager und weiteren Aspekten steht ein Statusmodell. Es gibt ein ganzes Monolith, sogar zwei, plus eine Menge APIs, die sich mit der Lieferung beschäftigen. Sie wissen viel mehr über die Lieferung.

Es scheint, dass es sich um ähnliche Bereiche handelt, aber für das Order Processing in BOB und für das Liefersystem unterscheiden sich die Status. Einige Kurierdienste kommunizieren keine Zwischenstatus, sondern nur die endgültigen: „zugestellt“ oder „verloren“. Andere hingegen berichten sehr detailliert über den Transport des Produkts. Jeder hat seine eigenen Validierungsregeln: Für manche ist eine gültige E-Mail ausreichend, um sie zu bearbeiten; für andere ist sie nicht gültig, aber die Bestellung wird trotzdem bearbeitet, weil es eine Telefonnummer gibt, um Kontakt aufzunehmen, während jemand anderes sagt, dass solche Bestellungen gar nicht bearbeitet werden.

Datenstrom

Im Fall von Kafka stellt sich die Frage der Organisation des Datenflusses. Diese Aufgabe hängt mit der Wahl einer Strategie in mehreren Punkten zusammen; lassen Sie uns alle durchgehen.

In ein Thema oder in verschiedene?

Wir haben eine Ereignisspezifikation. In BOB notieren wir, dass eine bestimmte Bestellung geliefert werden muss, und geben an: die Bestellnummer, ihren Inhalt, einige SKUs und Barcodes usw. Wenn die Ware im Lager ankommt, kann die Lieferung Status, Zeitstempel und alles andere, was benötigt wird, erhalten. Aber dann möchten wir in BOB Updates zu diesen Daten erhalten. Dadurch entsteht ein umgekehrter Prozess zum Abrufen von Daten aus der Lieferung. Ist das dasselbe Ereignis? Oder ist es ein separater Austausch, der ein eigenes Thema verdient?

Wahrscheinlich werden sie sehr ähnlich sein, und die Versuchung, ein einziges Thema zu erstellen, ist nicht unbegründet, denn ein separates Thema bedeutet separate Verbraucher, separate Konfigurationen und eine separate Generierung von all dem. Aber das ist nicht sicher.

Neues Feld oder neues Ereignis?

Aber wenn wir die gleichen Ereignisse verwenden, entsteht ein anderes Problem. Beispielsweise können nicht alle Liefersysteme ein solches DTO erzeugen, das BOB generieren kann. Wir senden ihnen die ID, und sie speichern sie nicht, weil sie sie nicht benötigen, aber aus der Sicht des Startprozesses des Event-Bus ist dieses Feld zwingend erforderlich.

Wenn wir für den Event-Bus die Regel festlegen, dass dieses Feld obligatorisch ist, müssen wir entweder im BOB oder im Handler des Start-Events zusätzliche Validierungsregeln aufstellen. Die Validierung beginnt sich im gesamten Dienst auszubreiten, was nicht sehr praktisch ist.

Ein weiteres Problem ist die Versuchung zur inkrementellen Entwicklung. Man sagt uns, dass wir etwas zum Event hinzufügen müssen und vielleicht, wenn wir gut darüber nachdenken, hätte es ein eigenes Event sein sollen. In unserem Schema ist ein eigenes Event jedoch ein eigenes Topic. Ein eigenes Topic umfasst den gesamten Prozess, den ich oben beschrieben habe. Der Entwickler hat die Versuchung, einfach ein weiteres Feld in das JSON-Schema einzufügen und neu zu generieren.

Im Fall von Refunds haben wir innerhalb von einem halben Jahr zu einem Ereignis von Ereignissen gelangt. Wir hatten ein Meta-Ereignis, das als Refund-Update bezeichnet wird und ein Feld vom Typ enthält, das beschreibt, worin eigentlich dieses Update besteht. Darauf basierend hatten wir "prächtig" funktionierende Switches mit Validatoren, die angaben, wie dieses Event mit diesem Typ validiert werden sollte.

Versionierung von Ereignissen

Zur Validierung von Nachrichten in Kafka kann man nutzen Avro, aber man hätte sofort dafür planen und Confluent nutzen müssen. In unserem Fall bezüglich der Versionierung müssen wir vorsichtig sein. Es wird nicht immer möglich sein, die Nachrichten aus dem Replikationsprotokoll erneut zu lesen, da das Modell „weggegangen“ ist. Es gelingt uns meist, Versionen so zu erstellen, dass das Modell abwärtskompatibel ist: zum Beispiel, indem wir ein Feld vorübergehend optional machen. Wenn die Unterschiede zu groß sind, beginnen wir, in ein neues Thema zu schreiben, und migrieren die Kunden, wenn sie das alte gelesen haben.

Garantie der Lesereihenfolge für Partitions

Topics innerhalb von Kafka sind in Partitions aufgeteilt. Das ist nicht sehr wichtig, solange wir die Entitäten und den Austausch entwerfen, aber entscheidend, wenn wir bestimmen, wie wir konsumieren und skalieren.

In der Regel schreiben Sie in Kafka ein Topic. Standardmäßig wird eine Partition verwendet, und alle Nachrichten dieses Topics landen in ihr. Der Consumer liest diese Nachrichten dann der Reihe nach. Angenommen, es ist erforderlich, das System zu erweitern, damit zwei verschiedene Consumer die Nachrichten lesen können. Wenn Sie beispielsweise eine SMS senden, können Sie Kafka anweisen, eine zusätzliche Partition zu erstellen, sodass Kafka die Nachrichten in zwei Teile aufteilt – die eine Hälfte hierhin, die andere dorthin.

Wie teilt Kafka sie auf? Jede Nachricht hat einen Body (in dem wir JSON speichern) und einen Key. Zu diesem Schlüssel kann eine Hash-Funktion angewendet werden, die bestimmt, in welche Partition die Nachricht gelangt.

In unserem Fall mit Refunds ist das wichtig. Wenn wir zwei Partitionen haben, besteht die Möglichkeit, dass der parallele Consumer das zweite Ereignis früher verarbeitet als das erste, was problematisch sein könnte. Die Hash-Funktion gewährleistet, dass Nachrichten mit demselben Schlüssel in dieselbe Partition gelangen.

Events vs. Commands

Dies ist ein weiteres Problem, mit dem wir konfrontiert sind. Ein Event ist ein Ereignis: wir sagen, dass etwas irgendwo passiert ist (something_happened), zum Beispiel, dass ein Artikel storniert oder eine Rückerstattung erfolgt ist. Wenn jemand diese Ereignisse anhört, wird bei "Artikel storniert" eine Rückerstattung geschaffen, und "es hat eine Rückerstattung gegeben" wird irgendwo in den Setups aufgezeichnet.

Aber normalerweise, wenn Sie Ereignisse entwerfen, wollen Sie sie nicht umsonst schreiben – Sie gehen davon aus, dass sie jemand lesen wird. Der Anreiz, nicht something_happened (item_canceled, refund_refunded) zu schreiben, sondern something_should_be_done, ist groß. Zum Beispiel, der Artikel ist bereit zur Rückgabe.

Einerseits gibt es Hinweise darauf, wie das Ereignis verwendet werden wird. Andererseits sieht es viel weniger nach einem normalen Ereignisnamen aus. Außerdem ist es von hier nicht weit zu einem Befehl do_something. Aber Sie haben keine Garantie, dass dieses Ereignis jemand gelesen hat; und wenn es jemand gelesen hat, dann erfolgreich; und wenn es erfolgreich gelesen wurde, hat er etwas getan, und dieses Etwas war erfolgreich. In dem Moment, in dem das Ereignis zu do_something wird, ist Feedback notwendig, und das ist das Problem.

Erfahrung in der Entwicklung des Refund Tool-Dienstes mit asynchroner API auf Kafka

Im asynchronen Austausch in RabbitMQ, wenn Sie eine Nachricht gelesen haben und eine HTTP-Anfrage gestellt haben, erhalten Sie eine Rückmeldung — mindestens, dass die Nachricht akzeptiert wurde. Wenn Sie in Kafka etwas aufgezeichnet haben, gibt es eine Nachricht, dass Sie in Kafka geschrieben haben, aber wie es verarbeitet wurde, bleibt unklar.

Deshalb mussten wir in unserem Fall ein Antwortereignis einführen und das Monitoring so einstellen, dass wenn eine bestimmte Anzahl von Ereignissen innerhalb eines bestimmten Zeitrahmens auftritt, auch die gleiche Anzahl von Antwortereignissen eintreffen sollte. Wenn das nicht passiert, sieht es so aus, als wäre etwas schiefgegangen. Beispielsweise, wenn wir das Ereignis 'item_ready_to_refund' senden, erwarten wir, dass eine Rückerstattung erstellt wird, das Geld an den Kunden zurückgegeben wird und wir das Ereignis 'money_refunded' erhalten. Aber das ist nicht sicher, daher ist Monitoring erforderlich.

Nuancen

Es gibt ein ziemlich offensichtliches Problem: Wenn Sie sequenziell von einem Thema lesen und eine meiner Nachrichten schlecht ist, fällt der Consumer aus, und Sie kommen nicht weiter. Sie müssen alle Consumer stoppen, den Offset weiter committen, um fortzufahren.

Wir wussten darüber Bescheid, hatten damit gerechnet, und dennoch ist es passiert. Es passierte, weil das Ereignis aus der Sicht des Event-Busses gültig war, das Ereignis aus der Sicht des Anwendungsvalidators gültig war, aber es war nicht aus der Sicht von PostgreSQL gültig, da wir in einem System MySQL mit UNSIGNED INT hatten, während im neu geschriebenen System PostgreSQL nur INT verwendet wurde. Letzteres hat eine etwas kleinere Größe, und die ID passte nicht. Symfony ist mit einer Ausnahme abgestürzt. Natürlich haben wir die Ausnahme gefangen, weil wir darauf vorbereitet waren und vorhatten, diesen Offset zu committen, aber zuvor wollten wir den Fehlerzähler erhöhen, da die Nachricht nicht erfolgreich verarbeitet wurde. Die Zähler in diesem Projekt liegen ebenfalls in der Datenbank, während Symfony bereits die Kommunikation mit der Datenbank geschlossen hatte, und eine zweite Ausnahme hat den gesamten Prozess ohne Chance auf das Commit des Offsets beendet.

Die Dienstleistung lag eine Zeit lang brach – glücklicherweise ist das bei Kafka nicht so schlimm, da die Nachrichten erhalten bleiben. Wenn die Arbeit wiederhergestellt wird, können sie nachgelesen werden. Das ist praktisch.

Mit Kafka besteht die Möglichkeit, über Tools einen beliebigen Offset zu setzen. Um dies zu tun, müssen jedoch alle Consumer gestoppt werden – in unserem Fall bedeutet das, einen speziellen Release vorzubereiten, der keine Consumer enthält, um dann neu zu deployen. So kann der Offset über das Tooling in Kafka verschoben werden, und die Nachricht wird verarbeitet.

Ein weiterer Punkt – Wiederholungsprotokoll vs rdkafka.so – hängt mit den spezifischen Anforderungen unseres Projekts zusammen. Wir nutzen PHP, und in PHP kommunizieren in der Regel alle Bibliotheken über das rdkafka.so-Repository mit Kafka, woraufhin eine Art Wrapper folgt. Vielleicht sind das unsere persönlichen Schwierigkeiten, aber es stellte sich heraus, dass es alles andere als einfach ist, einen bereits gelesenen Abschnitt erneut zu lesen. Kurz gesagt, wir hatten einige Softwareprobleme.

Wenn wir zu den Besonderheiten der Arbeit mit Partitions zurückkehren, steht direkt in der Dokumentation Consumer >= Topic-Partitionen. Ich erfuhr jedoch viel später davon, als ich es mir gewünscht hätte. Wenn Sie skalieren möchten und zwei Consumer haben wollen, benötigen Sie mindestens zwei Partitionen. Das heißt, wenn Sie eine Partition hatten, in der sich 20.000 Nachrichten angesammelt haben, und Sie eine neue erstellt haben, wird die Anzahl der Nachrichten nicht sofort gleichmäßig verteilt sein. Daher müssen Sie sich mit Partitionen auseinandersetzen, um zwei parallele Consumer zu haben.

Überwachung

Ich denke, dass es durch unsere Überwachung noch deutlicher wird, welche Probleme im aktuellen Ansatz bestehen.

Zum Beispiel zählen wir, wie viele Produkte in der Datenbank kürzlich den Status geändert haben. Entsprechend hätten auf Basis dieser Änderungen Ereignisse stattfinden müssen, und diese Zahl senden wir an unser Überwachungssystem. Anschließend erhalten wir aus Kafka die tatsächliche Anzahl der aufgezeichneten Ereignisse. Offensichtlich sollte die Differenz zwischen diesen beiden Zahlen immer null sein.

Erfahrung in der Entwicklung des Refund Tool-Dienstes mit asynchroner API auf Kafka

Außerdem müssen wir überwachen, wie es dem Producer geht, ob der Events-Bus Nachrichten empfangen hat, und wie es dem Consumer geht. Auf den Diagrammen unten läuft alles gut bei dem Refund Tool, aber bei BOB gibt es offensichtlich Probleme (blaue Spitzen).

Erfahrung in der Entwicklung des Refund Tool-Dienstes mit asynchroner API auf Kafka

Ich habe bereits über den Consumer-Group-Lag gesprochen. Grob gesagt, ist das die Anzahl der ungelesenen Nachrichten. Generell arbeiten unsere Consumer schnell, sodass der Lag normalerweise 0 beträgt, aber es kann manchmal einen kurzfristigen Anstieg geben. Kafka kann das out-of-the-box, aber es muss ein gewisser Zeitraum festgelegt werden.

Es gibt ein Projekt Burrow, der Ihnen mehr Informationen zu Kafka gibt. Er liefert einfach den Status über die API für die Consumer-Gruppe, wie es dieser Gruppe geht. Neben OK und Failed gibt es auch Warnungen, und Sie können erfahren, dass Ihre Consumer Schwierigkeiten haben, mit dem Tempo der Produktion Schritt zu halten – sie schaffen es nicht, das Geschriebene rechtzeitig zu verarbeiten. Das System ist ziemlich intelligent und einfach zu verwenden.

Erfahrung in der Entwicklung des Refund Tool-Dienstes mit asynchroner API auf Kafka

So sieht die Antwort über die API aus. Hier ist die Gruppe bob-live-fifa, Partition refund.update.v1, Status OK, Lag 0 – der letzte finale Offset ist der folgende.

Erfahrung in der Entwicklung des Refund Tool-Dienstes mit asynchroner API auf Kafka

Überwachung updated_at SLA (stuck) Ich habe bereits erwähnt. Zum Beispiel hat das Produkt den Status erreicht, dass es für die Rücksendung bereit ist. Wir setzen einen Cron ein, der sagt, dass wenn dieses Objekt innerhalb von 5 Minuten nicht in den Refund-Status übergegangen ist (wir erstatten das Geld über Zahlungssysteme sehr schnell), dann ist definitiv etwas schiefgelaufen, und es ist ein klarer Fall für den Support. Daher nutzen wir einfach einen Cron, der solche Dinge überwacht, und wenn sie größer als 0 sind, wird eine Alert-Nachricht gesendet.

Zusammenfassend lässt sich sagen, dass es praktisch ist, Ereignisse zu nutzen, wenn:

  • mehrere Systeme Informationen benötigen;
  • das Ergebnis der Verarbeitung nicht wichtig ist;
  • es wenige Ereignisse gibt oder die Ereignisse klein sind.

Auf den ersten Blick scheint der Artikel ein ganz konkretes Thema zu haben – die asynchrone API auf Kafka. Doch in diesem Zusammenhang gibt es gleich vieles, was ich empfehlen möchte.
Zunächst HighLoad++ , man muss bis November warten, doch bereits im April wird es eine Version in St. Petersburg geben, und im Juni werden wir über hohe Lasten in Nowosibirsk sprechen.
Zweitens, der Referent Sergej Zaika ist Mitglied des Programmkomitees unserer neuen Konferenz über Wissensmanagement KnowledgeConf. Die Konferenz ist eintägig und findet am 26. April statt, jedoch hat das Programm viel zu bieten.
Außerdem wird es im Mai PHP Russland und RIT++ geben (zusammen mit DevOpsConf) – dort kann man ebenfalls Themen einreichen, von seinen Erfahrungen berichten und über seine gesammelten Rückschläge klagen.

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