Für eine Schulung über Microservices, die derzeit bei Xebia entwickelt wird, haben wir Implementierungen eines Webshops sowohl in einer monolithischen als auch in einer Microservices-Architektur erstellt. Diese Beispiele haben wir dann in einer Reihe von Workshops verwendet, um eine Reihe von Microservices-Konzepten zu erklären (siehe
Zuerst haben wir uns einen monolithischen Webshop gebaut. Es handelt sich um eine einfache Anwendung, die eine Reihe von HTTP-Schnittstellen bietet, die JSON-Dokumente akzeptieren und produzieren. Sie können das Ergebnis hier
Dies war also unser Ausgangspunkt. Wie sollten wir diesen Monolithen in kleinere Teile aufteilen? Um diese Frage zu beantworten, begannen wir mit der Definition unserer Dienste in einer Post-It-gesättigten Design-Sitzung. Wir verwendeten Event Storming, um herauszufinden, wo die Grenzen unserer Dienste liegen sollten. Eine gute Anlaufstelle für die Lektüre von Event Storming ist der Blog von Ziobrando. Das Ergebnis sehen Sie auf dem Bild unten.
Wir kamen auf sieben Aggregate, die in vier Diensten in Software umgesetzt wurden: Katalog, Shop, Zahlung und Fulfillment. Wir beschlossen, dass diese vier Dienste für den Anfang ausreichen würden. Ein Dienst ruft einen anderen über REST auf und tauscht JSON-Dokumente über HTTP aus. Den Code finden Sie hier. Bei der Umstellung von einem Monolithen auf Dienste haben wir den Code des Monolithen viermal kopiert und anschließend unnötigen Code aus jedem Dienst entfernt. Dies führte anfangs zu einer gewissen Duplizierung von Klassen, aber nach und nach begannen die Domänenmodelle der Dienste auseinander zu driften. Eine Bestellung im Kontext von Payment ist wirklich sehr einfach im Vergleich zu einer Bestellung im Kontext von Shop, so dass viele Details entfernt werden können. Die JSON-Unterstützung von Spring ist sehr hilfreich, denn sie ermöglicht es Ihnen, JSON, das nicht zum Modell der Zielklasse passt, einfach zu ignorieren: Die komplexe Bestellung in Shop kann von der einfachen Bestellung in Payment geparst werden.
Obwohl die neue Lösung wahrscheinlich gut funktionieren wird, waren wir nicht ganz zufrieden. Was würde zum Beispiel passieren, wenn einer der Dienste nicht mehr verfügbar wäre? In unserer Implementierung würde das bedeuten, dass die Website nicht mehr erreichbar wäre; keine Zahlung -> keine Bestellung. Das ist schade, denn in der Praxis kann es vorkommen, dass ein Geschäft eine Bestellung annimmt und eine Rechnung schickt, die später bezahlt werden soll. Inspirationen zu Integrationsmustern finden Sie im
Um dieses Problem zu beheben, haben wir beschlossen, Warteschlangen zwischen den Diensten einzurichten: ein Dienst darf einen anderen Dienst nicht aufrufen, sondern nur Nachrichten an Warteschlangen senden. Das Ergebnis finden Sie
Die Ereignisse im Diagramm entsprechen den Ereignissen im Code: ein Dienst informiert die Welt darüber, dass etwas Interessantes passiert ist, z.B. ein neuer Auftrag abgeschlossen oder eine Zahlung eingegangen ist. Andere Dienste melden ihr Interesse an bestimmten Ereignissen an und übernehmen bei Bedarf die Verarbeitung, was dem Muster #3 in Geros Blog entspricht.
Diese Architektur ist in dem Sinne robuster, dass sie mit Verzögerungen und der Nichtverfügbarkeit von Teilen der Infrastruktur umgehen kann. Das hat allerdings seinen Preis:
- Eine Benutzeroberfläche wird immer komplexer. Die Kunden erwarten eine vollständige Ansicht ihrer Bestellung einschließlich der Zahlungsinformationen, aber diese basiert auf UI-Teilen, die von verschiedenen Diensten kontrolliert werden. Nun stellt sich die Frage, wie Sie eine Benutzeroberfläche erstellen können, die für den Endbenutzer konsistent aussieht, aber dennoch robust ist und die Grenzen der Dienste respektiert.
- Was passiert mit den von Shop gespeicherten Bestelldaten, wenn im weiteren Verlauf des Prozesses etwas schief geht? Stellen Sie sich vor, ein Kunde schließt eine Bestellung ab, bekommt eine Zahlungsschnittstelle angezeigt und zahlt dann nicht? Das bedeutet, dass Shop mit veralteten Bestellungen zurückbleiben könnte. Wir brauchen also möglicherweise eine Berichterstattung darüber, damit ein Vertriebsmitarbeiter dem Kunden nachgehen kann, oder einen Batch-Job, um alte Bestellungen einfach zu archivieren.
- Wir haben uns beim Refactoring oft verlaufen. Das Bild, das die wichtigsten Ereignisse zeigt, hat uns wirklich geholfen, auf dem richtigen Weg zu bleiben. Während dies bei Babysystemen wie unserem Beispiel schon schwer genug war, scheint es bei echter Software wirklich komplex zu sein. Mit einem Monolithen ist es einfacher zu sehen, was passiert, weil Sie Ihre IDE verwenden können, um den Weg durch den Code zu verfolgen. Wie man in größeren Systemen auf dem richtigen Weg bleibt, ist noch eine offene Frage.
Wir planen, beide Themen zu einem späteren Zeitpunkt zu untersuchen und hoffen, über unsere Ergebnisse berichten zu können.
Verfasst von

Jan Vermeir
Developing software and infrastructure in teams, doing whatever it takes to get stable, safe and efficient systems in production.
Unsere Ideen
Weitere Blogs
Contact



