Monolithische Anwendung in Mikrodienste refaktorieren

Last reviewed 2024-06-26 UTC

Dieser Referenzleitfaden ist der zweite in einer vierteiligen Reihe zum Entwerfen, Erstellen und Bereitstellen von Mikrodiensten. In dieser Reihe werden die verschiedenen Elemente einer Mikrodienstarchitektur beschrieben. Die Reihe enthält Informationen zu den Vor- und Nachteilen des Mikrodienstarchitekturmusters und zu ihrer Anwendung.

  1. Einführung in Mikrodienste
  2. Monolithische Anwendung in Mikrodienste refaktorieren (dieses Dokument)
  3. Kommunikation zwischen Diensten in einem Mikrodienst-Setup
  4. Verteiltes Tracing in Mikrodienstanwendungen

Diese Serie richtet sich an Anwendungsentwickler und Architekten, die Migrationen entwerfen und implementieren, um eine monolithische Anwendung in eine Mikrodienstanwendung zu refaktorieren.

Die Umwandlung einer monolithischen Anwendung in Mikrodienste ist eine Form der Anwendungsmodernisierung. Um eine Anwendungsmodernisierung zu erreichen, empfehlen wir, nicht den gesamten Code gleichzeitig zu refaktorieren. Stattdessen empfehlen wir, Ihre monolithische Anwendung inkrementell zu refaktorieren. Wenn Sie eine Anwendung inkrementell refaktorieren, erstellen Sie nach und nach eine neue Anwendung aus Mikrodiensten und führen die Anwendung zusammen mit der monolithischen Anwendung aus. Dieser Ansatz wird auch als Strangler Fig-Muster bezeichnet. Im Laufe der Zeit verkleinert sich die von der monolithischen Anwendung implementierte Funktionalität, bis sie entweder vollständig verschwindet oder zu einem anderen Mikrodienst wird.

Wenn Sie Funktionen von einer monolithischen Anwendung trennen möchten, müssen Sie die Daten, die Logik und die für den Nutzer sichtbaren Komponenten der Funktion sorgfältig extrahieren und an den neuen Dienst weiterleiten. Es ist wichtig, dass Sie mit dem Problembereich vertraut sind, bevor Sie in den Lösungsbereich wechseln.

Wenn Sie den Problembereich verstehen, verstehen Sie den natürlichen Grenzen der Domain, die die richtige Maß an Isolation bieten. Wir empfehlen Ihnen, größere Dienste anstelle von kleineren Diensten zu erstellen, bis Sie die Domain sehr gut durchblicken.

Die Definition von Dienstgrenzen ist ein iterativer Prozess. Da dieser Prozess viel Arbeit erfordert, müssen Sie kontinuierlich die Kosten für die Entkoppelung mit den Vorteile abwägen, die Sie erhalten. Im Folgenden finden Sie Faktoren, die Ihnen bei der Bewertung der Durchführung der Entkoppelung einer monolithischen Anwendung helfen können:

  • Vermeiden Sie es, alles auf einmal zu refaktorieren. Zur Priorisierung der Dienstentkoppelung müssen Sie die Kosten mit den Vorteilen abwägen.
  • Dienste in einer Mikrodienstarchitektur richten sich nach geschäftlichen Aspekten und nicht nach technischen Aspekten.
  • Wenn Sie Dienste schrittweise migrieren, konfigurieren Sie die Kommunikation zwischen Diensten und der monolithischen Anwendung, um klar definierte API-Verträge zu durchzugehen.
  • Mikrodienste erfordern viel mehr Automatisierung: Denken Sie im Voraus an Continuous Integration (CI), Continuous Deployment (CD), zentrales Logging und Monitoring.

In den folgenden Abschnitten werden verschiedene Strategien zum Entkoppeln von Diensten und zum schrittweisen Migrieren Ihrer monolithischen Anwendung erläutert.

Durch domaingesteuertes Design entkoppeln

Mikrodienste sollten auf Geschäftsfunktionen anstatt auf horizontale Schichten wie Datenzugriff oder Messaging ausgerichtet sein. Mikrodienste sollten auch eine lose Kopplung und eine hohe funktionale Kohäsion haben. Mikrodienste sind lose gekoppelt, wenn Sie einen Dienst ändern können, ohne dass andere Dienste gleichzeitig aktualisiert werden müssen. Ein Mikrodienst ist kohäsiv, wenn er einen einzigen, klar definierten Zweck hat, z. B. die Verwaltung von Nutzerkonten oder die Verarbeitung von Zahlungen.

Das domaingesteuerte Design (DDD) erfordert ein gutes Verständnis der Domain, für die die Anwendung geschrieben wird. Das erforderliche Domainwissen zum Erstellen der Anwendung befindet sich bei den entsprechenden Personen – den Domainexperten.

So können Sie den DDD-Ansatz rückwirkend auf eine vorhandene Anwendung anwenden:

  1. Identifizieren Sie eine ubiquitäre Sprache – ein gemeinsames Vokabular, das von allen Stakeholdern gemeinsam genutzt wird. Als Entwickler ist es wichtig, in Ihrem Code Begriffe zu verwenden, die eine nichttechnische Person verstehen kann. Was Ihr Code zu erreichen versucht, sollte eine Widerspiegelung Ihrer Unternehmensprozesse sein.
  2. Ermitteln Sie die relevanten Module in der monolithischen Anwendung und wenden Sie dann das gemeinsame Vokabular auf diese Module an.
  3. Definieren Sie Bounded Contexts, bei denen Sie explizite Grenzen auf die identifizierten Module mit klar definierten Verantwortlichkeiten anwenden. Die von Ihnen identifizierten Bounded Contexts sind Kandidaten, die in kleinere Mikrodienste refaktoriert werden können.

Das folgende Diagramm zeigt, wie Sie Bounded Contexts auf eine vorhandene E-Commerce-Anwendung anwenden können:

Bounded Contexts werden auf eine Anwendung angewendet.

Abbildung 1. Anwendungsfunktionen werden in Bounded Contexts unterteilt, die zu Diensten migriert werden.

In Abbildung 1 sind die Funktionen der E-Commerce-Anwendung in Bounded Contexts unterteilt und werden so in Dienste umgewandelt:

  • Funktionen zur Bestellungsverwaltung und Auftragsausführung sind in folgende Kategorien gebunden:
    • Die Funktion zur Bestellungsverwaltung wird zum Bestelldienst migriert.
    • Die Funktionen für die Logistikauslieferungsverwaltung werden zum Auslieferungsdienst migriert.
    • Die Inventarfunktion wird zum Inventardienst migriert.
  • Abrechnungsfunktionen werden in eine einzelne Kategorie gebunden:
    • Die Verbraucher-, Verkäufer- und Drittanbieterfunktionen werden miteinander verbunden und zum Kontodienst migriert.

Dienste für die Migration priorisieren

Ein idealer Ausgangspunkt zum Entkoppeln von Diensten besteht darin, die lose gekoppelten Module in der monolithischen Anwendung zu identifizieren. Sie können ein lose gekoppeltes Modul als einen der ersten Kandidaten für die Konvertierung in einen Mikrodienst auswählen. Sehen Sie sich Folgendes an, um eine Abhängigkeitsanalyse für jedes Modul durchzuführen:

  • Die Art der Abhängigkeit – Abhängigkeiten von Daten oder anderen Modulen
  • Den Umfang der Abhängigkeit – wie sich eine Änderung im identifizierten Modul auf andere Module auswirken könnte

Das Migrieren eines Moduls mit umfassenden Datenabhängigkeiten ist in der Regel keine triviale Aufgabe. Wenn Sie zuerst Features migrieren und die zugehörigen Daten später migrieren, kann es vorübergehend sein, dass Daten aus mehreren Datenbanken gelesen und in mehrere Datenbanken geschrieben werden. Daher müssen Sie die Herausforderungen der Datenintegrität und Synchronisierung berücksichtigen.

Es wird empfohlen, Module zu extrahieren, die im Vergleich zum Rest der monolithischen Anwendung unterschiedliche Ressourcenanforderungen haben. Wenn ein Modul beispielsweise eine In-Memory-Datenbank hat, können Sie diese in einen Dienst umwandeln, der dann auf Hosts mit mehr Arbeitsspeicher bereitgestellt werden kann. Wenn Sie Module mit bestimmten Ressourcenanforderungen in Dienste umwandeln, können Sie die Skalierung Ihrer Anwendung erheblich vereinfachen.

Aus betrieblicher Sicht bedeutet das Refaktorieren eines Moduls in seinen eigenen Dienst auch, dass Ihre vorhandenen Teamstrukturen angepasst werden müssen. Der beste Weg, um die Verantwortlichkeit zu stärken, besteht darin, kleine Teams, die für einen gesamten Dienst zuständig sind, entsprechend zu befähigen.

Weitere Faktoren, die sich auf die Priorisierung von Diensten für die Migration auswirken können, sind geschäftliche Relevanz, umfassende Testabdeckung, Aufstellung in Sachen Sicherheit der Anwendung und Gewinnung der Zustimmung der Organisationsmitarbeiter. Anhand Ihrer Bewertungen können Sie die Dienste wie im ersten Dokument dieser Reihe beschrieben, in Bezug auf deren Vorteil durch die Refaktorierung sortieren.

Dienst aus einer monolithischen Anwendung extrahieren

Nachdem Sie den idealen Dienstkandidaten ermittelt haben, müssen Sie festlegen, wie sowohl der Mikrodienst als auch monolithische Module koexistieren können. Eine Möglichkeit, diese Koexistenz zu verwalten, ist die Einführung eines IPC-Adapters (Inter-Process Communication), der bei der Zusammenarbeit der Module helfen kann. Mit der Zeit übernimmt der Mikrodienst die Last und die monolithische Komponente wird beseitigt. Dieser inkrementelle Prozess verringert das Risiko, wenn Sie von der monolithischen Anwendung auf den neuen Mikrodienst umstellen, da Sie Fehler oder Leistungsprobleme nach und nach erkennen können.

Das folgende Diagramm zeigt, wie der IPC-Ansatz implementiert wird:

Ein IPC-Ansatz wird implementiert, damit Module zusammenarbeiten können.

Abbildung 2. Ein IPC-Adapter koordiniert die Kommunikation zwischen der monolithischen Anwendung und einem Mikrodienstmodul.

In Abbildung 2 ist Modul Z der Dienstkandidat, den Sie aus der monolithischen Anwendung extrahieren möchten. Die Module X und Y hängen von Modul Z ab. Die Mikrodienstmodule X und Y verwenden einen IPC-Adapter in der monolithischen Anwendung, um über eine REST API mit Modul Z zu kommunizieren.

Im nächsten Dokument dieser Reihe, Zwischen-Diensten-Kommunikation in einer Mikrodienst-Einrichtung, wird das Strangler Fig-Muster beschrieben und wie ein Dienst aus der monolithischen Anwendung dekonstruiert wird.

Monolithische Datenbank verwalten

In der Regel haben monolithische Anwendungen eigene monolithische Datenbanken. Eines der Prinzipien einer Mikrodienstarchitektur besteht darin, für jeden Mikrodienst eine eigene Datenbank zu haben. Wenn Sie Ihre monolithische Anwendung in Mikrodienste modernisieren, müssen Sie dann die monolithische Datenbank basierend auf den von Ihnen identifizierten Dienstgrenzen aufteilen.

Analysieren Sie zuerst die Datenbankzuordnungen, um zu ermitteln, wo eine monolithische Datenbank aufgeteilt werden soll. Im Rahmen der Dienstextraktionsanalyse haben Sie einige Erkenntnisse zu den Mikrodiensten gesammelt, die Sie erstellen müssen. Mit dem gleichen Ansatz können Sie auch die Datenbanknutzung analysieren und Tabellen oder andere Datenbankobjekte den neuen Mikrodiensten zuordnen. Tools wie SchemaCrawler, SchemaSpy und ERBuilder können Ihnen bei einer solchen Analyse helfen. Durch das Zuordnen von Tabellen und anderen Objekten können Sie die Kopplung zwischen Datenbankobjekten besser verstehen, die sich über Ihre potenziellen Mikrodienstgrenzen erstrecken.

Das Aufteilen einer monolithischen Datenbank ist jedoch komplex, da es unter Umständen keine klare Trennung zwischen Datenbankobjekten gibt. Sie müssen auch andere Probleme wie Datensynchronisierung, transaktionale Integrität, Joins und Latenz berücksichtigen. Im nächsten Abschnitt werden Muster beschrieben, mit denen Sie auf diese Probleme reagieren können, wenn Sie Ihre monolithische Datenbank aufteilen.

Tabellen referenzieren

Bei monolithischen Anwendungen ist es üblich, dass Module über einen SQL-Join zur Tabelle des anderen Moduls auf erforderliche Daten von einem anderen Modul zugreifen. Das folgende Diagramm zeigt anhand des vorherigen Beispiels für die E-Commerce-Anwendung diesen SQL-Join-Zugriffsprozess:

Ein Modul verwendet einen SQL-Join, um auf Daten aus einem anderen Modul zuzugreifen.

Abbildung 3. Ein Modul führt Daten mit der Tabelle eines anderen Moduls per Join zusammen.

In Abbildung 3 verwendet ein Bestellmodul einen Fremdschlüssel product_id, um eine Bestellung mit der Produkttabelle per Join zusammenzuführen, um Produktinformationen abzurufen.

Wenn Sie Module jedoch als einzelne Dienste dekonstruieren, empfehlen wir Ihnen, den Bestelldienst die Datenbank des Produktdienstes nicht direkt aufrufen zu lassen, um einen Join-Vorgang auszuführen. In den folgenden Abschnitten werden Optionen beschrieben, mit denen Sie die Datenbankobjekte trennen können.

Daten über eine API teilen

Wenn Sie die Kernfunktionen oder Module in Mikrodienste aufteilen, verwenden Sie in der Regel APIs zum Freigeben und Bereitstellen von Daten. Der referenzierte Dienst stellt Daten als API bereit, die der aufrufende Dienst benötigt, wie im folgenden Diagramm dargestellt:

Daten werden über eine API bereitgestellt.

Abbildung 4. Ein Dienst ruft mit einem API-Aufruf Daten von einem anderen Dienst ab.

In Abbildung 4 verwendet ein Bestellmodul einen API-Aufruf, um Daten aus einem Produktmodul abzurufen. Diese Implementierung hat aufgrund zusätzlicher Netzwerk- und Datenbankaufrufe offensichtliche Leistungsprobleme. Die Freigabe von Daten über eine API funktioniert jedoch gut, wenn die Datengröße begrenzt ist. Wenn der aufgerufene Dienst Daten mit einer bekannten Änderungsrate zurückgibt, können Sie einen lokalen TTL-Cache für den Aufrufer implementieren, um Netzwerkanfragen an den aufgerufenen Dienst zu reduzieren.

Daten replizieren

Eine weitere Möglichkeit, Daten zwischen zwei separaten Mikrodiensten zu teilen, besteht darin, Daten in der abhängigen Dienstdatenbank zu replizieren. Die Datenreplikation ist schreibgeschützt und kann jederzeit neu erstellt werden. Dieses Muster ermöglicht einen kohäsiveren Dienst. Das folgende Diagramm zeigt, wie die Datenreplikation zwischen zwei Mikrodiensten funktioniert:

Daten werden zwischen Mikrodiensten repliziert.

Abbildung 5. Daten aus einem Dienst werden in einer abhängigen Dienstdatenbank repliziert.

In Abbildung 5 wird die Datenbank des Produktdienstes in die Datenbank des Bestelldienstes repliziert. Mit dieser Implementierung kann der Bestelldienst Produktdaten ohne wiederholte Aufrufe an den Produktdienst abrufen.

Sie können Datenreplikation mit Methoden wie materialisierten Ansichten, Change Data Capture (CDC) und Ereignisbenachrichtigungen erstellen. Die replizierten Daten unterliegen der Eventual Consistency. Bei der Replikation von Daten kann es aber zu Verzögerungen kommen. Daher besteht das Risiko, dass veraltete Daten bereitgestellt werden.

Statische Daten als Konfiguration

Statische Daten wie Ländercodes und unterstützte Währungen ändern sich nur langsam. Sie können solche statischen Daten als Konfiguration in einen Mikrodienst einfügen. Moderne Mikrodienste und Cloud-Frameworks bieten Features zur Verwaltung solcher Konfigurationsdaten mithilfe von Konfigurationsservern, Schlüssel/Wert-Speichern und Vaults. Sie können diese Features deklarativ einschließen.

Geteilte änderbare Daten

Monolithische Anwendungen haben ein häufiges Muster, das als geteilter änderbarer Status bezeichnet wird. In einer geteilten änderbarer Status-Konfiguration verwenden mehrere Module eine einzelne Tabelle, wie im folgenden Diagramm dargestellt:

Eine geteilter änderbarer Status-Konfiguration stellt eine einzelne Tabelle für mehrere Module zur Verfügung.

Abbildung 6. Mehrere Module verwenden eine einzige Tabelle.

In Abbildung 6 verwenden die Funktionen für Bestellung, Zahlung und Versand der E-Commerce-Anwendung dieselbe ShoppingStatus-Tabelle, um den Bestellstatus des Kunden während des gesamten Kaufprozesses beizubehalten.

Zur Migration einer gemeinsam genutzten monolithischen Anwendung mit veränderlichem Status können Sie einen separaten ShoppingStatus-Mikrodienst entwickeln, um die ShoppingStatus-Datenbanktabelle zu verwalten. Dieser Mikrodienst stellt APIs bereit, um den Einkaufsstatus eines Kunden zu verwalten, wie im folgenden Diagramm dargestellt:

APIs sind für andere Dienste verfügbar gemacht.

Abbildung 7. Ein Mikrodienst stellt APIs für mehrere andere Dienste bereit.

In Abbildung 7 verwenden die Mikrodienste für Zahlung, Bestellung und Versand die ShoppingStatus-Mikrodienst-APIs. Wenn die Datenbanktabelle eng mit einem der Dienste verbunden ist, empfehlen wir, die Daten in diesen Dienst zu verschieben. Sie können die Daten dann über eine API bereitstellen, damit andere Dienste sie nutzen können. Mit dieser Implementierung können Sie dafür sorgen, dass Sie nicht zu viele detaillierte Dienste haben, die sich häufig gegenseitig aufrufen. Wenn Sie Dienste falsch aufteilen, müssen Sie die Definition der Dienstgrenzen neu prüfen.

Verteilte Transaktionen

Nachdem Sie den Dienst von der monolithischen Anwendung isoliert haben, wird eine lokale Transaktion im ursprünglichen monolithischen System möglicherweise auf mehrere Dienste verteilt. Eine Transaktion, die sich über mehrere Dienste erstreckt, wird als verteilte Transaktion betrachtet. In der monolithischen Anwendung sorgt das Datenbanksystem dafür, dass die Transaktionen atomar sind. Zum Verarbeiten von Transaktionen zwischen verschiedenen Diensten in einem Mikrodienstsystem müssen Sie einen globalen Transaktionskoordinator erstellen. Der Transaktionskoordinator verarbeitet Rollbacks, Kompensationsaktionen und andere Transaktionen, die im nächsten Dokument dieser Reihe, Kommunikation zwischen Diensten in einem Mikrodienst-Setup, beschrieben werden.

Datenkonsistenz

Verteilte Transaktionen bringen die Herausforderung mit sich, die Datenkonsistenz über Dienste hinweg aufrechtzuerhalten. Alle Aktualisierungen müssen in atomaren Schritten durchgeführt werden. In einer monolithischen Anwendung garantieren die Attribute von Transaktionen, dass eine Abfrage auf Basis der Isolationsebene eine konsistente Ansicht der Datenbank zurückgibt.

Betrachten Sie im Gegensatz dazu eine mehrstufige Transaktion in einer auf Mikrodiensten basierenden Architektur. Wenn eine Diensttransaktion fehlschlägt, müssen die Daten durch einen Rollback der Schritte abgeglichen werden, die für die anderen Dienste erfolgreich waren. Andernfalls ist die globale Ansicht der Anwendungsdaten zwischen den Diensten inkonsistent.

Es kann schwierig sein zu ermitteln, wo ein Schritt fehlgeschlagen ist, der die Eventual Consistency implementiert. Beispielsweise könnte ein Schritt nicht sofort fehlschlagen, sondern blockieren oder eine Zeitüberschreitung auslösen. Daher müssen Sie möglicherweise eine Art Zeitüberschreitungsmechanismus implementieren. Wenn die doppelten Daten beim Zugriff durch den aufgerufenen Dienst veraltet sind, kann das Caching oder die Replikation von Daten zwischen Diensten, um die Netzwerklatenz zu reduzieren, zu inkonsistenten Daten führen.

Das nächste Dokument der Reihe, Zwischen-Diensten-Kommunikation in einer Mikrodienstkonfiguration, enthält ein Beispiel für ein Muster zum Verarbeiten verteilter Transaktionen über Mikrodienste hinweg.

Kommunikation zwischen Diensten entwerfen

In einer monolithischen Anwendung rufen sich Komponenten oder Anwendungsmodule gegenseitig direkt über Funktionsaufrufe auf. Im Gegensatz dazu besteht eine auf Mikrodiensten beruhende Anwendung aus mehreren Diensten, die über das Netzwerk miteinander interagieren.

Wenn Sie die Kommunikation zwischen Diensten entwerfen, überlegen Sie zuerst, wie Dienste miteinander interagieren sollen. Dienstinteraktionen können Folgendes sein:

  • 1:1-Interaktion: Jede Clientanfrage wird von genau einem Dienst verarbeitet.
  • 1:n-Interaktionen: Jede Anfrage wird von mehreren Diensten verarbeitet.

Überlegen Sie sich auch, ob die Interaktion synchron oder asynchron ist:

  • Synchron: Der Client erwartet eine zeitnahe Antwort vom Dienst und kann blockiert werden, während er wartet.
  • Asynchron: Der Client blockiert nicht, während er auf eine Antwort wartet. Die Antwort, falls vorhanden, wird nicht unbedingt sofort gesendet.

Die folgende Tabelle zeigt Kombinationen von Interaktionsarten:

1:1-Beziehung Eins-zu-N
Synchron Anfrage und Antwort: Anfrage an einen Dienst senden und auf eine Antwort warten.
Asynchron Benachrichtigung: Anfrage an einen Dienst senden, aber es wird keine Antwort erwartet oder gesendet. Veröffentlichen und abonnieren: Der Client veröffentlicht eine Benachrichtigung und null oder mehr interessierte Dienste verarbeiten sie.
Anfrage und asynchrone Antwort: Anfrage an einen Dienst senden, der asynchron antwortet. Der Client blockiert nicht. Veröffentlichen und asynchrone Antworten: Der Client veröffentlicht eine Anfrage und wartet auf Antworten von interessierten Diensten.

Jeder Dienst verwendet in der Regel eine Kombination dieser Interaktionsarten.

Kommunikation zwischen Diensten implementieren

Sie können zwischen verschiedenen IPC-Technologien wählen, um die Kommunikation zwischen Diensten zu implementieren. Dienste können beispielsweise synchrone Anfrage-Antwort-basierte Kommunikationsmechanismen wie HTTP-basiertes REST, gRPC oder Thrift verwenden. Alternativ können Dienste asynchrone, nachrichtenbasierte Kommunikationsmechanismen wie AMQP oder STOMP verwenden. Sie können auch aus verschiedenen Nachrichtenformaten auswählen. Dienste können beispielsweise für Menschen lesbare, textbasierte Formate wie JSON oder XML verwenden. Alternativ können Dienste ein binäres Format wie Avro oder Protocol Buffers verwenden.

Wenn Sie Dienste so konfigurieren, dass andere Dienste direkt aufgerufen werden, kommt es zu einer hohen Kopplung zwischen Diensten. Stattdessen empfehlen wir die Verwendung von Messaging oder ereignisbasierter Kommunikation:

  • Messaging: Wenn Sie Messaging implementieren, müssen Dienste sich nicht mehr direkt gegenseitig aufrufen. Stattdessen wissen alle Dienste von einem Nachrichten-Broker und sie senden Nachrichten an diesen Broker. Der Nachrichten-Broker speichert diese Nachrichten in einer Nachrichtenwarteschlange. Andere Dienste können die für sie relevanten Nachrichten abonnieren.
  • Ereignisbasierte Kommunikation: Wenn Sie die ereignisgesteuerte Verarbeitung implementieren, erfolgt die Kommunikation zwischen Diensten über Ereignisse, die von einzelnen Diensten generiert werden. Einzelne Dienste schreiben ihre Ereignisse in einen Nachrichten-Broker. Dienste können die relevanten Ereignisse verfolgen. Bei diesem Muster sind Dienste lose gekoppelt, da die Ereignisse keine Nutzlasten enthalten.

In einer Mikrodienstanwendung empfehlen wir die asynchrone Kommunikation zwischen Diensten anstelle der synchronen Kommunikation. Das Anfrage-Antwort-Muster ist ein bekanntes Architekturmuster. Daher scheint das Entwerfen einer synchronen API möglicherweise natürlicher als die Entwicklung eines asynchronen Systems. Die asynchrone Kommunikation zwischen Diensten kann über Messaging oder eine ereignisgesteuerte Kommunikation implementiert werden. Die asynchrone Kommunikation bietet folgende Vorteile:

  • Lose Kopplung: Ein asynchrones Modell teilt die Anfrage-Antwort-Interaktion in zwei separate Nachrichten auf, eine für die Anfrage und eine für die Antwort. Der Nutzer eines Dienstes initiiert die Anfragenachricht und wartet auf die Antwort und der Dienstanbieter wartet auf Anfragenachrichten, auf die er mit Antwortnachrichten antwortet. Der Aufrufer muss dabei nicht auf die Antwortnachricht warten.
  • Fehlerisolation: Der Sender kann weiter Nachrichten senden, auch wenn der nachgelagerte Nutzer fehlschlägt. Der Nutzer holt den Rückstand ab, wenn er wiederhergestellt wird. Diese Möglichkeit ist in einer Mikrodienstarchitektur besonders nützlich, da jeder Dienst einen eigenen Lebenszyklus hat. Synchrone APIs erfordern jedoch, dass der nachgelagerte Dienst verfügbar ist. Andernfalls schlägt der Vorgang fehl.
  • Reaktionsfähigkeit: Ein vorgelagerter Dienst kann schneller antworten, wenn er nicht auf nachgelagerte Dienste wartet. Wenn eine Kette von Dienstabhängigkeiten vorhanden ist (Dienst A ruft B auf, der C aufruft usw.), kann das Warten auf synchrone Aufrufe zu einer inakzeptablen Latenz führen.
  • Ablaufsteuerung: Eine Nachrichtenwarteschlange fungiert als Zwischenspeicher, sodass Empfänger Nachrichten in ihrer eigenen Geschwindigkeit verarbeiten können.

Bei der effektiven Verwendung des asynchronen Messagings stellen sich jedoch einige Herausforderungen:

  • Latenz: Wenn der Nachrichtenbroker zu einem Engpass wird, kann die End-to-End-Latenz hoch werden.
  • Aufwand in der Entwicklung und im Test: Abhängig von der Wahl der Messaging- oder Ereignisinfrastruktur kann es vorkommen, dass doppelte Nachrichten vorhanden sind, was das Kombinieren von Vorgängen erschwert. Außerdem kann es schwierig sein, die Anfrage-Antwort-Semantik mit asynchronem Messaging zu implementieren und zu testen. Sie benötigen eine Möglichkeit, Anfrage- und Antwortnachrichten zu korrelieren.
  • Durchsatz: Die asynchrone Nachrichtenverarbeitung, entweder über eine zentrale Warteschlange oder über einen anderen Mechanismus, kann zu einem Engpass im System werden. Die Backend-Systeme wie Warteschlangen und nachgelagerte Nutzer sollten an die Durchsatzanforderungen des Systems angepasst werden.
  • Komplizierte Fehlerbehandlung: In einem asynchronen System weiß der Aufrufer nicht, ob eine Anfrage erfolgreich war oder fehlgeschlagen ist. Daher muss die Fehlerbehandlung "out of band" abgewickelt werden. Diese Art von System kann die Implementierung von Logik wie Wiederholungsversuchen oder exponentiellen Backoffs erschweren. Die Fehlerbehandlung wird noch komplizierter, wenn mehrere verkettete asynchrone Aufrufe vorhanden sind, die alle erfolgreich sein oder fehlschlagen müssen.

Das nächste Dokument der Reihe, Zwischen-Dienst-Kommunikation in einer Mikrodienst-Einrichtung, enthält eine Referenzimplementierung, um einige der in der vorherigen Liste genannten Herausforderungen zu beheben.

Nächste Schritte