Aufbau von Microservices seit dem Jahr 2019 und in Zukunft

Die Microservice-Architektur entwickelt sich ständig weiter. Lerne moderne Best Practices kennen, um gleich alles richtig zu machen.

Sten Pittet Sten Pittet

"Microservices" ist ein beliebtes, modernes organisatorisches Verfahren der Softwareentwicklung. Das Leitmotiv von Microservices ist die Entwicklung von Anwendungen durch Aufsplitten von Businesskomponenten in Services, die unabhängig bereitgestellt und betrieben werden. Die Trennung von Zuständigkeiten zwischen Services wird als "Servicegrenzen" definiert.

Servicegrenzen sind eng mit den Geschäftsanforderungen und den Grenzen der Organisationsstrukturen verbunden. Einzelne Services können an separate Teams, Budgets und Roadmaps gebunden sein. Beispiele für Servicegrenzen sind Services für Zahlungsabwicklung und Benutzerauthentifizierung. Microservices unterscheiden sich von älteren Softwareentwicklungsverfahren, bei denen alle Komponenten gebündelt wurden.

In diesem Dokument beziehen wir uns auf ein imaginäres Start-up namens "Pizzup", um die Anwendung von Microservices in einem modernen Softwareunternehmen zu veranschaulichen.

Wie erstellt man Microservices?

Schritt 1: Beginne mit einem Monolith

Die erste Best Practice für Microservices ist, dass du sie wahrscheinlich gar nicht benötigst. Wenn du keine Benutzer für deine Anwendung hast, besteht die Möglichkeit, dass sich die Geschäftsanforderungen während der Erstellung deines MVP schnell ändern werden. Dies liegt einfach an der Art der Softwareentwicklung und dem Feedbackzyklus, der stattfinden muss, während du die wichtigsten Geschäftsfunktionen identifizierst, die dein System bereitstellen muss. Microservices erhöhen den Aufwand und die Komplexität der Verwaltung exponentiell. Aus diesem Grund ist es für neue Projekte viel weniger Aufwand, den gesamten Code und die Logik in einer einzigen Codebasis zu speichern, da dies das Verschieben der Grenzen der verschiedenen Module deiner Anwendung erleichtert.

Zum Beispiel haben wir mit Pizzup vielleicht mit einer sehr grundlegenden Vorstellung von dem Problem begonnen, das wir für unsere Kunden lösen wollen: Wir möchten, dass die Leute Pizza online bestellen können.

Ein Pizzup-Benutzer sagt: "Als Benutzer kann ich Pizza online bestellen!"

Wenn wir über den Vorgang der Pizzabestellung nachdenken, werden wir zuerst die verschiedenen Grundfunktionen identifizieren, die unsere Anwendung braucht, um diesen Bedarf zu decken. Wir müssen in der Lage sein, eine Liste der verschiedenen Pizzen zu verwalten, die wir zubereiten können. Wir müssen den Kunden die Möglichkeit geben, eine oder mehrere Pizzen auszuwählen, die Zahlung abzuwickeln, die Lieferung zu planen und so weiter. Wir können unseren Kunden auch die Möglichkeit zur Erstellung eines Kontos bieten, um die nächste Bestellung bei Pizzup zu erleichtern. Nachdem wir mit unseren ersten Benutzern gesprochen haben, erfahren wir möglicherweise, dass eine Live-Verfolgung der Lieferung und die Unterstützung von Mobilgeräten uns definitiv einen Vorteil gegenüber der Konkurrenz verschaffen würde.

Eine Grafik, die den Unterschied zwischen Endbenutzern und Admin-Anwendungen für die Pizzup-App zeigt.

Was zu Beginn eine einfache Anforderung war, wird schnell zu einer Liste von Funktionen, die du bereitstellen musst.

Microservices funktionieren gut, wenn du die Rollen der verschiedenen Services, die von deinem System benötigt werden, gut verstanden hast. Sie sind viel schwieriger zu handhaben, wenn die Kernanforderungen einer Anwendung noch ausgearbeitet werden müssen. Es ist in der Tat ziemlich kostspielig, Service-Interaktionen, APIs und Datenstrukturen in Microservices neu zu definieren, da möglicherweise viel mehr bewegliche Teile koordiniert werden müssen. Aus diesem Grund empfehlen wir, die Anwendung einfach zu halten, bis du genügend Benutzerfeedback gesammelt hast, damit du sicher sein kannst, dass du die Grundbedürfnisse deiner Kunden kennst und entsprechend planen kannst.

Aber Vorsicht: Der Aufbau eines Monoliths kann schnell zu kompliziertem Code führen, der schwer in kleinere Teile zu zerlegen ist. Probiere so viel wie möglich aus, um klare Module zu identifizieren, damit du sie später aus dem Monolith extrahieren kannst. Du kannst auch damit beginnen, die Logik von deiner Web-Benutzeroberfläche zu trennen und die Interaktion über eine RESTful API über HTTP mit deinem Backend sicherzustellen. Dies wird den Übergang zu Microservices in Zukunft erleichtern, wenn du beginnst, einige der API-Ressourcen zu verschiedenen Services zu verschieben.

Schritt 2: Organisiere deine Teams auf die richtige Weise

Bisher schien es, als sei der Aufbau von Microservices größtenteils eine technische Angelegenheit. Du musst eine Codebasis in mehrere Services aufteilen, die richtigen Muster implementieren, um die Services bei Fehlern ordnungsgemäß zu beenden und nach Netzwerkproblemen wiederherzustellen, mit der Datenkonsistenz umzugehen, die Servicelast zu überwachen usw. Du musst eine Reihe neuer Konzepte verstehen lernen, aber was du unbedingt berücksichtigen musst, ist die Umstrukturierung der Organisation deiner Teams.

Jedes Unternehmen, das ein System (in seiner allgemeinen Definition) entwirft, wird ein Design erstellen, dessen Struktur eine Kopie der Kommunikationsstruktur des Unternehmens ist.

- Conway's Law

Das Gesetz von Conway hat sich in allen Arten von Teams als real erwiesen. Wenn ein Softwareteam mit einem Backend-Team, einem Frontend-Team und einem Operations-Team getrennt organisiert ist, liefern sie am Ende separate Frontend- und Backend-Monolithen, die an das Operations-Team übergeben werden, damit dieses sich in der Produktion um die Verwaltung kümmert.

Diese Art von Struktur ist nicht gut für Microservices geeignet, da jeder Service als ein eigenes Produkt angesehen werden kann, das unabhängig von den anderen ausgeliefert werden muss. Stattdessen solltest du kleinere Teams erstellen, die über alle Kompetenzen verfügen, die sowohl für die Entwicklung als auch die Wartung ihrer Services erforderlich sind. Werner Vogels, CTO von Amazon, beschrieb diese Situation mit dem Satz "you build it, you run it" (wer die Software entwickelt, ist auch für ihre Ausführung verantwortlich). Es hat große Vorteile, deine Teams auf diese Weise zu organisieren. Zunächst erhalten deine Entwickler ein besseres Verständnis für die Auswirkungen ihres Codes auf die Produktion. Dies wird dazu beitragen, einen besseren Release zu produzieren und das Risiko zu verringern, dass deine Kunden mit Problemen konfrontiert werden. Zweitens werden Deployments für jedes Team selbstverständlich, da sie in der Lage sind, an Verbesserungen des Codes sowie an der Automatisierung der Deployment-Pipeline zu arbeiten.

Schritt 3: Teile den Monolith auf, um eine Microservices-Architektur zu erstellen

Wenn du die Grenzen deiner Services identifiziert und herausgefunden hast, wie du die Kompetenzen in deinen Teams vertikaler verteilst, kannst du damit beginnen, deinen Monolith aufzuteilen, um Microservices aufzubauen. Im Folgenden findest du die wichtigsten Punkte, über die du zu diesem Zeitpunkt nachdenken solltest.

Eine RESTful API sorgt für einfache Kommunikation zwischen Services

Wenn du noch keine RESTful API verwendest, ist dies jetzt ein guter Zeitpunkt, sie in deinem System einzuführen. Wie Martin Fowler erklärt, möchtest du "intelligente Endpunkte und dumme Pipes" haben. Das Kommunikationsprotokoll zwischen deinen Services sollte so einfach wie möglich und nur für die Übertragung von Daten verantwortlich sein, ohne sie zu transformieren. Die eigentliche Magie findet an den Endpunkten statt – diese erhalten eine Anfrage, verarbeiten sie und geben im Gegenzug eine Antwort aus.

Hier unterscheiden sich Microservices von SOA, da die Komplexität des Enterprise Service Bus vermieden wird. Microservice-Architekturen streben danach, die Dinge so einfach wie möglich zu halten, um eine enge Kopplung der Komponenten zu vermeiden. In einigen Fällen kannst du eine ereignisgesteuerte Architektur mit asynchroner nachrichtenbasierter Kommunikation verwenden. Aber du solltest dir auf jeden Fall grundlegende Nachrichtenwarteschlangenservices wie RabbitMQ ansehen und vermeiden, die über das Netzwerk übertragenen Nachrichten komplexer zu machen.

Teile deine Datenstruktur auf

Es ist durchaus üblich, eine einzige Datenbank für all die verschiedenen Funktionen in einem Monolith zu haben. Wenn ein Benutzer auf seine Bestellung zugreift, siehst du direkt in der Benutzertabelle nach, um die Kundeninformationen anzuzeigen, und dieselbe Tabelle könnte verwendet werden, um die vom Abrechnungssystem verwaltete Rechnung zu befüllen. Dies scheint logisch und einfach zu sein, aber mit Microservices sollen diese Services entkoppelt werden – damit auf Rechnungen auch dann zugegriffen werden kann, wenn das Bestellsystem nicht verfügbar ist – und weil du damit die Rechnungstabelle unabhängig von anderen Services optimieren oder weiterentwickeln kannst. Dies bedeutet, dass jeder Service möglicherweise über einen eigenen Datenspeicher verfügt, um die Daten zu erhalten, die er benötigt.

Hierdurch entstehen offensichtlich neue Probleme, da letztendlich einige Daten in verschiedenen Datenbanken dupliziert werden. In diesem Fall solltest du Konsistenz anstreben und eine ereignisgesteuerte Architektur einführen, um die serviceübergreifende Synchronisierung von Daten zu unterstützen. Zum Beispiel können deine Abrechnungs- und Lieferverfolgungsservices auf Ereignisse des Kontoservices reagieren, wenn ein Kunde seine persönlichen Daten aktualisiert. Nachdem die Services dieses Ereignis empfangen haben, aktualisieren sie ihren Datenspeicher entsprechend. Diese ereignisgesteuerte Architektur ermöglicht es, die Kontoservicelogik einfach zu halten, da sie nicht alle anderen abhängigen Services kennen muss. Sie teilt dem System einfach mit, was geschehen ist, und die anderen Services reagieren entsprechend darauf.

Du kannst auch alle Kundeninformationen im Kontoservice und nur eine Fremdschlüsselreferenz in deinem Abrechnungs- und Lieferservice aufbewahren. Du interagierst dann mit dem Kontodienst, um bei Bedarf die relevanten Kundendaten zu erhalten, anstatt vorhandene Datensätze zu duplizieren. Es gibt keine universellen Lösungen für diese Probleme, und du musst dich mit jedem Einzelfall befassen, um festzustellen, welcher Ansatz jeweils der beste ist.

Plane beim Aufbau deiner Microservices-Architektur das Auftreten von Fehlern ein

Wir haben gesehen, wie Microservices gegenüber einer monolithischen Architektur große Vorteile bieten können. Sie sind kleiner und spezialisiert, was sie leicht verständlich macht. Sie sind entkoppelt, was bedeutet, dass du einen Service refaktorieren kannst, ohne befürchten zu müssen, die anderen Komponenten des Systems zu beschädigen oder die Entwicklung der anderen Teams zu verlangsamen. Sie bieten deinen Entwicklern auch mehr Flexibilität, da sie bei Bedarf verschiedene Technologien auswählen können, ohne durch die Anforderungen anderer Services eingeschränkt zu werden.

Kurz gesagt, eine Microservice-Architektur erleichtert die Entwicklung und Aufrechterhaltung jeder Geschäftsfähigkeit. Wie alle Services in ihrer Gesamtheit miteinander interagieren, um Aktionen durchzuführen, ist jedoch etwas komplizierter. Dein System ist jetzt verteilt und verfügt über mehrere Schwachstellen, die du berücksichtigen musst. Dabei geht es nicht nur um fehlende Reaktionen eines Services, sondern auch um den Umgang mit langsameren Reaktionen im Netzwerk. Die Wiederherstellung nach einem Fehler kann ebenfalls manchmal schwierig sein, da du sicherstellen musst, dass Services, die wieder in Betrieb gehen und ausgeführt werden, nicht durch ausstehende Nachrichten überflutet werden.

Wenn du damit beginnst, Funktionen aus deinen monolithischen Systemen zu extrahieren, darfst du auf keinen Fall vergessen, von Anfang an den Umgang mit Fehlern einzuplanen.

Lege Wert auf Überwachung, um das Testen von Microservices zu erleichtern

Tests sind ein weiterer Nachteil von Microservices im Vergleich zu einem monolithischen System. Eine Anwendung, die als eine einzige Codebasis erstellt wird, benötigt nicht viel, um eine Testumgebung in Betrieb zu nehmen. In den meisten Fällen musst du einen Backend-Server in Verbindung mit einer Datenbank starten, um deine Testsuite ausführen zu können.

In der Welt der Microservices ist dies nicht so einfach. Komponententests dürften sich nicht allzu sehr von denen des Monoliths unterscheiden und sollten dir keine Probleme bereiten. Integrations- und Systemtests sind jedoch schwieriger. Unter Umständen musst du mehrere Services zusammen starten, verschiedene Datenspeicher in Betrieb nehmen, und deine Konfiguration muss möglicherweise Nachrichtenwarteschlangen enthalten, die bei deinem Monolith nicht erforderlich waren. In dieser Situation wird es viel kostspieliger, Funktionstests durchzuführen, und die zunehmende Anzahl von beweglichen Teilen macht es sehr schwierig, die verschiedenen Arten von Fehlern vorherzusagen, die auftreten können.

Aus diesem Grund musst du großen Wert auf die Überwachung legen, um Probleme frühzeitig erkennen und entsprechend reagieren zu können. Du musst die Grundlagen deiner verschiedenen Services verstehen und in der Lage sein, nicht nur zu reagieren, wenn sie ausfallen, sondern auch, wenn sie sich unerwartet verhalten. Ein Vorteil der Einführung einer Microservice-Architektur besteht darin, dass dein System gegen teilweisen Ausfall widerstandsfähig sein sollte. Wenn du also Anomalien im Lieferverfolgungsservice unserer Pizzup-Anwendung feststellst, ist dies weniger schlimm als in einem monolithischen System. Unsere Anwendung sollte so gestaltet sein, dass alle anderen Services ordnungsgemäß reagieren und unsere Kunden Pizzen bestellen können, während wir die Lieferverfolgung wiederherstellen.

Nutze Continuous Delivery für reibungslosere Deployments

Die manuelle Freigabe eines monolithischen Systems in die Produktion ist mühsam und riskant, aber nicht unmöglich. Natürlich empfehlen wir diesen Ansatz nicht und ermutigen jedes Softwareteam, Continuous Delivery für alle Entwicklungsarbeiten zu nutzen, aber zu Beginn eines Projekts kannst du deine ersten Deployments selbst über die Befehlszeile durchführen.

Dieser Ansatz ist nicht nachhaltig, wenn du eine zunehmende Anzahl von Services hast, die mehrmals täglich bereitgestellt werden müssen. Daher ist es im Rahmen deiner Umstellung auf Microservices von entscheidender Bedeutung, dass du Continuous Delivery einführst, um das Risiko eines Release-Fehlers zu reduzieren und sicherzustellen, dass sich dein Team auf die Entwicklung und den Betrieb der Anwendung konzentriert, anstatt sich mit dem Deployment aufzuhalten. Continuous Delivery bedeutet auch, dass dein Service Akzeptanztests bestanden hat, bevor er in die Produktion geht – natürlich treten Fehler auf, aber im Laufe der Zeit entsteht eine robuste Testsuite, sodass dein Team zunehmend Vertrauen in die Qualität der Releases haben kann.

Das Ausführen von Microservices ist kein Sprint

Microservices entwickeln sich gerade schnell zu einer beliebten und weit verbreiteten Best Practice der Branche. Für komplexe Projekte bieten sie eine größere Flexibilität bei der Art und Weise, wie Software entwickelt und bereitgestellt wird. Sie helfen auch bei der Identifizierung und Formalisierung der Geschäftskomponenten deines Systems, was nützlich ist, wenn mehrere Teams an derselben Anwendung arbeiten. Es gibt jedoch auch einige eindeutige Nachteile bei der Verwaltung verteilter Systeme, und die Aufteilung einer monolithischen Architektur sollte nur erfolgen, wenn ein klares Verständnis der Servicegrenzen besteht.

Der Aufbau von Microservices sollte als Weg und nicht als unmittelbares Ziel für ein Team angesehen werden. Fange klein an, um die technischen Anforderungen eines verteilten Systems kennenzulernen, und zu verstehen, wie Services beim Auftreten von Fehlern ordnungsgemäß beendet und einzelne Komponenten skaliert werden können. Mit zunehmender Erfahrung und Kenntnis kannst du nach und nach immer mehr Services extrahieren.

Die Migration in eine Microservices-Architektur muss nicht auf einen Schlag durchgeführt werden. Eine iterative Strategie zur sequenziellen Migration kleinerer Komponenten zu Microservices ist sicherer. Identifiziere die am besten definierten Servicegrenzen innerhalb einer etablierten Monolith-Anwendung und arbeite iterativ daran, sie in ihren eigenen Microservice zu entkoppeln.

Zusammenfassung

Zusammenfassend lässt sich sagen, dass Microservices eine Strategie ist, die sowohl für den Entwicklungsprozess des technischen Codes als auch für die allgemeine Strategie der Geschäftsorganisation von Vorteil ist. Microservices helfen dabei, Teams in Einheiten zu organisieren, die sich auf die Entwicklung und den Besitz bestimmter Geschäftsfunktionen konzentrieren. Dieser granulare Fokus verbessert die allgemeine Geschäftskommunikation und -effizienz. Die Vorteile von Microservices bringen aber auch einige Nachteile mit sich. Es ist wichtig, dass Servicegrenzen vor der Migration auf eine Microservice-Architektur klar definiert sind. Die Microservice-Architektur ist noch ziemlich jung, aber sie ist eine vielversprechende Art, Anwendungen zu entwickeln, und es lohnt sich auf jeden Fall, sich mit ihr zu beschäftigen. Behalte jedoch im Hinterkopf, dass sie möglicherweise (noch) nicht für dein Team geeignet ist.