Was kann ein so großes Unternehmen wie Lamoda mit einem optimierten Prozess und Dutzenden miteinander verbundener Dienste dazu bewegen, seinen Ansatz grundlegend zu ändern? Die Motivation kann ganz unterschiedlich sein: von gesetzlichen Vorschriften bis hin zum angeborenen Drang aller Programmierer, zu experimentieren.
Doch das bedeutet keineswegs, dass man nicht auf zusätzliche Vorteile hoffen kann. Worüber man konkret profitieren kann, wenn man eine events-gesteuerte API auf Kafka einführt, wird Sergej Zaika erzählen (). Auch die gesammelten Erfahrungen und interessanten Entdeckungen werden natürlich nicht fehlen – Experimente kommen nicht ohne sie aus.

Haftungsausschluss: Dieser Artikel basiert auf Materialien eines Meetups, das Sergej im November 2018 auf HighLoad++ durchgeführt hat. Die lebhaften Erfahrungen von Lamoda mit Kafka haben nicht weniger Zuhörer angezogen als andere Vorträge im Zeitplan. Es scheint uns ein hervorragendes Beispiel dafür zu sein, dass man immer Gleichgesinnte finden kann und dass die Organisatoren von HighLoad++ weiterhin bestrebt sind, eine einladende Atmosphäre dafür zu schaffen.
Über den Prozess
Lamoda ist eine große E-Commerce-Plattform, die über ein eigenes Callcenter, einen Lieferservice (und viele Partner), ein Fotostudio, ein riesiges Lager und all das, was auf eigener Software basiert, verfügt. Es gibt Dutzende von Zahlungsmethoden, B2B-Partner, die Teile oder alle diese Dienstleistungen nutzen können und aktuelle Informationen zu ihren Produkten erhalten möchten. Darüber hinaus ist Lamoda in drei Ländern tätig, abgesehen von der RF, wo alles ein wenig anders ist. Insgesamt gibt es wahrscheinlich mehr als hundert Möglichkeiten, eine neue Bestellung zu konfigurieren, die auf ihre jeweils eigene Weise verarbeitet werden muss. All dies funktioniert mit Hilfe von Dutzenden von Dienstleistungen, die manchmal auf nicht offensichtliche Weise miteinander kommunizieren. Außerdem gibt es ein zentrales System, dessen Hauptverantwortung die Bestellstatus ist. Wir nennen es BOB, und ich arbeite damit.
Refund Tool mit events-gesteuerter API
Der Begriff events-gesteuert ist ziemlich abgedroschen; gleich werden wir näher definieren, was damit gemeint ist. Ich werde mit dem Kontext beginnen, in dem wir beschlossen haben, den Ansatz der events-gesteuerten API in Kafka auszuprobieren.

In jedem Geschäft, neben den Bestellungen, für die die Käufer bezahlen, gibt es Momente, in denen der Laden verlangt, das Geld zurückzugeben, weil das Produkt dem Kunden nicht zusagt. Dieser vergleichsweise kurze Prozess: Wir klären die Informationen, falls nötig, und überweisen das Geld.
Die Rückabwicklung hat sich aufgrund der Änderungen in der Gesetzgebung kompliziert gestaltet, und wir mussten dafür einen eigenen Microservice implementieren.

Unsere Motivation:
- Gesetz FZ-54 — kurz gesagt, das Gesetz erfordert, dass jede Geldtransaktion, sei es eine Rückzahlung oder ein Zahlungseingang, innerhalb eines recht kurzen SLAs von wenigen Minuten an das Finanzamt gemeldet wird. Wir als E-Commerce-Unternehmen führen relativ viele Transaktionen durch. Technisch bedeutet das eine neue Verantwortung (und damit einen neuen Service) sowie Anpassungen in allen beteiligten Systemen.
- BOB split — ein internes Unternehmensprojekt zur Befreiung von BOB von vielen unangebrachten Verantwortlichkeiten und zur Reduzierung seiner Gesamtheit.

In diesem Diagramm sind die Hauptsysteme von Lamoda dargestellt. Derzeit besteht der Großteil aus eher einem Sternenbild aus 5-10 Microservices rund um einen schrumpfenden Monolithen. Sie wachsen langsam, aber wir versuchen, sie kleiner zu halten, denn eine Veröffentlichung eines fokussierten Mittelteils ist beängstigend – wir dürfen nicht zulassen, dass er ausfällt. Alle Austausche (Pfeile) müssen wir reservieren und darauf setzen, dass jeder von ihnen möglicherweise nicht verfügbar sein kann.
Auch in BOB gibt es ziemlich viele Austausche: Zahlungssysteme, Lieferdienste, Benachrichtigungen usw.
Technisch gesehen ist BOB:
- ~150k Zeilen Code + ~100k Testzeilen;
- php7.2 + Zend 1 & Symfony Components 3;
- >100 APIs & ~50 ausgehende Integrationen;
- 4 Länder mit ihrer eigenen Geschäftslogik.
Die Bereitstellung von BOB ist kostspielig und schmerzhaft, die Menge an Code und die damit verbundenen Aufgaben sind so umfangreich, dass niemand sie vollständig im Kopf behalten kann. Insgesamt gibt es viele Gründe, ihn zu vereinfachen.
Rückgabeprozess
Ursprünglich sind zwei Systeme in den Prozess involviert: BOB und Payment. Jetzt kommen noch zwei hinzu:
- Fiscalization Service, der die Probleme mit der Fiskalisierung und der Kommunikation mit externen Diensten übernehmen wird.
- Refund Tool, in dem einfach neue Austausche ausgelagert werden, um BOB nicht aufzublähen.
Jetzt sieht der Prozess so aus:

- An BOB wird eine Anfrage zur Rückzahlung gestellt.
- BOB informiert darüber das Refund Tool.
- Refund Tool sagt zu Payment: „Erstatte das Geld“.
- Payment erstattet das Geld.
- Refund Tool und BOB synchronisieren die Status untereinander, da sie das derzeit beide benötigen. Wir sind noch nicht bereit, vollständig auf Refund Tool umzuschwenken, da es in BOB eine UI gibt, Berichte für die Buchhaltung und viele Daten, die sich nicht einfach übertragen lassen. Wir müssen auf zwei Stühlen sitzen.
- Die Anfrage zur Fiskalisierung wird gesendet.
Wir haben letztendlich eine Art Ereignisbus auf Kafka geschaffen, auf dem alles basierte. Hurra, jetzt haben wir einen einzigen Fehlerpunkt (Sarkasmus).

Die Vor- und Nachteile sind ziemlich offensichtlich. Wir haben einen Bus erstellt, was bedeutet, dass jetzt alle Dienste davon abhängen. Das vereinfacht das Design, bringt aber einen einzigen Fehlerpunkt in das System. Fällt Kafka aus, steht der Prozess still.
Was ist ein ereignisgesteuertes API?
Eine gute Antwort auf diese Frage finden Sie im Vortrag von Martin Fowler (GOTO 2017). .
Kurz gesagt, was wir gemacht haben:
- Wir haben alle asynchronen Kommunikationen über Ereignisspeicherverbunden. Anstatt allen interessierten Verbrauchern über Statusänderungen zu berichten, schreiben wir in ein zentrales Repository ein Ereignis über eine Zustandsänderung, und die interessierten Verbraucher lesen alles, was dort erscheint.
- Ein Ereignis (event) in diesem Fall ist eine Benachrichtigung (notifications) darüber, dass sich etwas irgendwo geändert hat. Zum Beispiel hat sich der Status einer Bestellung geändert. Ein Verbraucher, dem bestimmte begleitende Änderungsdaten wichtig sind, die in der Benachrichtigung nicht enthalten sind, kann den aktuellen Stand selbst abfragen.
- Die maximale Option ist ein vollständiges Event Sourcing, State Transfer, bei dem das Ereignis alle Informationen enthält, die zur Verarbeitung erforderlich sind: wohin und in welchen Status sie gewechselt haben, wie sich die Daten geändert haben usw. Es geht nur um die Zweckmäßigkeit und das Volumen an Informationen, das Sie sich leisten können zu speichern.
Im Rahmen des Starts des Refund Tools haben wir die dritte Option verwendet. Das hat die Behandlung von Ereignissen vereinfacht, da es nicht notwendig war, detaillierte Informationen zu beschaffen, und das Szenario ausgeschlossen hat, bei dem jedes neue Ereignis einen Anstieg von klärenden GET-Anfragen von Verbrauchern auslöst.
Der Service Refund Tool ist nicht belastet,deshalb ist Kafka dort eher ein Versuch als eine Notwendigkeit. Ich glaube nicht, dass das Unternehmen glücklich wäre, wenn der Rückerstattungsdienst ein Hochlastprojekt wäre.
Asynchroner Austausch AS IS
Für asynchrone Austausche verwendet die PHP-Abteilung normalerweise RabbitMQ. Wir haben die Daten für die Anfrage gesammelt, diese in die Warteschlange gestellt, und der Verbraucher desselben Dienstes hat sie verarbeitet und gesendet (oder nicht gesendet). Für die API verwendet Lamoda aktiv Swagger. Wir konzipieren die API, beschreiben sie in Swagger, generieren den Client- und Servercode. Außerdem nutzen wir eine etwas erweiterte JSON RPC 2.0.
Überall werden ESB-Busse verwendet, manche arbeiten mit ActiveMQ, aber insgesamt RabbitMQ – Standard.
Async-Austausch TO BE
Bei der Gestaltung des Austauschs über den Events-Bus gibt es eine Analogie. Wir beschreiben auf ähnliche Weise den zukünftigen Datenaustausch anhand der Struktur des Events. Das YAML-Format, die Code-Generierung mussten wir selbst vornehmen, der Generator erstellt DTOs gemäß der Spezifikation und lehrt Clients und Server, damit zu arbeiten. Die Generierung erfolgt in zwei Sprachen – Golang und PHP. Das ermöglicht es, die Bibliotheken konsistent zu halten. Der Generator wurde in Golang geschrieben, weshalb er den Namen gogi erhielt.
Event-Sourcing auf Kafka ist eine gängige Praxis. Es gibt eine Lösung von der Haupt-Enterprise-Version von Kafka Confluent, es gibt , eine Lösung von unseren „Brüdern“ im Domainbereich Zalando. Unsere Motivation, mit Vanilla Kafka zu beginnen, besteht darin, die Lösung kostenlos zu halten, bis wir endgültig entschieden haben, ob wir sie umfassend nutzen werden, und uns zudem Spielraum für Anpassungen zu lassen: wir wollen Unterstützung für unsere JSON RPC 2.0, Generatoren für zwei Sprachen und sehen, was es noch gibt.
Ironisch ist, dass wir selbst in einem so glücklichen Fall, in dem es ein ungefähr analoges Unternehmen wie Zalando gibt, das eine ähnliche Lösung entwickelt hat, diese nicht effektiv nutzen können.
Architektonisch sieht das Muster beim Start so aus: Wir lesen direkt aus Kafka, schreiben aber nur über den Events-Bus. Für das Lesen in Kafka gibt es viel Vorhandenes: Broker, Lastverteiler und sie ist mehr oder weniger bereit für horizontale Skalierung, das wollten wir erhalten. Schreiben hingegen wollten wir über ein Gateway, auch bekannt als Events-Bus, und das ist der Grund.
Events-Bus
Oder Event-Bus. Dies ist einfach ein stateless HTTP-Gateway, das mehrere wichtige Rollen übernimmt:
- Validierung des Produzierens – wir überprüfen, ob die Events unserer Spezifikation entsprechen.
- Das Hauptsystem für Events, das heißt, es ist das Haupt- und einzige System im Unternehmen, das die Frage beantwortet, welche Events mit welchen Strukturen als gültig gelten. In die Validierung fließen einfach Datentypen und Enums für die strikte Spezifikation des Inhalts ein.
- Hash-Funktion für Sharding – die Struktur der Kafka-Nachricht ist key-value und der Hash vom key wird berechnet, um zu bestimmen, wo diese abzulegen ist.
Warum
Wir arbeiten in einem großen Unternehmen mit einem etablierten Prozess. Warum etwas ändern? Dies ist ein Experiment, und wir erwarten, mehrere Vorteile zu erzielen.
1:n+1 Austausche (eins zu vielen)
Mit Kafka ist es sehr einfach, neue Verbraucher über die API anzuschließen.
Angenommen, Sie haben ein Verzeichnis, das in mehreren Systemen gleichzeitig (und in einigen neuen) aktuell gehalten werden muss. Früher haben wir ein Bundle erfunden, das das Set-API implementierte, und der Master-System hat die Adressen der Verbraucher mitgeteilt. Jetzt sendet das Master-System Updates an ein Topic, und alle, die interessiert sind, lesen mit. Ein neues System ist erschienen - es wurde an das Topic angeschlossen. Ja, auch ein Bundle, aber einfacher.
Im Fall des Refund-Tools, das ein Teil von BOB ist, ist es uns bequem, sie über Kafka synchronisiert zu halten. Die Zahlung sagt, dass das Geld zurückgegeben wurde: BOB, RT haben davon erfahren, ihre Status geändert, und der Fiscalization Service hat auch davon erfahren und einen Beleg ausgestellt.

Wir planen einen einheitlichen Notifications Service, der den Kunden über Neuigkeiten zu seiner Bestellung/Rückgaben informieren würde. Momentan ist diese Verantwortung auf mehrere Systeme verteilt. Es reicht aus, den Notifications Service zu lehren, relevante Informationen aus Kafka zu filtern und darauf zu reagieren (und die Benachrichtigungen in den anderen Systemen zu deaktivieren). Es sind keine neuen direkten Austausche erforderlich.
Datengetrieben
Die Informationen zwischen den Systemen werden transparent - egal wie 'blutig' Ihr Unternehmen ist und wie umfangreich Ihr Backlog. Bei Lamoda gibt es eine Abteilung für Datenanalytik, die Daten über Systeme sammelt und sie sowohl für das Geschäft als auch für intelligente Systeme in wiederverwendbare Form bringt. Kafka ermöglicht es, ihnen schnell viele Daten zur Verfügung zu stellen und diesen Informationsfluss aktuell zu halten.
Replikationsprotokoll
Nachrichten verschwinden nicht nach dem Lesen, wie bei RabbitMQ. Wenn ein Ereignis genügend Informationen für die Verarbeitung enthält, haben wir eine Historie der letzten Änderungen an dem Objekt, und bei Bedarf die Möglichkeit, diese Änderungen anzuwenden.
Die Aufbewahrungsdauer des Replikationsprotokolls hängt von der Intensität der Aufzeichnungen in dieses Topic ab, Kafka ermöglicht es, die Zeitlimits und die Datenmenge flexibel einzustellen. Für intensive Topics ist es wichtig, dass alle Verbraucher die Informationen rechtzeitig abrufen, bevor sie verschwinden, selbst im Fall einer vorübergehenden Unfähigkeit. Normalerweise gelingt es, Daten für Tage aufzubewahren, was für den Support völlig ausreichend ist.

Ein bisschen eine Zusammenfassung der Dokumentation für diejenigen, die nicht mit Kafka vertraut sind (das Bild stammt ebenfalls aus der Dokumentation)
In AMQP gibt es Warteschlangen: Wir schreiben Nachrichten in die 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 ein Exchange mit einem Fanout-Mechanismus eingerichtet werden, der sie selbst klont.
In Kafka gibt es eine ähnliche Abstraktion Thema, in das Sie Nachrichten schreiben, aber sie verschwinden nicht nach dem Lesen. Bei der Verbindung mit Kafka erhalten Sie standardmäßig alle Nachrichten, und es besteht die Möglichkeit, den Ort zu speichern, an dem Sie aufgehört haben. Das bedeutet, dass Sie sequenziell lesen, eine Nachricht nicht als gelesen markieren, aber die ID speichern können, von der aus Sie dann weiterlesen. Die ID, an der Sie aufgehört haben, wird als Offset bezeichnet, und der Mechanismus heißt Commit-Offset.
Dementsprechend kann unterschiedliche Logik implementiert werden. Zum Beispiel haben wir BOB in 4 Instanzen für verschiedene Länder – Lamoda ist in Russland, Kasachstan, der Ukraine und Weißrussland verfügbar. Da sie separat bereitgestellt werden, haben sie unterschiedliche Konfigurationen und ihre eigene Geschäftslogik. Wir geben in der Nachricht an, auf welches Land sie sich bezieht. Jeder BOB-Verbraucher in jedem Land liest mit unterschiedlichen groupId, und wenn die Nachricht nicht relevant ist, wird sie übersprungen, d.h. der Offset wird sofort um +1 bestätigt. Wenn dasselbe Thema von unserem Payment Service gelesen wird, geschieht dies mit einer separaten Gruppe, sodass die Offsets sich nicht überschneiden.
Anforderungen an Ereignisse:
- Datenvollständigkeit. Es wäre wünschenswert, dass genügend Daten im Ereignis vorhanden sind, um es verarbeiten zu können.
- Integrität. Wir delegieren dem Events-Bus die Überprüfung, dass das Ereignis konsistent ist und verarbeitet werden kann.
- Die Reihenfolge ist wichtig. Im Falle einer Rückgabe sind wir gezwungen, mit der Historie zu arbeiten. Bei Benachrichtigungen ist die Reihenfolge unerheblich, wenn es sich um homogene Benachrichtigungen handelt; die E-Mail wird gleich sein, unabhängig davon, welcher Auftrag zuerst eingetroffen ist. Im Falle einer Rückgabe gibt es einen klaren Prozess; wenn die Reihenfolge geändert wird, könnten Ausnahmen auftreten, Rückerstattungen werden nicht ausgelöst oder verarbeitet – und wir geraten in einen anderen Status.
- Kohärenz. Wir haben ein Repository, und jetzt erstellen wir anstelle von APIs Events. Wir benötigen eine Möglichkeit, schnell und kostengünstig Informationen über neue Events und Änderungen an bestehenden Events an unsere Dienste zu übermitteln. Dies wird durch eine gemeinsame Spezifikation in einem separaten Git-Repository und Code-Generatoren erreicht. Daher sind die Clients und Server in unseren verschiedenen Diensten abgestimmt.
Kafka bei Lamoda
Wir haben drei Kafka-Installationen:
- Logs;
- Forschung & Entwicklung;
- Events-Bus.
Heute sprechen wir nur über den letzten Punkt. In unserem Events-Bus haben wir nicht sehr große Installationen - 3 Broker (Server) und insgesamt 27 Themen. Ein Thema ist in der Regel ein Prozess. Aber das ist ein heikles Thema, und darauf werden wir gleich zu sprechen kommen.

Oben ist das Diagramm der rps. Der Prozess Rückerstattungen ist durch eine türkisfarbene Linie markiert (ja, ja, die, die auf der X-Achse liegt), und die rosa Linie zeigt den Prozess der Inhaltsaktualisierung.
Der Katalog von Lamoda enthält Millionen von Produkten, und die Daten werden ständig aktualisiert. Einige Kollektionen geraten aus der Mode, dafür werden neue veröffentlicht, und im Katalog erscheinen ständig neue Modelle. Wir versuchen vorherzusagen, was morgen für unsere Kunden interessant sein wird, deshalb kaufen wir ständig neue Artikel, fotografieren sie und aktualisieren das Schaufenster.
Die rosa Spitzen sind Produktaktualisierungen, also Änderungen bei den Artikeln. Man sieht deutlich, dass die Jungs fotografiert haben, fotografiert haben und dann plötzlich! — eine Reihe von Ereignissen hochgeladen haben.
Lamoda Events Anwendungsfälle
Die entwickelte Architektur verwenden wir für folgende Operationen:
- Statusverfolgung von Rücksendungen: Call-to-Action und Statusverfolgung von allen beteiligten Systemen. Bezahlung, Status, Fiskalierung, Benachrichtigungen. Hier haben wir einen Ansatz ausprobiert, Werkzeuge erstellt, alle Fehler gesammelt, Dokumentation geschrieben und unseren Kollegen erklärt, wie sie damit umgehen können.
- Aktualisierung von Produktkarten: Konfiguration, Metadaten, Eigenschaften. Liest ein System (das anzeigt), während mehrere schreiben.
- E-Mail, Push und SMS: Bestellung zusammengestellt, Bestellung angekommen, Rücksendung akzeptiert usw., viele davon.
- Bestand, Lageraktualisierung — quantitative Aktualisierung der Artikel, einfach Zahlen: Eingang ins Lager, Rücksendung. Alle Systeme, die mit der Reservierung von Produkten zu tun haben, müssen mit den aktuellsten Daten arbeiten. Momentan ist das System zur Bestandsaktualisierung ziemlich komplex, Kafka wird es vereinfachen.
- Datenanalyse (F&E-Abteilung), ML-Tools, Analytik, Statistik. Wir wollen, dass die Informationen transparent sind – dafür eignet sich Kafka gut.
Jetzt kommt der interessantere Teil über die Fehler und interessanten Entdeckungen, die in einem halben Jahr gemacht wurden.
Entwurfsprobleme
Angenommen, wir wollen eine neue Sache machen – zum Beispiel den gesamten Lieferprozess auf Kafka umzustellen. Derzeit wird ein Teil des Prozesses in der Auftragsbearbeitung in BOB umgesetzt. Hinter der Übermittlung des Auftrags an den Lieferdienst, der Bewegung zu einem Zwischenlager und ähnlichem steht ein Statusmodell. Es gibt ein ganzes Monolithen, sogar zwei, plus eine Menge APIs, die sich mit der Lieferung befassen. Sie wissen viel mehr über die Lieferung.
Es scheint, dass dies ähnliche Bereiche sind, aber für die Auftragsbearbeitung in BOB und für das Liefersystem unterscheiden sich die Status. Einige Kurierdienste senden beispielsweise keine Zwischenstatus, sondern nur die endgültigen: 'geliefert' oder 'verloren'. Andere hingegen berichten sehr detailliert über die Bewegung der Ware. Jeder hat seine eigenen Validierungsregeln: Für den einen ist eine gültige E-Mail-Adresse notwendig, um verarbeitet zu werden; für andere ist sie nicht gültig, aber der Auftrag wird trotzdem bearbeitet, weil es eine Telefonnummer für die Kontaktaufnahme gibt, und wieder andere sagen, dass ein solcher Auftrag überhaupt nicht bearbeitet wird.
Datenfluss
Im Falle von Kafka stellt sich die Frage der Organisation des Datenflusses. Diese Aufgabe hängt von der Wahl einer Strategie in mehreren Punkten ab; wir gehen sie alle durch.
In einem Topic oder in verschiedenen?
Wir haben eine Spezifikation für das Ereignis. In BOB schreiben wir, dass ein bestimmter Auftrag geliefert werden muss, und geben an: Auftragsnummer, Inhalt, einige SKUs und Barcodes usw. Wenn die Ware im Lager ankommt, kann die Lieferung Status, Zeitstempel und alles Notwendige erhalten. Aber dann wollen wir in BOB Updates zu diesen Daten erhalten. Es entsteht ein umgekehrter Prozess der Datenerfassung aus der Lieferung. Ist das dasselbe Ereignis? Oder ist das ein separater Austausch, der ein separates Topic verdient?
Wahrscheinlich werden sie sehr ähnlich sein, und die Versuchung, ein Topic zu erstellen, ist nicht unbegründet, denn ein separates Topic bedeutet separate Consumer, separate Konfigurationen, separate Generierung davon. Aber es ist nicht sicher.
Neues Feld oder neues Ereignis?
Wenn wir jedoch dieselben Ereignisse verwenden, taucht ein anderes Problem auf. Beispielsweise können nicht alle Liefersysteme ein solches DTO generieren, das BOB generieren kann. Wir senden ihnen die ID, aber sie speichern sie nicht, weil sie nicht benötigt werden, und aus der Perspektive des Beginns des Prozess-Event-Bus ist dieses Feld obligatorisch.
Wenn wir für den Event-Bus die Regel einführen, dass dieses Feld obligatorisch ist, sind wir gezwungen, in BOB oder im Handler des Startereignisses zusätzliche Validierungsregeln festzulegen. Die Validierung beginnt sich im Dienst zu verteilen — das ist nicht besonders praktisch.
Ein weiteres Problem ist die Versuchung der inkrementellen Entwicklung. Uns wird gesagt, dass wir etwas zum Ereignis hinzufügen müssen, und vielleicht, wenn man gut nachdenkt, hätte das ein separates Ereignis sein sollen. In unserem Schema ist ein separates Ereignis jedoch ein separates Topic. Ein separates Topic ist der gesamte 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 sind wir so innerhalb von sechs Monaten zu einem Ereignis von Ereignissen gekommen. Wir hatten ein Meta-Ereignis, das Refund-Update heißt und ein Feld vom Typ enthält, das beschreibt, worin dieser Update besteht. Von dort hatten wir „wunderbare“ Switches mit Validierern, die sagten, wie man dieses Ereignis mit diesem Typ validieren muss.
Versionierung von Ereignissen
Zur Validierung von Nachrichten in Kafka kann man verwenden , aber wir mussten von Anfang an darauf achten und Confluent verwenden. In unserem Fall mit der Versionierung müssen wir vorsichtig sein. Es wird nicht immer möglich sein, Nachrichten aus dem Replikationsprotokoll wiederzulesen, weil das Modell „abgefahren“ ist. Im Wesentlichen versuchen wir, Versionen so zu erstellen, dass das Modell abwärtskompatibel bleibt: zum Beispiel, indem wir ein Feld vorübergehend optional machen. Wenn die Unterschiede zu stark sind, fangen wir an, in ein neues Topic zu schreiben, und wir übertragen die Kunden, wenn sie das alte gelesen haben.
Garantie der Lesereihenfolge von Partitions
Topics innerhalb von Kafka sind in Partitions unterteilt. Das ist zunächst nicht besonders wichtig, während wir die Entitäten und den Austausch entwerfen, aber wichtig, wenn wir entscheiden, wie wir das konsumieren und skalieren.
In der Regel schreiben Sie in Kafka ein Topic. Standardmäßig wird eine Partition verwendet, und alle Nachrichten dieses Topics landen darin. Der Consumer liest diese Nachrichten dann nacheinander. Angenommen, Sie müssen das System erweitern, sodass zwei verschiedene Consumer die Nachrichten lesen. Wenn Sie zum Beispiel eine SMS senden, können Sie Kafka sagen, dass es eine zusätzliche Partition erstellen soll, und Kafka beginnt, die Nachrichten auf zwei Teile zu verteilen – die Hälfte dort, die Hälfte hier.
Wie trennt Kafka sie? Jedes Nachrichten hat einen Körper (in dem wir JSON speichern) und einen Schlüssel. An diesen Schlüssel kann eine Hash-Funktion angehängt werden, die bestimmt, in welche Partition die Nachricht gelangt.
In unserem Fall mit Rückerstattungen ist das wichtig, wenn wir zwei Partitionen nehmen, besteht die Chance, dass ein paralleler Consumer das zweite Ereignis schneller verarbeitet als das erste, und das kann Probleme verursachen. Die Hash-Funktion garantiert, dass Nachrichten mit demselben Schlüssel in der gleichen Partition landen.
Ereignisse vs. Befehle
Dies ist ein weiteres Problem, dem wir begegnet sind. Ein Ereignis ist ein bestimmtes Geschehen: Wir sagen, dass irgendwo etwas passiert ist (something_happened), zum Beispiel, dass ein Artikel storniert wurde oder eine Rückerstattung stattgefunden hat. Wenn jemand diese Ereignisse hört, wird beim „Artikel storniert“ eine Rückerstattungseinheit erstellt, und „Rückerstattung erfolgt“ wird irgendwo in den Setups festgehalten.
Aber normalerweise, wenn Sie Ereignisse entwerfen, möchten Sie sie nicht umsonst schreiben – Sie rechnen damit, dass sie jemand lesen wird. Es besteht die große Versuchung, nicht something_happened (item_canceled, refund_refunded) zu schreiben, sondern something_should_be_done. Zum Beispiel, Artikel ist zur Rückgabe bereit.
Einerseits gibt es Hinweise darauf, wie das Ereignis verwendet wird. Andererseits ähnelt es viel weniger einem normalen Ereignisnamen. Außerdem ist es von hier nicht weit weg zu dem Befehl do_something. Aber Sie haben keine Garantie, dass dieses Ereignis von jemandem gelesen wurde; und wenn es gelesen wurde, dann erfolgreich; und wenn es erfolgreich gelesen wurde, dann wurde etwas getan, und dieses Etwas ist erfolgreich durchgegangen. In dem Moment, in dem das Ereignis zu do_something wird, wird Feedback erforderlich, und das ist ein Problem.

In der asynchronen Kommunikation in RabbitMQ, wenn Sie eine Nachricht gelesen haben, gehen Sie in das HTTP, und Sie haben eine Antwort – zumindest, dass die Nachricht angenommen wurde. Wenn Sie in Kafka geschrieben haben, gibt es eine Nachricht, dass Sie in Kafka geschrieben haben, aber wie sie verarbeitet wurde, wissen Sie nichts.
Daher mussten wir in unserem Fall ein entsprechendes Ereignis einführen und das Monitoring so einstellen, dass, wenn eine bestimmte Anzahl von Ereignissen auftritt, nach einer bestimmten Zeit die gleiche Anzahl an Rückmeldungen erfolgen sollte. Wenn dies nicht geschieht, scheint es, dass etwas schiefgelaufen ist. Zum Beispiel, wenn wir das Ereignis „item_ready_to_refund“ gesendet haben, erwarten wir, dass eine Rückerstattung erstellt wird, der Kunde sein Geld zurückbekommt und wir das Ereignis „money_refunded“ erhalten. Aber das ist nicht sicher, daher ist ein Monitoring erforderlich.
Nuancen
Es gibt ein ziemlich offensichtliches Problem: Wenn Sie die Nachrichten der Reihe nach vom Thema lesen und eine Nachricht schlecht ist, fällt der Consumer aus, und Sie kommen nicht weiter. Sie müssen alle Consumer anhalten, den Offset weiter committen, um weiterzulesen.
Darüber waren wir uns bewusst, wir hatten darauf gebaut, und trotzdem ist es passiert. Das passierte, weil das Ereignis aus Sicht des Events-Bus gültig war, das Ereignis war aus Sicht des Anwendungsvalidators gültig, aber es war nicht aus Sicht von PostgreSQL gültig, weil wir in einem System MySQL mit UNSIGNED INT haben und im neu geschriebenen System war es einfach INT bei PostgreSQL. Es ist ein wenig kleiner, und die ID passte nicht. Symfony starb mit einer Ausnahme. Natürlich haben wir die Ausnahme abgefangen, weil wir darauf gebaut hatten, und wollten diesen Offset committen, aber davor wollten wir den Fehlerzähler erhöhen, da die Nachricht nicht erfolgreich verarbeitet wurde. Die Zähler in diesem Projekt liegen auch in der Datenbank, und Symfony hatte bereits die Kommunikation mit der Datenbank beendet, und die zweite Ausnahme hat den gesamten Prozess ohne Chance auf Commit des Offsets getötet.
Eine Zeit lang lag der Service brach - zum Glück ist das mit Kafka nicht so schlimm, weil die Nachrichten erhalten bleiben. Wenn die Arbeit wiederhergestellt wird, können sie nachgelesen werden. Das ist praktisch.
Kafka bietet die Möglichkeit, über Tools einen beliebigen Offset festzulegen. Aber um dies zu tun, müssen alle Consumer angehalten werden - in unserem Fall muss ein separates Release vorbereitet werden, in dem keine Consumer vorhanden sind, keine Neuredeployments. Dann kann der Offset über Tools in Kafka verschoben werden, und die Nachricht geht durch.
Ein weiterer Aspekt - Replikationsprotokoll vs rdkafka.so — steht im Zusammenhang mit der Spezifik des Projekts. Wir verwenden PHP, und in PHP kommunizieren in der Regel alle Bibliotheken über das Repository rdkafka.so mit Kafka, und dann erfolgt eine Art Wrapper. Vielleicht sind das unsere persönlichen Schwierigkeiten, aber es stellt sich heraus, dass es nicht so einfach ist, einen bereits gelesenen Abschnitt noch einmal zu lesen. Insgesamt gab es Softwareprobleme.
Zurück zu den Besonderheiten der Arbeit mit Partitions, es steht direkt in der Dokumentation geschrieben consumers >= topic partitions. Aber ich habe davon viel später erfahren, als ich gerne wollte. Wenn Sie skalieren möchten und zwei Konsumenten haben wollen, benötigen Sie mindestens zwei Partitions. Das heißt, wenn Sie eine Partition hatten, in der 20.000 Nachrichten angesammelt wurden und Sie eine frische erstellt haben, wird die Anzahl der Nachrichten nicht sofort gleichmäßig verteilt. Daher müssen Sie sich mit Partitions befassen, um zwei parallele Konsumenten zu haben.
Überwachung
Ich denke, dass es umso deutlicher wird, welche Probleme im bestehenden Ansatz vorhanden sind, je nachdem, wie wir überwachen.
Zum Beispiel zählen wir, wie viele Artikel in der Datenbank kürzlich ihren Status geändert haben, und demnach sollten aufgrund dieser Änderungen Ereignisse aufgetreten sein. Diese Zahl senden wir an unser Überwachungssystem. Dann erhalten wir aus Kafka eine zweite Zahl, wie viele Ereignisse tatsächlich aufgezeichnet wurden. Offensichtlich sollte die Differenz zwischen diesen beiden Zahlen immer null sein.

Darüber hinaus müssen wir überwachen, wie es dem Producer geht, ob der Events-Bus Nachrichten angenommen hat und wie es dem Konsumenten geht. Zum Beispiel sieht bei Refund Tool alles gut aus, aber bei BOB gibt es offensichtlich einige Probleme (blaue Spitzen).

Ich habe bereits den Lag der Consumer-Gruppe erwähnt. Grob gesagt, ist das die Anzahl der ungelesenen Nachrichten. Insgesamt arbeiten unsere Konsumenten schnell, daher liegt der Lag normalerweise bei 0, aber manchmal kann es zu kurzfristigen Spitzen kommen. Kafka kann das out of the box, aber es ist notwendig, einen bestimmten Zeitraum festzulegen.
Es gibt ein Projekt , das Ihnen mehr Informationen über Kafka liefert. Es gibt einfach über die API den Status der Consumer-Gruppe wieder, wie es dieser Gruppe geht. Neben OK und Failed gibt es dort auch Warnungen, und Sie können herausfinden, dass Ihre Konsumenten mit dem Tempo des Produzierens nicht mithalten – sie schaffen es nicht, das, was geschrieben wird, zu lesen. Das System ist ziemlich intelligent und lässt sich gut nutzen.

So sieht die Antwort über die API aus. Hier ist die Gruppe bob-live-fifa, Partition refund.update.v1, Status OK, Lag 0 – letzter endgültiger Offset so und so.

Überwachung updated_at SLA (stuck) Ich habe bereits erwähnt. Zum Beispiel hat das Produkt den Status erreicht, dass es zur Rückgabe bereit ist. Wir setzen Cron ein, der besagt, dass, wenn dieses Objekt innerhalb von 5 Minuten nicht in den Refund-Status übergegangen ist (wir erstatten das Geld über die Zahlungssysteme sehr schnell), etwas definitiv schief gelaufen ist und es ein Fall für den Support ist. Daher nehmen wir einfach Cron, der solche Sachen liest, und wenn sie größer als 0 sind, sendet er einen Alert.
Zusammenfassend lässt sich sagen, dass es praktisch ist, Ereignisse zu verwenden, wenn:
- Informationen mehreren Systemen benötigt werden;
- das Ergebnis der Verarbeitung nicht wichtig ist;
- es nur wenige Ereignisse gibt oder die Ereignisse klein sind.
Auf den ersten Blick hat der Artikel ein ganz konkretes Thema - eine asynchrone API auf Kafka, aber in diesem Zusammenhang möchte ich gleich vieles empfehlen.
Erstens, das nächste muss bis November warten, die Petersburger Version wird bereits im April erscheinen, und im Juni werden wir über hohe Lasten in Nowosibirsk sprechen.
Zweitens, der Autor des Berichts Sergej Zaika ist Mitglied des Programmkomitees unserer neuen Konferenz über Wissensmanagement . Die Konferenz ist eintägig und findet am 26. April statt, aber das Programm ist sehr umfangreich.
Außerdem wird es im Mai und (mit DevOpsConf im Programm) - dort kann man noch sein Thema vorschlagen, über seine Erfahrungen berichten und sich über seine gemachten Fehler beschweren.
Quelle: habr.com
