Seit 2019 gilt in Russland ein Gesetz zur obligatorischen Kennzeichnung. Das Gesetz betrifft nicht alle Warengruppen, und die Fristen für den Inkrafttreten der Kennzeichnungsanforderungen variieren je nach Warengruppe. Zunächst sind Tabak, Schuhe und Medikamente von der obligatorischen Kennzeichnung betroffen, später werden auch weitere Produkte wie Parfüm, Textilien und Milch hinzukommen. Diese gesetzliche Neuerung hat die Entwicklung neuer IT-Lösungen angeregt, die es ermöglichen, die gesamte Lebensdauer eines Produkts vom Herstellungsprozess bis zum Kauf durch den Endverbraucher nachzuvollziehen. Dies betrifft alle Beteiligten: sowohl den Staat als auch alle Unternehmen, die Produkte mit obligatorischer Kennzeichnung vertreiben.
Bei X5 erhielt das System zur Verfolgung von gekennzeichneten Produkten und zum Austausch von Daten mit dem Staat und den Lieferanten den Namen „Markus“. Lassen Sie uns der Reihe nach erklären, wie und von wem es entwickelt wurde, welchen Technologie-Stack es hat und warum wir stolz darauf sein können.

Echter HighLoad
„Markus“ löst zahlreiche Aufgaben, wobei die Hauptaufgabe die Integration zwischen den Informationssystemen von X5 und dem staatlichen Informationssystem für die Rückverfolgbarkeit von markierten Produkten (GIS MP) ist. Die Plattform speichert auch alle erhaltenen Markierungscodes und die gesamte Historie ihrer Bewegungen zwischen Objekten, was hilft, Überbestände in den markierten Produkten zu vermeiden. Nehmen wir als Beispiel Tabakprodukte, die zu den ersten Gruppen markierter Waren gehören: Ein Lkw mit Zigaretten enthält etwa 600.000 Schachteln, von denen jede einen einzigartigen Code hat. Die Aufgabe unseres Systems besteht darin, die Legalität der Bewegungen jeder dieser Schachteln zwischen Lagern und Geschäften zu verfolgen und schließlich zu überprüfen, ob deren Verkauf an den Endverbraucher zulässig ist. Wir erfassen etwa 125.000 Kassenvorgänge pro Stunde und müssen auch dokumentieren, wie jede dieser Schachteln ins Geschäft gelangt ist. Unter Berücksichtigung aller Bewegungen zwischen den Objekten erwarten wir in einem Jahr Dutzende von Milliarden Datensätzen.
Team M
Obwohl "Markus" innerhalb von X5 als Projekt angesehen wird, erfolgt die Umsetzung nach einem produktorientierten Ansatz. Das Team arbeitet nach Scrum. Der Projektstart war im Sommer letzten Jahres, aber die ersten Ergebnisse kamen erst im Oktober – unser eigenes Team wurde vollständig zusammengestellt, die Systemarchitektur wurde entworfen und die Hardware wurde angeschafft. Aktuell besteht das Team aus 16 Personen, von denen sechs an der Entwicklung von Backend und Frontend arbeiten, drei Systemanalysen durchführen. Weitere sechs Personen sind für manuelle, lastbasierte und automatisierte Tests sowie die Produktbetreuung verantwortlich. Zudem haben wir einen SRE-Spezialisten.
Nicht nur Entwickler schreiben den Code in unserem Team, fast alle Kollegen können programmieren und erstellen automatisierte Tests, Lasttests und Skripte zur Automatisierung. Wir legen großen Wert darauf, da sogar die Produktpflege ein hohes Maß an Automatisierung erfordert. Den Kollegen, die zuvor nicht programmiert haben, versuchen wir stets, Unterstützung zu bieten und kleine Aufgaben zu geben.
Aufgrund der Coronavirus-Pandemie haben wir unser gesamtes Team ins Homeoffice versetzt. Mit allen notwendigen Tools für das Entwicklungsmanagement, einem etablierten Workflow in Jira und GitLab konnten wir diese Phase problemlos meistern. Die Monate im Homeoffice haben gezeigt, dass die Produktivität des Teams davon nicht beeinträchtigt wurde. Für viele Mitarbeiter hat sich der Komfort bei der Arbeit erhöht. Einzig fehlt der persönliche Austausch.
Team-Meeting vor dem Homeoffice

Meetings während des Homeoffice

Technologischer Stack der Lösung
Das Standard-Repository und CI/CD-Tool für X5 ist GitLab. Wir nutzen es zur Speicherung des Codes, für kontinuierliche Tests und für das Deployment auf Test- und Produktivserver. Zudem praktizieren wir Code-Reviews, bei denen mindestens zwei Kollegen die vom Entwickler vorgenommenen Änderungen am Code genehmigen müssen. Statische Code-Analyzer wie SonarQube und JaCoCo helfen uns, den Code sauber zu halten und das erforderliche Niveau an Unit-Test-Abdeckung zu gewährleisten. Alle Änderungen im Code müssen diese Prüfungen durchlaufen. Alle manuellen Testfälle werden anschließend automatisiert.
Um die Geschäftsprozesse erfolgreich mit "Markus" durchzuführen, mussten wir eine Reihe von technologischen Herausforderungen meistern, die wir der Reihe nach angehen.
Aufgabe 1. Die Notwendigkeit der horizontalen Skalierbarkeit des Systems
Für diese Herausforderung haben wir einen mikroservicebasierten Architekturansatz gewählt. Es war dabei besonders wichtig, die Verantwortungsbereiche der einzelnen Services zu verstehen. Wir haben uns bemüht, diese auf Basis der Geschäftsoperationen unter Berücksichtigung der spezifischen Prozesse zu gliedern. Die Warenannahme im Lager ist beispielsweise eine nicht sehr häufig durchgeführte, aber sehr umfangreiche Operation, bei der wir so schnell wie möglich Informationen über die akzeptierten Warenmengen, die in einer Lieferung bis zu 600.000 Einzelteile erreichen können, vom staatlichen Regulierungsbehörde erhalten müssen, um die Zulässigkeit der Annahme dieser Waren im Lager zu überprüfen und alle notwendigen Informationen dem Lagersystem zu übergeben. Im Gegensatz dazu ist die Auslagerung von Waren viel intensiver, arbeitet jedoch mit kleineren Datenmengen.
Alle unsere Dienste basieren auf dem Prinzip des Stateless und selbst interne Operationen versuchen wir in Schritte zu unterteilen, indem wir, wie wir sie nennen, self-Themen von Kafka verwenden. Das ist, wenn ein Microservice eine Nachricht an sich selbst sendet, wodurch die Last bei ressourcenintensiven Operationen besser verteilt wird und die Wartung des Produkts vereinfacht wird – dazu später mehr.
Wir haben uns entschieden, die Module zur Interaktion mit externen Systemen in separate Dienste auszulagern. Dadurch konnten wir das Problem der häufig wechselnden APIs externer Systeme lösen, ohne dabei die Dienste mit Business-Funktionalität zu beeinträchtigen.

Alle Mikrodienste werden in einem OpenShift-Cluster bereitgestellt, der sowohl das Problem der Skalierung jedes Mikrodienstes löst als auch ermöglicht, dass wir keine externen Tools für die Service Discovery verwenden müssen.
Aufgabe 2. Die Notwendigkeit, eine hohe Last und einen sehr intensiven Datenaustausch zwischen den Diensten der Plattform aufrechtzuerhalten: Zu Beginn des Projekts werden etwa 600 Operationen pro Sekunde ausgeführt. Wir erwarten, dass dieser Wert mit der Anbindung der Handelsobjekte an unsere Plattform auf 5000 ops/sec ansteigt.
Wir haben dieses Problem durch das Bereitstellen eines Kafka-Clusters gelöst und nahezu vollständig auf synchrone Interaktionen zwischen den Mikrodiensten der Plattform verzichtet. Dies erfordert eine sehr sorgfältige Analyse der Systemanforderungen, da nicht alle Operationen asynchron sein können. Dabei übermitteln wir nicht nur Ereignisse über den Broker, sondern auch alle erforderlichen Geschäftsinformationen in der Nachricht. Infolgedessen kann die Nachrichtenlänge mehrere hundert Kilobyte erreichen. Das Limit für die Nachrichtengröße in Kafka zwingt uns zu einer präzisen Prognose der Nachrichtenlängen, und wenn nötig, teilen wir sie, wobei die Teilung logisch und geschäftsorientiert ist.
Beispielsweise teilen wir Waren, die mit dem Auto angekommen sind, nach Kisten auf. Für synchronisierte Abläufe werden separate Microservices bereitgestellt und es erfolgt eine gründliche Lastprüfung. Der Einsatz von Kafka stellte uns vor eine neue Herausforderung – die Überprüfung der Funktionsweise unseres Dienstes unter Berücksichtigung der Integration von Kafka macht alle unsere Unit-Tests asynchron. Diese Aufgabe haben wir gelöst, indem wir eigene Utility-Methoden mit einem Embedded Kafka Broker erstellt haben. Dies mindert nicht die Notwendigkeit, Unit-Tests für einzelne Methoden zu schreiben, jedoch bevorzugen wir es, komplexe Fälle mithilfe von Kafka zu testen.
Wir haben viel Wert auf das Tracing von Logs gelegt, damit ihre TraceId bei Auftreten von Ausnahmen während des Betriebs der Services oder bei der Verarbeitung von Kafkabatches nicht verloren gehen. Während es bei ersterem keine besonderen Probleme gab, mussten wir im zweiten Fall alle TraceIds, die mit dem Batch ankamen, in das Log schreiben und eine für die fortgesetzte Nachverfolgung auswählen. So kann der Benutzer bei der Suche nach der ursprünglichen TraceId leicht herausfinden, mit welcher die Nachverfolgung fortgesetzt wurde.
Aufgabe 3. Die Notwendigkeit der Speicherung großer Datenmengen: Über 1 Milliarde Markierungen pro Jahr für Tabak gelangen zu X5. Dafür ist ein ständiger und schneller Zugriff erforderlich. Insgesamt muss das System ungefähr 10 Milliarden Datensätze zur Historie der Bewegung von markierten Waren verarbeiten.
Für die Lösung der dritten Aufgabe wurde die NoSQL-Datenbank MongoDB ausgewählt. Wir haben einen Shard aus 5 Knoten, und in jedem Knoten befindet sich ein Replica Set aus 3 Servern. Das ermöglicht eine horizontale Skalierung des Systems durch Hinzufügen neuer Server. in einen Cluster und dessen Ausfallsicherheit zu gewährleisten. Hier stießen wir auf ein weiteres Problem - die Sicherstellung der Transaktionsfähigkeit im MongoDB-Cluster unter Berücksichtigung der Nutzung horizontal skalierbarer Mikroservices. Ein Beispiel hierfür ist eine unserer Aufgaben, die darin besteht, Wiederverkaufsversuche von Produkten mit identischen Kennzeichnungscodes zu identifizieren. In diesem Kontext treten Probleme mit fehlerhaften Scans oder fehlerhaften Kassiervorgängen auf. Wir haben festgestellt, dass solche Duplikate sowohl innerhalb eines einzelnen verarbeiteten Kafka-Batches als auch in zwei parallel verarbeiteten Batches entstehen können. Daher führte die Überprüfung auf Duplikate durch eine Abfrage an die Datenbank zu nichts. Für jeden unserer Mikroservices haben wir das Problem entsprechend der Geschäftslogik dieses Dienstes separat gelöst. Zum Beispiel haben wir für die Belege eine Überprüfung innerhalb des Batches und eine separate Verarbeitung zur Erkennung von Duplikaten bei der Einfügung hinzugefügt.
Um sicherzustellen, dass die Nutzung der Benutzer mit Transaktionshistorien keinen Einfluss auf das Wichtigste hat – den Betrieb unserer Geschäftsprozesse – haben wir alle historischen Daten in einen separaten Dienst mit einer eigenen Datenbank ausgelagert, der ebenfalls über Kafka Informationen erhält. Auf diese Weise arbeiten die Benutzer mit einem isolierten Dienst, der die Dienste, die aktuelle Transaktionen verarbeiten, nicht beeinflusst.
Aufgabe 4. Wiederverarbeitung von Warteschlangen und Monitoring:
In verteilten Systemen treten unvermeidlich Probleme und Verfügbarkeitsfehler bei Datenbanken, Warteschlangen und externen Datenquellen auf. Im Fall von "Markus" sind externe Systemintegrationen eine Quelle solcher Fehler. Es war notwendig, eine Lösung zu finden, die es ermöglicht, fehlerhafte Antworten mit einem festgelegten Timeout erneut anzufordern, ohne gleichzeitig die Verarbeitung erfolgreicher Anfragen in der Hauptwarteschlange zu unterbrechen. Daher wurde das Konzept des "topic based retry" gewählt. Für jedes Hauptthema wird ein oder mehrere Retry-Themen erstellt, in die fehlerhafte Nachrichten geleitet werden, wobei eine Verzögerung bei der Verarbeitung der Nachrichten aus dem Hauptthema ausgeschlossen wird. Das Interaktionsschema ist —

Für die Umsetzung dieses Schemas benötigten wir Folgendes – die Integration dieser Lösung mit Spring und die Vermeidung von Code-Duplikaten. Im Internet stießen wir auf eine ähnliche Lösung, die auf Spring BeanPostProcessor basierte, allerdings erschien uns diese zu umständlich. Unser Team entwickelte eine einfachere Lösung, die sich in den Spring-Zyklus zur Erstellung von Consumer integrieren lässt und zusätzlich Retry-Consumers hinzufügt. Ein Prototyp unserer Lösung wurde dem Spring-Team vorgestellt, den Sie sich ansehen können. Die Anzahl der Retry-Consumers und die Anzahl der Versuche jedes Consumers lassen sich über Parameter konfigurieren, abhängig von den Anforderungen des Geschäftsprozesses. Damit alles funktioniert, muss lediglich die jedem Spring-Entwickler bekannte Annotation org.springframework.kafka.annotation.KafkaListener hinzugefügt werden.
Wenn eine Nachricht nach allen Wiederholungsversuchen nicht verarbeitet werden konnte, gelangt sie mithilfe des Spring DeadLetterPublishingRecoverer in das DLT (Dead Letter Topic). Auf Wunsch des Supports haben wir diese Funktionalität erweitert und einen separaten Service erstellt, der es ermöglicht, die in das DLT gelangten Nachrichten, den StackTrace, die traceId und andere nützliche Informationen dazu zu betrachten. Zudem wurden Überwachungen und Alarme für alle DLT-Topics hinzugefügt, sodass das Auftreten einer Nachricht im DLT-Topic jetzt im Grunde genommen Anlass zur Analyse und zur Erstellung eines Fehlerscheins ist. Das ist sehr praktisch – an der Bezeichnung des Topics erkennen wir sofort, an welchem Schritt des Prozesses das Problem aufgetreten ist, was die Suche nach der Hauptursache erheblich beschleunigt.

Vor kurzem haben wir eine Schnittstelle implementiert, die es unserem Support ermöglicht, Nachrichten erneut zu senden, nachdem die Ursachen behoben wurden (zum Beispiel die Wiederherstellung der Funktionalität eines externen Systems) und natürlich die entsprechende Fehlermeldung zur Analyse erstellt wurde. Hier kamen unsere Self-Topics ins Spiel, um nicht die gesamte lange Verarbeitungskette neu zu starten; stattdessen kann sie ab dem benötigten Schritt erneut gestartet werden.

Betrieb der Plattform
Die Plattform ist bereits im produktiven Einsatz. Jeden Tag führen wir Lieferungen und Versand durch und schließen neue Verteilzentren und Geschäfte an. Im Rahmen des Piloten arbeitet das System mit den Produktgruppen "Tabak" und "Schuhe".
Unser gesamtes Team ist an der Durchführung der Piloten beteiligt, analysiert auftretende Probleme und bringt Verbesserungsvorschläge für unser Produkt ein, von der Optimierung der Logs bis hin zu Änderungen in den Prozessen.
Um unsere Fehler nicht zu wiederholen, werden alle während des Piloten gefundenen Fälle in automatisierten Tests dokumentiert. Eine große Anzahl von automatisierten und Unit-Tests ermöglicht Regressionstests, sodass wir Hotfixes buchstäblich innerhalb weniger Stunden anwenden können.
Momentan entwickeln und verbessern wir unsere Plattform weiter und stehen ständig neuen Herausforderungen gegenüber. Wenn Sie interessiert sind, werden wir in den kommenden Artikeln über unsere Lösungen berichten.
Quelle: habr.com
