Tutorial
17.05.2022, 10:02 Uhr
Skalierbare Microservices-Architektur mit .NET leicht gemacht
Das Tutorial soll den einfachsten Weg zum Aufbau einer .NET-basierten Microservices-Architektur aufzeigen, die sich einfach skalieren und an Kundenanforderungen anpassen lässt.
Wenn Endava gemeinsam mit seinen Kunden Lösungen auf Basis einer Microservices-Architektur entwirft, stoßen sie häufig auf die Anforderung, das Gesamtsystem schnell und einfach zu verwalten und einen möglichst hohen Automatisierungsgrad zu erreichen, um einzelne Komponenten nicht anpassen zu müssen.
Dies ist eine echte Herausforderung. Das Tutorial soll den einfachsten Weg zum Aufbau einer .NET-basierten Microservices-Architektur aufzeigen, die sich schnell und sehr einfach skalieren und an Kundenanforderungen anpassen lässt.
Es sollen keine Änderungen am Code oder an den Einstellungen der einzelnen Dienste vorgenommen, sondern das System nur durch die Orchestrierung von Containern in Docker gesteuert werden.
Das Ergebnis ist eine einfache Microservices-Architektur, die sich mit wenigen Änderungen in den Container-Einstellungen leicht skalieren lässt. Die Skalierung der Anwendung wird von zwei Open-Source-Komponenten übernommen: Ocelot, das als Gateway und Load Balancer fungiert, und HashiCorp Consul, der identitätsbasierte Netzwerkdienst, der als Service Discovery Agent fungiert.
Eine solche Architektur ermöglicht es uns, mehrere Instanzen eines einzelnen Dienstes neu zu verteilen, ohne die Verteilung mit anderen Diensten zu koordinieren. Die neu bereitgestellten Dienstinstanzen werden automatisch für die Diensterkennung registriert und sind sofort über das Gateway verfügbar. Sie können sich vorstellen, wie groß der Vorteil für jedes Entwicklungsteam ist!
Natürlich wird die Verwendung eines einzigen Gateway-Dienstes zu einem Single Point of Failure in unserer Architektur, so dass wir mindestens zwei Instanzen des Dienstes bereitstellen müssen, um eine hohe Verfügbarkeit zu erreichen, aber dieses Problem überlassen wir Ihnen.
Der Consul-Service
Ein wichtiger Teil dieses Tutorials ist die Verwendung des Consul-Dienstes zur dynamischen Ermittlung von Dienstendpunkten. Consul verwaltet automatisch ein Dienstregister, das aktualisiert wird, wenn eine neue Instanz eines Dienstes registriert wird und für den Empfang von Datenverkehr zur Verfügung steht. Sobald ein Dienst bei Consul registriert ist, kann er über den standardmäßigen DNS-Mechanismus oder über eine benutzerdefinierte API gefunden werden. So können wir unsere Dienste leicht skalieren.
Wenn wir mehrere Instanzen desselben Dienstes betreiben, verteilt Consul den Datenverkehr nach dem Zufallsprinzip auf die verschiedenen Instanzen und gleicht die Last zwischen ihnen aus.
Consul bietet auch Zustandsprüfungen für die registrierten Dienstinstanzen. Wenn einer der Dienste die Gesundheitsprüfung nicht besteht, erkennt die Registry diesen Zustand und vermeidet die Rückgabe der Adresse dieses Dienstes an Clients, die den Dienst suchen.
Service-Selbstregistrierung
Der erste Schritt besteht darin, dass sich eine Service-Instanz beim Service Discovery Service registriert, indem sie ihren Namen, ihre ID und ihre Adresse angibt. Anschließend kann das Gateway die Adresse dieses Dienstes abrufen, indem es die Consul Service Discovery API anhand des Namens oder der ID des Dienstes abfragt.
Wichtig ist hierbei, dass die Service-Instanzen mit einer eindeutigen Service-ID registriert werden, um zwischen den verschiedenen Instanzen eines Dienstes zu unterscheiden, die auf demselben Consul Service Agent laufen. Jeder Dienst muss eine eindeutige ID pro Knoten haben, so dass im Falle eines Namenskonflikts (wie in unserem Fall) die eindeutigen IDs die eindeutige Identifizierung jedes Dienstes ermöglichen.
Die Architektur der Anwendung
Unser Tutorial verwendet drei Instanzen eines sehr einfachen Mikrodienstes, der lediglich die Anfrage-URL und -ID zurückgibt, sowie einen einzigen Gateway-Mikrodienst (Ocelot), um externen Clients eine API zur Verfügung zu stellen. Alle Dienste, einschließlich Consul, sind mit Docker containerisiert, basierend auf leichtgewichtigen GNU/Linux-Distributionen ihrer Basis-Container-Images.
IMPLEMENTIERUNG UNSERER TUTORIAL-ANWENDUNG
Schauen wir uns nun an, wie wir die Selbstregistrierung in der .NET-Anwendung implementieren können. Zunächst müssen wir die für die Diensterkennung erforderliche Konfiguration aus den Umgebungsvariablen lesen, die über die Datei docker-compose.override.yml übergeben wurden.
Nachdem wir die Konfiguration gelesen haben, die erforderlich ist, um den Diensterkennungsdienst zu erreichen, können wir sie verwenden, um unseren Dienst zu registrieren. Der nachstehende Code ist als Hintergrundaufgabe implementiert (d. h. als gehosteter Dienst), die unseren Dienst in Consul registriert, indem sie alle vorherigen Informationen über den Dienst außer Kraft setzt. Wenn der Dienst heruntergefahren wird, wird er automatisch aus der Consul-Registrierung entfernt.
Schließlich müssen wir unsere Konfiguration und den gehosteten Dienst mit seinen Consul-Abhängigkeiten beim Dependency Injection Container registrieren. Dazu verwenden wir eine einfache Erweiterungsmethode, die innerhalb unserer Dienste gemeinsam genutzt werden kann:
Sobald wir unsere Dienste im Service Discovery Service registriert haben, können wir mit der Implementierung des API-Gateways beginnen.
Erstellen eines API-Gateways mit Ocelot
Ocelot erfordert, dass Sie eine Konfigurationsdatei bereitstellen, die eine Liste von Routes (Konfiguration, die verwendet wird, um Upstream-Anfragen auf API-Endpunkte abzubilden) und eine GlobalConfiguration (andere Konfigurationseinstellungen wie QoS, Parameter zur Ratenbegrenzung usw.) enthält.
In der folgenden ocelot.json-Datei können Sie sehen, wie wir HTTP-Anfragen weiterleiten. Wir müssen angeben, welche Art von Load Balancer wir verwenden werden. In unserem Fall ist dies ein RoundRobin, der die verfügbaren Dienste in einer Schleife durchläuft und Anfragen an sie weiterleitet.
Es ist wichtig, Consul als Service Discovery Service in der GlobalConfiguration für den ServiceDiscoveryProvider festzulegen.
Einige der wichtigsten ServiceDiscoveryProvider-Einstellungen im Abschnitt GlobalConfiguration sind die folgenden
- Host - der Host von Consul
- Port - der Port von Consul
- Type Consul - bedeutet, dass Ocelot per Anfrage Service-Informationen von Consul erhält
- Typ PollConsul - bedeutet, dass Ocelot Consul nach den neuesten Dienstinformationen abfragt
- PollingInterval - teilt Ocelot mit, wie oft es Consul für Änderungen in der Diensteregistrierung aufrufen soll
Nachdem wir unsere Konfiguration definiert haben, können wir mit der Implementierung eines API-Gateways auf der Grundlage von .NET 5 und Ocelot beginnen. Unten sehen Sie die Implementierung eines Ocelot-API-Gateway-Dienstes, der unsere Konfigurationsdatei ocelot.json und Consul als Dienstregistrierung verwendet.
Die Klasse Program enthält die Methode Main(), die den Einstiegspunkt der .NET-Anwendungen darstellt. Die Program-Klasse erstellt auch den Webhost beim Start.
Die Startup-Klasse konfiguriert die Dienste der Anwendung und definiert die Middleware-Pipeline. In diesem Schritt ist es wichtig, die AddConsul-Middleware mithilfe der AddConsul-Erweiterung in die Pipeline aufzunehmen:
Ausführen der Dienste in Docker
Wie bereits erwähnt, werden wir alle Dienste, einschließlich Consul, mit Docker containerisieren und leichtgewichtige GNU/Linux-Distributionen für die Basis-Container-Images verwenden.
Dazu müssen wir zunächst unsere docker-compose.yml einrichten, die Konfigurationsdatei für Docker Compose. Sie ermöglicht es uns, mehrere Docker-Container gleichzeitig einzusetzen, zu kombinieren und zu konfigurieren. In unserem Tutorial sieht sie wie folgt aus:
Beachten Sie, dass unsere Dienste keine Konfigurationsdateien enthalten, da wir die Datei docker-compose.override.yml verwenden werden, um Konfigurationswerte bereitzustellen. In dieser Konfigurationsdatei können Sie bestehende Einstellungen aus der Datei docker-compose.yml überschreiben oder sogar völlig neue Dienste hinzufügen. In unserem Tutorial sieht sie so aus:
Starten der Container
Um die Docker-Compose-Datei auszuführen und die Container zu starten, öffnen Sie die Powershell und navigieren Sie zur Compose-Datei im Stammordner. Führen Sie dann den folgenden Befehl aus: docker-compose up -d -build, wodurch alle Container gestartet und ausgeführt werden. Der Parameter -d führt den Befehl als "abgetrennten" Befehl aus. Das bedeutet, dass die Container im Hintergrund ausgeführt werden und Ihr Powershell-Fenster nicht blockieren. Um die laufenden Container zu überprüfen, verwenden Sie den Befehl docker ps.
Consul Web UI
Consul bietet von Haus aus eine schöne Web-Benutzeroberfläche. Sie können sie über Port 8500 (http://localhost:8500) aufrufen. Werfen wir einen Blick auf einige der Bildschirme.
Die Startseite für die Consul UI-Dienste, die alle relevanten Informationen zu einem Consul-Agenten und einer Web-Service-Prüfung anzeigt, ist unten dargestellt.
Anhand dieser Bildschirme können wir sehen, dass Consul uns ein Dienstregister zur Verfügung stellt und regelmäßige Gesundheitsprüfungen der Dienste durchführt, die wir bei ihm registriert haben.
Probieren Sie es aus
Lassen Sie uns einige Aufrufe über das API-Gateway unter http://localhost:9500/api/values tätigen. Der Load Balancer durchläuft die verfügbaren Dienstinstanzen, leitet Anfragen an sie weiter und gibt die von ihnen erzeugten Antworten zurück:
Sie können nun die Architektur in Aktion sehen, wobei der Load Balancer die eingehenden Anfragen auf die verfügbaren Serviceinstanzen verteilt.
Fazit
Microservice-Systeme sind oft nicht einfach zu erstellen und zu warten. Dieses Tutorial hat jedoch gezeigt, wie einfach es ist, eine Anwendung mit einer .NET-Microservice-Architektur zu entwickeln und bereitzustellen.
Wie wir gesehen haben, bietet Consul erstklassige Unterstützung für Service Discovery, Health Checks, Key-Value Storage für Konfigurationselemente und Multi-Data-Center-Deployments. Ocelot kann verwendet werden, um ein API-Gateway bereitzustellen, das mit der Consul-Service-Registrierung kommuniziert und Service-Registrierungen abruft, während der Load Balancer die Last auf eine Gruppe von Service-Instanzen verteilt, indem er die verfügbaren Services in einer Schleife durchläuft und Anfragen an sie weiterleitet.
Die Verwendung von beidem macht das Leben von Entwicklern, die vor solchen Herausforderungen stehen, deutlich einfacher. Meinen Sie nicht auch?
Den Quellcode für dieses Tutorial finden Sie auf GitHub von Endava.
Matjaz Bravc ist ein Senior Software Engineer bei Endava (https://www.endava.com/) mit mehr als 15 Jahren Erfahrung in der Konzeption und Entwicklung skalierbarer Anwendungen auf agile Weise. Seine Erfahrung erstreckt sich über verschiedene Bereiche und Technologien (hauptsächlich Microsoft Stack). Er ist ein erfahrener .NET-Entwickler mit starker Backend-C#-Entwicklungserfahrung. Sein Fachwissen umfasst auch unternehmenskritische verteilte Systeme mit einem starken Fokus auf objektorientierter Programmierung, Microservice-Architektur, Data Warehouse Design und testgetriebener Entwicklung. Matjaz verbringt seine Freizeit gerne abseits des Computers mit Hobbys wie Tauchen, Kanufahren, Standup-Paddling und Mountainbiking.
Der Artikel ist eine Übersetzung des englischen Artikels.