Freigeben über


Kommunikation in einer Microservice-Architektur

Tipp

Dieser Inhalt ist ein Auszug aus dem eBook .NET Microservices Architecture for Containerized .NET Applications, verfügbar auf .NET Docs oder als kostenlose herunterladbare PDF, die offline gelesen werden kann.

.NET Microservices-Architektur für containerisierte .NET-Anwendungen eBook-Cover-Thumbnail.

In einer monolithischen Anwendung, die auf einem einzelnen Prozess ausgeführt wird, rufen Komponenten einander mithilfe von Methoden oder Funktionsaufrufen auf Sprachebene auf. Diese können stark gekoppelt werden, wenn Sie Objekte mit Code erstellen (z. B. new ClassName()), oder in entkoppelter Weise aufgerufen werden, wenn Sie Dependency Injection verwenden, indem Sie auf Abstraktionen statt auf konkrete Objektinstanzen verweisen. Auf beide Weise werden die Objekte innerhalb desselben Prozesses ausgeführt. Die größte Herausforderung beim Wechsel von einer monolithischen Anwendung zu einer mikroservicesbasierten Anwendung liegt in der Änderung des Kommunikationsmechanismus. Eine direkte Konvertierung von In-Prozess-Methodenaufrufen in RPC-Aufrufe zu Diensten führt zu einer häufigen und ineffizienten Kommunikation, die in verteilten Umgebungen nicht gut funktioniert. Die Herausforderungen, verteilte Systeme korrekt zu entwerfen, sind so gut bekannt, dass es sogar einen Kanon gibt, der als Fehlannahmen des verteilten Rechnens bekannt ist und die Annahmen auflistet, die Entwickler häufig machen, wenn sie von monolithischen zu verteilten Designs wechseln.

Es gibt keine Lösung, sondern mehrere. Eine Lösung umfasst das Isolieren der Business Microservices so weit wie möglich. Anschließend verwenden Sie zwischen den internen Microservices eine asynchrone Kommunikation und ersetzen die für die prozessinterne Kommunikation zwischen Objekten typische differenzierte Kommunikation durch die undifferenzierte Kommunikation. Dazu können Sie Aufrufe gruppieren und Daten zurückgeben, die die Ergebnisse mehrerer interner Aufrufe an den Client aggregieren.

Eine microservices-basierte Anwendung ist ein verteiltes System, das auf mehreren Prozessen oder Diensten ausgeführt wird, in der Regel sogar auf mehreren Servern oder Hosts. Jede Dienstinstanz ist in der Regel ein Prozess. Daher müssen Dienste mit einem prozessübergreifenden Kommunikationsprotokoll wie HTTP, AMQP oder einem binären Protokoll wie TCP interagieren, je nach Art der einzelnen Dienste.

Die Microservice-Community fördert die Philosophie von "intelligenten Endpunkten und dummen Leitungen". Dieser Slogan fördert ein Design, das so entkoppelt wie möglich zwischen Microservices und so kohär wie möglich innerhalb eines einzigen Microservice ist. Wie bereits erläutert, besitzt jeder Microservice seine eigenen Daten und seine eigene Domänenlogik. Aber die Microservices, die eine End-to-End-Anwendung erstellen, werden in der Regel einfach mithilfe von REST-Kommunikation und nicht mit komplexen Protokollen wie WS-* und flexiblen ereignisgesteuerten Kommunikationen anstelle von zentralisierten Business-Process-Orchestratoren choreographiert.

Die beiden häufig verwendeten Protokolle sind HTTP-Anforderung/Antwort mit Ressourcen-APIs (insbesondere bei der Abfrage), und leichte asynchrone Nachrichtenübermittlung, wenn Updates über mehrere Microservices hinweg kommuniziert werden. Diese werden in den folgenden Abschnitten ausführlicher erläutert.

Kommunikationstypen

Client und Dienste können über viele verschiedene Arten von Kommunikation kommunizieren, wobei jeder auf ein anderes Szenario und ziele ausgerichtet ist. Zunächst können diese Kommunikationsarten in zwei Achsen klassifiziert werden.

Die erste Achse definiert, ob das Protokoll synchron oder asynchron ist:

  • Synchrones Protokoll. HTTP ist ein synchrones Protokoll. Der Client sendet eine Anforderung und wartet auf eine Antwort vom Dienst. Das geschieht unabhängig von der Ausführung des Clientcodes, der synchron (Thread ist blockiert) oder asynchron (Thread ist nicht blockiert und die Antwort erreicht schließlich einen Rückruf) sein könnte. Der wichtige Punkt hier ist, dass das Protokoll (HTTP/HTTPS) synchron ist und der Clientcode seine Aufgabe nur fortsetzen kann, wenn es die HTTP-Serverantwort empfängt.

  • Asynchrones Protokoll. Andere Protokolle wie AMQP (ein Protokoll, das von vielen Betriebssystemen und Cloudumgebungen unterstützt wird) verwenden asynchrone Nachrichten. Der Clientcode oder der Absender der Nachricht wartet in der Regel nicht auf eine Antwort. Sie sendet die Nachricht einfach so, als ob eine Nachricht an eine RabbitMQ-Warteschlange oder einen anderen Nachrichtenbroker gesendet wird.

Die zweite Achse definiert, ob die Kommunikation über einen einzelnen Empfänger oder mehrere Empfänger verfügt:

  • Einzelner Empfänger. Jede Anforderung muss von genau einem Empfänger oder Dienst verarbeitet werden. Ein Beispiel für diese Kommunikation ist das Befehlsmuster.

  • Mehrere Empfänger. Jede Anforderung kann von null bis zu mehreren Empfängern verarbeitet werden. Dieser Kommunikationstyp muss asynchron sein. Ein Beispiel ist der Veröffentlichungs-/Abonnieren-Mechanismus , der in Mustern wie der ereignisgesteuerten Architektur verwendet wird. Dies basiert auf einer Ereignisbusschnittstelle oder einem Nachrichtenbroker, wenn Datenaktualisierungen zwischen mehreren Microservices über Ereignisse verteilt werden; sie wird in der Regel über einen Servicebus oder ein ähnliches Artefakt wie Azure Service Bus mithilfe von Themen und Abonnements implementiert.

Eine mikroservicebasierte Anwendung verwendet häufig eine Kombination dieser Kommunikationsstile. Der häufigste Typ ist die Einzelempfängerkommunikation mit einem synchronen Protokoll wie HTTP/HTTPS beim Aufrufen eines regulären Web-API-HTTP-Diensts. Microservices verwenden in der Regel auch Messagingprotokolle für die asynchrone Kommunikation zwischen Microservices.

Diese Achsen sind gut zu wissen, damit Sie Klarheit über die möglichen Kommunikationsmechanismen haben, aber sie sind nicht die wichtigen Bedenken beim Erstellen von Microservices. Weder die asynchrone Art der Clientthreadausführung noch die asynchrone Art des ausgewählten Protokolls sind die wichtigen Punkte bei der Integration von Microservices. Was wichtig ist, ist die Fähigkeit, Ihre Microservices asynchron zu integrieren und gleichzeitig die Unabhängigkeit der Microservices aufrechtzuerhalten, wie im folgenden Abschnitt erläutert.

Die asynchrone Microservice-Integration erzwingt die Autonomie von Microservice

Wie bereits erwähnt, ist der wichtige Punkt beim Erstellen einer microservices-basierten Anwendung die Art und Weise, wie Sie Ihre Microservices integrieren. Im Idealfall sollten Sie versuchen, die Kommunikation zwischen den internen Microservices zu minimieren. Je weniger Kommunikation zwischen Microservices, desto besser. Aber in vielen Fällen müssen Sie die Microservices irgendwie integrieren. Wenn Sie dies tun müssen, ist die wichtige Regel hier, dass die Kommunikation zwischen den Microservices asynchron sein sollte. Das bedeutet nicht, dass Sie ein bestimmtes Protokoll verwenden müssen (z. B. asynchrones Messaging im Vergleich zu synchronem HTTP). Dies bedeutet lediglich, dass die Kommunikation zwischen Microservices nur durch asynchrones Verteilen von Daten erfolgen sollte, aber versuchen Sie nicht, von anderen internen Microservices als Teil des HTTP-Anforderungs-/Antwortvorgangs des ursprünglichen Diensts zu abhängen.

Wenn möglich, hängen Sie niemals von der synchronen Kommunikation (Anforderung/Antwort) zwischen mehreren Microservices ab, nicht einmal für Abfragen. Das Ziel jedes Microservice ist es, autonom zu sein und für den Kunden zugänglich zu sein, selbst wenn die anderen Dienste, die Teil der End-to-End-Anwendung sind, heruntergefahren oder funktionsgestört sind. Wenn Sie denken, dass Sie einen Aufruf von einem Microservice an andere Microservices (z. B. die Durchführung einer HTTP-Anforderung für eine Datenabfrage) durchführen müssen, um eine Antwort auf eine Clientanwendung bereitstellen zu können, verfügen Sie über eine Architektur, die nicht widerstandsfähig ist, wenn einige Microservices fehlschlagen.

Darüber hinaus führt die Verwendung von HTTP-Abhängigkeiten zwischen Microservices wie beim Erstellen langer Anforderungs-/Antwortzyklen mit HTTP-Anforderungsketten wie im ersten Teil der Abbildung 4-15 gezeigt nicht nur dazu, dass Ihre Microservices nicht autonom sind, sondern auch ihre Leistung beeinträchtigt wird, sobald einer der Dienste in dieser Kette nicht gut funktioniert.

Je mehr Sie synchrone Abhängigkeiten zwischen Microservices hinzufügen, z. B. Abfrageanforderungen, desto schlechter wird die Gesamtantwortzeit für die Client-Apps.

Diagramm mit drei Arten von Kommunikation über Microservices hinweg.

Abbildung 4-15. Antimuster und Muster bei der Kommunikation zwischen Microservices

Wie im obigen Diagramm dargestellt, wird in synchroner Kommunikation eine "Kette" von Anforderungen zwischen Microservices erstellt, während sie die Clientanforderung bedienen. Dies ist ein Antimuster. In asynchronen Kommunikations-Microservices werden asynchrone Nachrichten oder HTTP-Abrufe für die Kommunikation mit anderen Microservices verwendet, die Clientanforderung wird jedoch sofort bereitgestellt.

Wenn Ihr Microservice ggf. eine zusätzliche Aktion in einem anderen Microservice auslösen muss, führen Sie diese Aktion nicht synchron und als Teil der ursprünglichen Microservice-Anforderungs- und Antwortoperation aus. Tun Sie dies stattdessen asynchron (mithilfe von asynchronen Messaging- oder Integrationsereignissen, Warteschlangen usw.). Rufen Sie die Aktion jedoch so weit wie möglich nicht synchron als Teil des ursprünglichen synchronen Anforderungs- und Antwortvorgangs auf.

Und schließlich (und hier treten die meisten Probleme beim Erstellen von Microservices auf), wenn Ihr anfänglicher Microservice Daten benötigt, die ursprünglich von anderen Microservices stammen, sollten Sie keine synchronen Anfragen für diese Daten stellen. Replizieren oder übertragen Sie stattdessen diese Daten (nur die Attribute, die Sie benötigen) in die Datenbank des ursprünglichen Dienstes, indem Sie eventuelle Konsistenz verwenden (in der Regel durch Integrationsereignisse, wie in den folgenden Abschnitten erläutert).

Wie weiter oben in der Identifizierung von Domänenmodellgrenzen für jeden Microservice-Abschnitt erwähnt, ist das Duplizieren einiger Daten über mehrere Microservices kein falsches Design– im Gegenteil, wenn Sie die Daten in die spezifische Sprache oder Begriffe dieser zusätzlichen Domäne oder des gebundenen Kontexts übersetzen können. Beispielsweise haben Sie in der eShopOnContainers-Anwendung einen Microservice namens identity-api , der für die meisten Daten des Benutzers mit einer Entität namens Userverantwortlich ist. Wenn Sie jedoch Daten über den Benutzer innerhalb des Ordering Microservice speichern müssen, speichern Sie sie als eine andere Entität namens Buyer. Die Buyer Entität teilt dieselbe Identität mit der ursprünglichen User Entität, hat aber möglicherweise nur die wenigen Attribute, die von der Ordering Domäne benötigt werden, und nicht das gesamte Benutzerprofil.

Sie können jedes Protokoll verwenden, um Daten asynchron über Mikroservices hinweg zu kommunizieren und zu verteilen, um eine mögliche Konsistenz zu haben. Wie bereits erwähnt, könnten Sie Integrationsereignisse mit einem Ereignisbus oder Nachrichtenbroker verwenden oder sogar HTTP verwenden, indem Sie stattdessen die anderen Dienste abfragen. Die Vorgehensweise ist nicht wichtig. Die wichtige Regel besteht darin, keine synchronen Abhängigkeiten zwischen Ihren Microservices zu erstellen.

In den folgenden Abschnitten werden die verschiedenen Kommunikationsstile erläutert, die Sie in einer mikroservicebasierten Anwendung verwenden können.

Kommunikationsstile

Es gibt viele Protokolle und Auswahlmöglichkeiten, die Sie für die Kommunikation verwenden können, je nachdem, welche Kommunikationsart Sie verwenden möchten. Wenn Sie einen synchronen Anforderungs-/Antwort-basierten Kommunikationsmechanismus verwenden, sind Protokolle wie HTTP- und REST-Ansätze am häufigsten, insbesondere wenn Sie Ihre Dienste außerhalb des Docker-Hosts oder Microservice-Clusters veröffentlichen. Wenn Sie intern zwischen Diensten kommunizieren (innerhalb Ihres Docker-Hosts oder Microservices-Clusters), sollten Sie auch Binäre Formatkommunikationsmechanismen (z. B. WCF mit TCP und Binärformat) verwenden. Alternativ können Sie asynchrone, nachrichtenbasierte Kommunikationsmechanismen wie AMQP verwenden.

Es gibt auch mehrere Nachrichtenformate wie JSON oder XML oder sogar Binärformate, die effizienter sein können. Wenn Ihr ausgewähltes Binärformat kein Standard ist, empfiehlt es sich wahrscheinlich nicht, Ihre Dienste mit diesem Format öffentlich zu veröffentlichen. Sie können ein nicht standardmäßiges Format für die interne Kommunikation zwischen Ihren Microservices verwenden. Sie können dies tun, wenn Sie zwischen Microservices innerhalb Ihres Docker-Hosts oder Microservice-Clusters (z. B. Docker Orchestrators) oder für proprietäre Clientanwendungen kommunizieren, die mit den Microservices kommunizieren.

Anforderungs-/Antwortkommunikation mit HTTP und REST

Wenn ein Client die Anforderungs-/Antwortkommunikation verwendet, sendet er eine Anforderung an einen Dienst, dann verarbeitet der Dienst die Anforderung und sendet eine Antwort zurück. Die Anforderungs-/Antwortkommunikation eignet sich besonders gut für das Abfragen von Daten für eine Echtzeit-UI (eine Live-Benutzeroberfläche) aus Client-Apps. Daher verwenden Sie in einer Microservice-Architektur wahrscheinlich diesen Kommunikationsmechanismus für die meisten Abfragen, wie in Abbildung 4-16 dargestellt.

Diagramm mit Anforderungs-/Antwort-Comms für Liveabfragen und -updates.

Abbildung 4-16. Verwenden der HTTP-Anforderungs-/Antwortkommunikation (synchron oder asynchron)

Wenn ein Client die Anforderungs-/Antwortkommunikation verwendet, wird davon ausgegangen, dass die Antwort in kurzer Zeit, in der Regel weniger als eine Sekunde oder höchstens wenige Sekunden eintreffen wird. Für verzögerte Antworten müssen Sie eine asynchrone Kommunikation basierend auf Messagingmustern und Messagingtechnologien implementieren, was ein anderer Ansatz ist, den wir im nächsten Abschnitt erläutern.

Ein beliebter Architekturstil für die Anforderungs-/Antwortkommunikation ist REST. Dieser Ansatz basiert auf und eng gekoppelt mit dem HTTP-Protokoll , das HTTP-Verben wie GET, POST und PUT umfasst. REST ist der am häufigsten verwendete Architekturkommunikationsansatz beim Erstellen von Diensten. Sie können REST-Dienste implementieren, wenn Sie ASP.NET Core Web API-Dienste entwickeln.

Beim Verwenden von HTTP-REST-Diensten als Schnittstellendefinitionssprache gibt es zusätzlichen Wert. Wenn Sie beispielsweise Swagger-Metadaten verwenden, um Ihre Dienst-API zu beschreiben, können Sie Tools verwenden, die Client-Stubs generieren, die Ihre Dienste direkt ermitteln und nutzen können.

Weitere Ressourcen

Push- und Echtzeitkommunikation basierend auf HTTP

Eine weitere Möglichkeit (in der Regel für andere Zwecke als REST) ist eine One-to-Many- und Echtzeit-Kommunikation mit übergeordneten Frameworks wie ASP.NET SignalR und Protokollen wie WebSockets.

Wie in Abbildung 4-17 gezeigt, ermöglicht die Echtzeit-HTTP-Kommunikation, dass der Server Code verwenden kann, um Inhalte an verbundene Clients zu senden, sobald die Daten verfügbar sind, anstatt dass der Server darauf wartet, dass ein Client neue Daten anfordert.

Diagramm mit Push- und Echtzeit-Comms basierend auf SignalR.

Abbildung 4-17. Asynchrone 1:n-Nachrichtenkommunikation in Echtzeit

SignalR ist eine gute Möglichkeit, die Echtzeitkommunikation für das Pushen von Inhalten an die Clients von einem Back-End-Server zu erreichen. Da sich die Kommunikation in Echtzeit befindet, zeigen Client-Apps die Änderungen fast sofort an. Dies wird in der Regel von einem Protokoll wie WebSockets mit vielen WebSockets-Verbindungen (eins pro Client) behandelt. Ein typisches Beispiel ist, wenn ein Dienst eine Änderung des Spielstands eines Sportspiels gleichzeitig an viele Client-Web-Apps übermittelt.