Freigeben über


Entwerfen einer mikroserviceorientierten Anwendung

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.

Dieser Abschnitt konzentriert sich auf die Entwicklung einer hypothetischen serverseitigen Unternehmensanwendung.

Anwendungsspezifikationen

Die hypothetische Anwendung verarbeitet Anforderungen, indem Geschäftslogik ausgeführt, auf Datenbanken zugegriffen und dann HTML-, JSON- oder XML-Antworten zurückgegeben werden. Wir werden sagen, dass die Anwendung verschiedene Clients unterstützen muss, einschließlich Desktopbrowsern mit Single Page Applications (SPAs), herkömmlichen Web-Apps, mobilen Web-Apps und nativen mobilen Apps. Die Anwendung kann auch eine API für Dritte verfügbar machen. Es sollte auch in der Lage sein, seine Microservices oder externen Anwendungen asynchron zu integrieren, sodass dieser Ansatz die Resilienz der Microservices bei teilweisen Fehlern unterstützt.

Die Anwendung besteht aus folgenden Komponententypen:

  • Präsentationskomponenten. Diese Komponenten sind für die Verarbeitung der Benutzeroberfläche und die Nutzung von Remotediensten verantwortlich.

  • Domänen- oder Geschäftslogik. Diese Komponente ist die Domänenlogik der Anwendung.

  • Datenbankzugriffslogik. Diese Komponente besteht aus Datenzugriffskomponenten, die für den Zugriff auf Datenbanken (SQL oder NoSQL) verantwortlich sind.

  • Anwendungsintegrationslogik. Diese Komponente enthält einen Nachrichtenkanal, der auf Nachrichtenbroker basiert.

Die Anwendung erfordert eine hohe Skalierbarkeit, während ihre vertikalen Subsysteme autonom skaliert werden können, da bestimmte Subsysteme mehr Skalierbarkeit erfordern als andere.

Die Anwendung muss in mehreren Infrastrukturumgebungen (mehrere öffentliche Clouds und lokal) bereitgestellt werden können und idealerweise plattformübergreifend sein und problemlos von Linux zu Windows (oder umgekehrt) wechseln können.

Kontext des Entwicklungsteams

Darüber hinaus gehen wir von folgendem Entwicklungsprozess für die Anwendung aus:

  • Sie haben mehrere Entwicklerteams, die sich auf verschiedene Geschäftsfelder der Anwendung konzentrieren.

  • Neue Teammitglieder müssen schnell produktiv werden, und die Anwendung muss leicht zu verstehen und zu ändern sein.

  • Die Anwendung wird eine langfristige Entwicklung und sich ständig ändernde Geschäftsregeln haben.

  • Sie benötigen eine gute langfristige Wartungsbarkeit, was bedeutet, dass sie flexibilität bei der Implementierung neuer Änderungen in der Zukunft haben und gleichzeitig mehrere Subsysteme mit minimalen Auswirkungen auf die anderen Subsysteme aktualisieren können.

  • Sie möchten eine kontinuierliche Integration und eine kontinuierliche Bereitstellung der Anwendung üben.

  • Sie möchten neue Technologien (Frameworks, Programmiersprachen usw.) nutzen, während Sie die Anwendung weiterentwickeln. Sie möchten keine vollständigen Migrationen der Anwendung vornehmen, wenn Sie zu neuen Technologien wechseln, da dies zu hohen Kosten und Auswirkungen auf die Vorhersehbarkeit und Stabilität der Anwendung führen würde.

Auswählen einer Architektur

Was sollte die Architektur der Anwendungsbereitstellung sein? Die Spezifikationen für die Anwendung zusammen mit dem Entwicklungskontext empfehlen dringend, dass Sie die Anwendung entwerfen sollten, indem Sie sie in autonome Subsysteme in Form von zusammenarbeitenden Microservices und Containern dekompilieren, wobei ein Microservice ein Container ist.

Bei diesem Ansatz implementiert jeder Dienst (Container) eine Reihe von zusammenhängenden und eng verbundenen Funktionen. Eine Anwendung kann z. B. aus Diensten wie Katalogdienst, Bestelldienst, Korbdienst, Benutzerprofildienst usw. bestehen.

Microservices kommunizieren mit Protokollen wie HTTP (REST), aber auch asynchron (z. B. mithilfe von AMQP), insbesondere beim Verteilen von Updates mit Integrationsereignissen.

Microservices werden unabhängig voneinander als Container entwickelt und bereitgestellt. Dieser Ansatz bedeutet, dass ein Entwicklungsteam einen bestimmten Mikroservice entwickeln und bereitstellen kann, ohne dass andere Subsysteme betroffen sind.

Jeder Microservice verfügt über eine eigene Datenbank, sodass er vollständig von anderen Microservices entkoppelt werden kann. Bei Bedarf wird die Konsistenz zwischen Datenbanken aus verschiedenen Microservices mithilfe von Integrationsereignissen auf Anwendungsebene (über einen logischen Ereignisbus) erreicht, wie in Command and Query Responsibility Segregation (CQRS) behandelt. Aus diesem Gründen müssen die Geschäftseinschränkungen die letztendliche Konsistenz zwischen den mehreren Microservices und verwandten Datenbanken umfassen.

eShopOnContainers: Eine Referenzanwendung für .NET und Microservices, die mithilfe von Containern bereitgestellt werden

Damit Sie sich auf die Architektur und Technologien konzentrieren können, anstatt über eine hypothetische Geschäftsdomäne nachzudenken, die Sie vielleicht nicht kennen, haben wir eine bekannte Geschäftsdomäne ausgewählt– nämlich eine vereinfachte E-Commerce-Anwendung (E-Shop), die einen Katalog von Produkten darstellt, Bestellungen von Kunden übernimmt, Den Bestand überprüft und andere Geschäftsfunktionen ausführt. Dieser containerbasierte Anwendungsquellcode ist im GitHub-Repository "eShopOnContainers " verfügbar.

Die Anwendung besteht aus mehreren Subsystemen, darunter mehrere Store-UI-Front-Ends (eine Webanwendung und eine native mobile App) sowie die Back-End-Microservices und Container für alle erforderlichen serverseitigen Vorgänge mit mehreren API-Gateways als konsolidierte Einstiegspunkte zu den internen Microservices. Abbildung 6-1 zeigt die Architektur der Referenzanwendung.

Diagramm von Client-Apps mit eShopOnContainers in einem einzelnen Docker-Host.

Abbildung 6-1. Die eShopOnContainers Referenzanwendungsarchitektur für die Entwicklungsumgebung

Das obige Diagramm zeigt, dass Mobile- und SPA-Clients mit einzelnen API-Gatewayendpunkten kommunizieren, die dann mit Microservices kommunizieren. Herkömmliche Webclients kommunizieren mit MVC Microservice, die über das API-Gateway mit Microservices kommunizieren.

Hostumgebung. In Abbildung 6-1 werden mehrere Container in einem einzelnen Docker-Host bereitgestellt. Dies wäre bei der Bereitstellung auf einem einzelnen Docker-Host mit dem Befehl "docker-compose up" der Fall. Wenn Sie jedoch einen Orchestrator- oder Containercluster verwenden, kann jeder Container in einem anderen Host (Knoten) ausgeführt werden, und jeder Knoten kann eine beliebige Anzahl von Containern ausführen, wie weiter oben im Architekturabschnitt erläutert.

Kommunikationsarchitektur. Die eShopOnContainers-Anwendung verwendet je nach Art der funktionalen Aktion zwei Kommunikationstypen (Abfragen im Vergleich zu Updates und Transaktionen):

  • Http-Client-zu-Microservice-Kommunikation über API-Gateways. Dieser Ansatz wird für Abfragen und beim Akzeptieren von Aktualisierungs- oder Transaktionsbefehlen aus den Client-Apps verwendet. Der Ansatz mit API-Gateways wird in späteren Abschnitten ausführlich erläutert.

  • Asynchrone ereignisbasierte Kommunikation. Diese Kommunikation erfolgt über einen Ereignisbus, um Updates über Microservices hinweg zu verteilen oder in externe Anwendungen zu integrieren. Der Ereignisbus kann mit jeder Messagingbroker-Infrastrukturtechnologie wie RabbitMQ oder mit Servicebussen auf höherer Ebene (Abstraktionsebene) wie Azure Service Bus, NServiceBus, MassTransit oder Brighter implementiert werden.

Die Anwendung wird als Eine Reihe von Microservices in Form von Containern bereitgestellt. Client-Apps können mit diesen Microservices kommunizieren, die als Container über die öffentlichen URLs ausgeführt werden, die von den API-Gateways veröffentlicht werden.

Datenhoheit pro Microservice

In der Beispielanwendung besitzt jeder Microservice eine eigene Datenbank oder Datenquelle, obwohl alle SQL Server-Datenbanken als einzelner Container bereitgestellt werden. Diese Entwurfsentscheidung wurde nur getroffen, damit ein Entwickler den Code von GitHub abrufen, ihn klonen und in Visual Studio oder Visual Studio Code öffnen kann. Alternativ können Sie auch die benutzerdefinierten Docker-Images mithilfe der .NET CLI und der Docker CLI kompilieren und dann in einer Docker-Entwicklungsumgebung bereitstellen und ausführen. Die Verwendung von Containern für Datenquellen ermöglicht Entwicklern das Erstellen und Bereitstellen in wenigen Minuten, ohne eine externe Datenbank oder eine andere Datenquelle mit harten Abhängigkeiten von der Infrastruktur (Cloud oder lokal) bereitstellen zu müssen.

In einer echten Produktionsumgebung sollten die Datenbanken für hohe Verfügbarkeit und Skalierbarkeit auf Datenbankservern in der Cloud oder lokal, aber nicht in Containern basieren.

Daher sind die Bereitstellungseinheiten für Microservices (und sogar für Datenbanken in dieser Anwendung) Docker-Container, und die Referenzanwendung ist eine Multicontaineranwendung, die Microservices-Prinzipien umfasst.

Weitere Ressourcen

Vorteile einer mikroservicebasierten Lösung

Eine microservicebasierte Lösung wie dies hat viele Vorteile:

Jeder Microservice ist relativ klein – einfach zu verwalten und zu entwickeln. Dies gilt insbesondere in folgenden Fällen:

  • Eine solche Lösung ist für Entwickler leicht verständlich und ermöglicht einen schnellen und produktiven Einstieg.

  • Container beginnen schnell, was Entwickler produktiver macht.

  • Eine IDE wie Visual Studio kann kleinere Projekte schnell laden, wodurch Entwickler produktiv arbeiten können.

  • Jeder Microservice kann unabhängig von anderen Microservices entworfen, entwickelt und bereitgestellt werden, die Flexibilität bieten, da es einfacher ist, neue Versionen von Microservices häufig bereitzustellen.

Es ist möglich, einzelne Bereiche der Anwendung zu skalieren. Beispielsweise muss der Katalogdienst oder der Korbdienst möglicherweise verkleinert werden, aber nicht der Bestellvorgang. Eine Microservice-Infrastruktur wird beim Skalieren effizienter mit den eingesetzten Ressourcen umgehen als eine monolithische Architektur.

Sie können die Entwicklungsarbeit zwischen mehreren Teams unterteilen. Jeder Dienst kann einem einzelnen Entwicklungsteam gehören. Jedes Team kann seinen Dienst unabhängig vom Rest der Teams verwalten, entwickeln, bereitstellen und skalieren.

Probleme sind isolierter. Wenn in einem Dienst ein Problem auftritt, wird zunächst nur dieser Dienst betroffen (außer wenn der falsche Entwurf verwendet wird, mit direkten Abhängigkeiten zwischen Microservices), und andere Dienste können weiterhin Anforderungen verarbeiten. Im Gegensatz dazu kann eine fehlerhafte Komponente in einer monolithischen Bereitstellungsarchitektur das gesamte System heruntersetzen, insbesondere, wenn sie Ressourcen wie z. B. einen Speicherverlust umfasst. Darüber hinaus können Sie, wenn ein Problem in einem Microservice behoben ist, nur den betroffenen Microservice bereitstellen, ohne die restliche Anwendung zu beeinträchtigen.

Sie können die neuesten Technologien verwenden. Da Sie mit der Entwicklung von Diensten unabhängig beginnen und diese parallel ausführen können (dank Containern und .NET), können Sie mit der verwendung der neuesten Technologien und Frameworks sinnvoll beginnen, anstatt auf einem älteren Stapel oder Framework für die gesamte Anwendung hängen zu bleiben.

Nachteile einer mikroservicebasierten Lösung

Eine microservicebasierte Lösung wie dies hat auch einige Nachteile:

Verteilte Anwendung. Durch die Verteilung der Anwendung wird für Entwickler beim Entwerfen und Erstellen der Dienste Komplexität hinzugefügt. Entwickler müssen z. B. die Dienstübergreifende Kommunikation mithilfe von Protokollen wie HTTP oder AMQP implementieren, wodurch Die Test- und Ausnahmebehandlung komplexer wird. Außerdem wird dem System Latenz hinzugefügt.

Die Komplexität der Bereitstellung. Eine Anwendung mit Dutzenden microservices-Typen und benötigt eine hohe Skalierbarkeit (sie muss viele Instanzen pro Dienst erstellen und diese Dienste auf vielen Hosts ausgleichen) bedeutet eine hohe Bereitstellungskomplexität für IT-Vorgänge und -Verwaltung. Wenn Sie keine mikroserviceorientierte Infrastruktur (z. B. einen Orchestrator und einen Scheduler) verwenden, kann diese zusätzliche Komplexität wesentlich mehr Entwicklungsanstrengungen erfordern als die Geschäftsanwendung selbst.

Atomtransaktionen. Atomtransaktionen zwischen mehreren Microservices sind in der Regel nicht möglich. Die geschäftsspezifischen Anforderungen müssen letztendlich die Konsistenz zwischen mehreren Microservices umfassen. Weitere Informationen finden Sie unter den Herausforderungen der idempotenten Nachrichtenverarbeitung.

Erhöhte globale Ressourcenanforderungen (Gesamtspeicher, Laufwerke und Netzwerkressourcen für alle Server oder Hosts). Wenn Sie eine monolithische Anwendung durch einen Mikroservices-Ansatz ersetzen, ist die Anzahl der anfänglichen globalen Ressourcen, die von der neuen mikroservicebasierten Anwendung benötigt werden, größer als die Infrastrukturanforderungen der ursprünglichen monolithischen Anwendung. Dieser Ansatz liegt daran, dass der höhere Grad an Granularität und verteilten Diensten mehr globale Ressourcen erfordert. Angesichts der generell niedrigen Ressourcenkosten und des Vorteils, bestimmte Bereiche der Anwendung im Vergleich zu langfristigen Kosten beim Weiterentwickeln monolithischer Anwendungen skalierbar zu machen, ist die verstärkte Nutzung von Ressourcen normalerweise ein guter Kompromiss für große, langfristige Anwendungen.

Probleme mit der direkten Client-zu-Microservice-Kommunikation. Wenn die Anwendung groß ist, mit Dutzenden von Microservices, gibt es Herausforderungen und Einschränkungen, wenn die Anwendung direkte Client-zu-Microservice-Kommunikation erfordert. Ein Problem ist ein potenzieller Konflikt zwischen den Anforderungen des Clients und den APIs, die von jedem der Microservices verfügbar gemacht werden. In bestimmten Fällen muss die Clientanwendung möglicherweise viele separate Anforderungen zum Verfassen der Benutzeroberfläche stellen, was im Internet ineffizient sein kann und über ein mobiles Netzwerk unpraktisch wäre. Daher sollten Anforderungen von der Clientanwendung an das Back-End-System minimiert werden.

Ein weiteres Problem bei der direkten Client-zu-Microservice-Kommunikation besteht darin, dass einige Microservices möglicherweise Protokolle verwenden, die nicht webfreundlich sind. Ein Dienst verwendet möglicherweise ein binäres Protokoll, während ein anderer Dienst AMQP-Messaging verwenden kann. Diese Protokolle sind nicht firewallfreundlich und werden am besten intern verwendet. In der Regel sollte eine Anwendung Protokolle wie HTTP und WebSockets für die Kommunikation außerhalb der Firewall verwenden.

Ein weiterer Nachteil bei diesem direkten Client-to-Service-Ansatz besteht darin, dass es schwierig macht, die Verträge für diese Microservices umzugestalten. Im Laufe der Zeit möchten Entwickler möglicherweise ändern, wie das System in Dienste partitioniert wird. Sie können beispielsweise zwei Dienste zusammenführen oder einen Dienst in zwei oder mehr Dienste aufteilen. Wenn Clients jedoch direkt mit den Diensten kommunizieren, kann die Durchführung dieser Art von Umgestaltung die Kompatibilität mit Client-Apps unterbrechen.

Wie im Abschnitt "Architektur" erwähnt, können Sie beim Entwerfen und Erstellen einer komplexen Anwendung, die auf Microservices basiert, die Verwendung mehrerer feinkörniger API-Gateways anstelle des einfacheren direkten Client-zu-Microservice-Kommunikationsansatzes in Betracht ziehen.

Aufteilung der Microservices. Unabhängig davon, welcher Ansatz Sie für Ihre Microservice-Architektur übernehmen, ist eine weitere Herausforderung, eine End-to-End-Anwendung in mehrere Microservices zu partitionieren. Wie im Abschnitt "Architektur" des Leitfadens erwähnt, gibt es verschiedene Techniken und Ansätze, die Sie ergreifen können. Grundsätzlich müssen Sie Bereiche der Anwendung identifizieren, die von den anderen Bereichen entkoppelt werden und eine geringe Anzahl von harten Abhängigkeiten aufweisen. In vielen Fällen wird dieser Ansatz auf die Servicepartitionierung nach Anwendungsfall angewandt. Beispielsweise haben wir in unserer E-Shop-Anwendung einen Bestellservice, der für alle Geschäftslogik im Zusammenhang mit dem Bestellvorgang verantwortlich ist. Außerdem verfügen wir über den Katalogdienst und den Korbdienst, der andere Funktionen implementiert. Im Idealfall sollte jeder Dienst nur über eine kleine Reihe von Zuständigkeiten verfügen. Dieser Ansatz ähnelt dem Prinzip der einzelverantwortlichen Verantwortung (Single Responsibility Principle, SRP), das auf Klassen angewendet wird, die besagt, dass eine Klasse nur einen Grund haben sollte, sich zu ändern. In diesem Fall geht es jedoch um Microservices, sodass der Bereich größer als eine einzelne Klasse ist. Vor allem muss ein Microservice unabhängig und konsistent sein. Das bezieht auch die Verantwortung für seine eigenen Datenquellen mit ein.

Externe und interne Architektur- und Entwurfsmuster

Die externe Architektur ist die microservice-Architektur, die von mehreren Diensten besteht, und folgt den im Architekturabschnitt dieses Handbuchs beschriebenen Prinzipien. Je nach Art der einzelnen Microservice-Architekturen und unabhängig von der von Ihnen gewählten high-level Microservice-Architektur ist es üblich und manchmal ratsam, unterschiedliche interne Architekturen zu haben, die jeweils auf unterschiedlichen Mustern basieren, für verschiedene Microservices. Die Microservices können sogar verschiedene Technologien und Programmiersprachen verwenden. Abbildung 6-2 veranschaulicht diese Vielfalt.

Diagramm, das externe und interne Architekturmuster vergleicht.

Abbildung 6-2. Externe und interne Architektur und Design

Beispielsweise sind in unserem Beispiel für eShopOnContainers der Katalog, der Korb und der Benutzerprofil-Microservices einfach (im Grunde CRUD-Subsysteme). Daher ist ihre interne Architektur und ihr Design einfach. Möglicherweise haben Sie jedoch andere Microservices, z. B. den Sortier-Microservice, der komplexer ist und sich ständig ändernde Geschäftsregeln mit einem hohen Grad an Domänenkomplexität darstellt. In solchen Fällen möchten Sie möglicherweise komplexere Muster in einem bestimmten Microservice implementieren, z. B. die mit DDD-Ansätzen (domain-driven design) definierten Muster, wie wir es im eShopOnContainers-Sortier-Microservice tun. (Wir werden diese DDD-Muster später im Abschnitt überprüfen, in dem die Implementierung des eShopOnContainers order microservice erläutert wird.)

Ein weiterer Grund für eine andere Technologie pro Microservice kann die Natur jedes Microservice sein. Es kann z. B. besser sein, eine funktionale Programmiersprache wie F# oder sogar eine Sprache wie R zu verwenden, wenn Sie KI- und maschinelle Lerndomänen anstelle einer objektorientierten Programmiersprache wie C# verwenden.

Die untere Linie besteht darin, dass jeder Microservice eine andere interne Architektur basierend auf unterschiedlichen Entwurfsmustern haben kann. Nicht alle Microservices sollten mit fortgeschrittenen DDD-Mustern implementiert werden, da dies übermäßig komplex wäre. Ebenso sollten komplexe Microservices mit sich ständig ändernder Geschäftslogik nicht als CRUD-Komponenten implementiert werden, oder Sie können mit Code mit niedriger Qualität enden.

Die neue Welt: mehrere Architekturmuster und polyglot microservices

Es gibt viele Architekturmuster, die von Softwarearchitekten und Entwicklern verwendet werden. Es folgen einige (Mischen von Architekturstilen und Architekturmustern):

Sie können auch Microservices mit vielen Technologien und Sprachen erstellen, z. B. ASP.NET Core Web APIs, NancyFx, ASP.NET Core SignalR (verfügbar mit .NET Core 2 oder höher), F#, Node.js, Python, Java, C++, GoLang usw.

Wichtig ist, dass für alle Situationen kein bestimmtes Architekturmuster oder -stil oder eine bestimmte Technologie geeignet ist. Abbildung 6-3 zeigt einige Ansätze und Technologien (obwohl nicht in einer bestimmten Reihenfolge), die in verschiedenen Microservices verwendet werden können.

Diagramm mit 12 komplexen Mikroservices in einer Polyglot-Weltarchitektur.

Abbildung 6-3. Multiarchitekturmuster und die polyglotte Mikroservices-Welt

Multi-Architektur-Muster und Polyglot-Mikroservices bedeuten, dass Sie Sprachen und Technologien entsprechend den Bedürfnissen jedes Mikroservice mischen und kombinieren können und diese trotzdem miteinander kommunizieren. Wie in Abbildung 6-3 gezeigt, können Sie in Anwendungen, die aus vielen Mikroservices (Gebundene Kontexte in domänengesteuerten Designterminologien oder einfach "Subsysteme" als autonome Microservices) bestehen, jeden Microservice auf eine andere Weise implementieren. Jedes kann ein anderes Architekturmuster aufweisen und je nach Art, Geschäftsanforderungen und Prioritäten der Anwendung unterschiedliche Sprachen und Datenbanken verwenden. In einigen Fällen können die Microservices ähnlich sein. Dies ist jedoch in der Regel nicht der Fall, da die Kontextgrenze und Anforderungen jedes Subsystems in der Regel unterschiedlich sind.

Für eine einfache CRUD-Wartungsanwendung ist es möglicherweise nicht sinnvoll, DDD-Muster zu entwerfen und zu implementieren. Aber für Ihre Kerndomäne oder Ihr Kerngeschäft müssen Sie möglicherweise erweiterte Muster anwenden, um die Geschäftskomplexität mit sich ständig ändernden Geschäftsregeln zu bewältigen.

Insbesondere wenn Sie mit großen Anwendungen umgehen, die von mehreren Subsystemen verfasst wurden, sollten Sie keine einzelne Architektur auf oberster Ebene basierend auf einem einzelnen Architekturmuster anwenden. Beispielsweise sollte CQRS nicht als Architektur auf oberster Ebene für eine gesamte Anwendung angewendet werden, kann aber für einen bestimmten Satz von Diensten nützlich sein.

Es gibt kein Allheilmittel oder ein richtiges Architekturmuster für jeden Fall. Sie können nicht über ein Architekturmuster verfügen, um sie alle zu regelen. Abhängig von den Prioritäten jedes Microservice müssen Sie einen anderen Ansatz für jeden auswählen, wie in den folgenden Abschnitten erläutert.