Beim Entwurf von Lösungen auf Basis der Microservice-Architektur stoßen wir häufig auf die Anforderung einer schnellen und einfachen Verwaltung des Gesamtsystems, einer möglichst hohen Automatisierung, ohne die notwendige Anpassung einzelner Komponenten.
Das ist eine echte Herausforderung und deshalb habe ich mich entschieden, ein Tutorial zu erstellen, das zeigt, wie man auf einfachste Weise eine Microservice-Architektur aufbaut, die sich schnell und sehr einfach skalieren und an Kundenanforderungen anpassen lässt.
Ich wollte nicht in den Code und die Einstellungen einzelner Dienste eingreifen, sondern das System nur mit der Orchestrierung von Containern in Docker steuern.
Das Ergebnis ist eine einfache Microservice-Architektur, die mit nur wenigen Änderungen an den Containereinstellungen leicht skaliert werden kann, alles andere wird von Ocelot als Gateway/Load Balancer und Consul als Service Discovery Agent bereitgestellt.
Eine solche Architektur ermöglicht uns die erneute Bereitstellung eines einzelnen Dienstes, ohne die Bereitstellung innerhalb anderer Dienste zu koordinieren. Der erneut bereitgestellte Dienst wird automatisch bei der Diensterkennung registriert und ist sofort über das Gateway verfügbar. Sie können sich vorstellen, wie viel Auftrieb das für jedes Entwicklungsteam bedeutet!
Natürlich wird die Verwendung eines einzelnen Gateway-Dienstes zu einem einzigen Fehlerpunkt für unsere Architektur, daher müssen wir mindestens zwei Instanzen davon bereitstellen, um eine hohe Verfügbarkeit zu gewährleisten. Aber das Problem überlasse ich Ihnen.
In meiner vorherigen Demo habe ich gezeigt, wie man Ocelot als Service-Gateway und Load Balancer zusammen mit Eureka für eine Service-Erkennung implementiert. Stattdessen verwendet Eureka in dieser Demo Consul für eine Serviceerkennung.
Consul ist eine Service-Mesh-Lösung, die eine voll ausgestattete Steuerungsebene mit Serviceerkennungs-, Konfigurations- und Segmentierungsfunktionen bietet. Jede dieser Funktionen kann je nach Bedarf einzeln verwendet werden oder sie können zusammen verwendet werden, um ein Full-Service-Mesh aufzubauen. Consul erfordert eine Datenebene und unterstützt sowohl ein Proxy- als auch ein natives Integrationsmodell. Consul wird mit einem einfachen integrierten Proxy ausgeliefert, sodass alles sofort funktioniert, unterstützt aber auch Proxy-Integrationen von Drittanbietern wie Envoy.
Die Hauptmerkmale von Consul sind:
Diensterkennung : Kunden von Consul können einen Dienst wie API oder MySQL registrieren, und andere Kunden können Consul verwenden, um Anbieter eines bestimmten Dienstes zu ermitteln. Mithilfe von DNS oder HTTP können Anwendungen die Dienste, auf die sie angewiesen sind, leicht finden.
Gesundheitsprüfung : Consul-Clients können eine beliebige Anzahl von Gesundheitsprüfungen durchführen, entweder im Zusammenhang mit einem bestimmten Dienst („gibt der Webserver 200 OK zurück“) oder mit dem lokalen Knoten („liegt die Speicherauslastung unter 90 %“)? Diese Informationen können von einem Betreiber zur Überwachung des Clusterzustands verwendet werden und werden von den Diensterkennungskomponenten verwendet, um den Datenverkehr von fehlerhaften Hosts wegzuleiten.
KV-Speicher : Anwendungen können den hierarchischen Schlüssel-/Wertspeicher von Consul für eine Reihe von Zwecken nutzen, einschließlich dynamischer Konfiguration, Funktionskennzeichnung, Koordination, Leiterwahl und mehr. Die einfache HTTP-API erleichtert die Verwendung.
Sichere Servicekommunikation : Consul kann TLS-Zertifikate für Dienste generieren und verteilen, um gegenseitige TLS-Verbindungen herzustellen. Mithilfe von Absichten kann definiert werden, welche Dienste kommunizieren dürfen. Die Dienstsegmentierung kann einfach mit Absichten verwaltet werden, die in Echtzeit geändert werden können, anstatt komplexe Netzwerktopologien und statische Firewall-Regeln zu verwenden.
Multi-Datacenter : Consul unterstützt sofort mehrere Datacenter. Dies bedeutet, dass Benutzer von Consul sich keine Gedanken über den Aufbau zusätzlicher Abstraktionsebenen machen müssen, um auf mehrere Regionen zu wachsen.
Consul ist so konzipiert, dass es sowohl für die DevOps-Community als auch für Anwendungsentwickler geeignet ist und sich daher perfekt für moderne, elastische Infrastrukturen eignet.
Quelle: Einführung des Konsuls
Ein wichtiger Teil des Tutorials ist die Verwendung des Consul zur dynamischen Erkennung von Dienstendpunkten. Sobald ein Dienst bei Consul registriert ist, kann er über typisches DNS oder eine benutzerdefinierte API erkannt werden.
Consul bietet Integritätsprüfungen für diese Dienstinstanzen. Wenn eine der Dienstinstanzen oder der Dienst selbst fehlerhaft ist oder die Gesundheitsprüfung nicht besteht, weiß die Registrierungsstelle davon und vermeidet die Rückgabe der Dienstadresse. Die Arbeit, die der Load-Balancer leisten würde, wird in diesem Fall von der Registry übernommen.
Da wir mehrere Instanzen desselben Dienstes verwenden, sendet Consul den Datenverkehr nach dem Zufallsprinzip an verschiedene Instanzen. Dadurch wird die Last zwischen den Dienstinstanzen ausgeglichen.
Consul bewältigt die Herausforderungen der Fehlererkennung und Lastverteilung auf mehrere Instanzen von Diensten, ohne dass ein zentraler Lastenausgleich erforderlich ist.
Es verwaltet automatisch die Registrierung, die aktualisiert wird, wenn eine neue Instanz des Dienstes registriert wird und für den Empfang von Datenverkehr verfügbar wird. Dies hilft uns, die Dienste einfach zu skalieren.
Bevor wir uns mit den Implementierungsdetails zur Implementierung der Selbstregistrierung bei Consul befassen, schauen wir uns an, wie die Diensterkennung mit Selbstregistrierung wirklich funktioniert.
Im ersten Schritt registriert sich eine Dienstinstanz beim Service Discovery Service, indem sie ihren Namen, ihre ID und ihre Adresse angibt. Nachdem dieses Gateway die Adresse dieses Dienstes erhalten kann, indem es die Consul-Diensterkennung nach seinem Namen/ID abfragt.
Hierbei ist vor allem zu beachten, dass die Dienstinstanzen mit einer eindeutigen Dienst-ID registriert werden, um zwischen Dienstinstanzen, die auf demselben Consul-Dienstagenten ausgeführt werden, eindeutig zu sein. Es ist erforderlich, dass alle Dienste eine eindeutige ID pro Knoten haben . Wenn also Namen in Konflikt geraten könnten (in unserem Fall), müssen eindeutige IDs bereitgestellt werden.
Schauen wir uns an, wie wir die Selbstregistrierung in der .NET-Anwendung implementieren können. Zuerst müssen wir die für die Diensterkennung erforderliche Konfiguration aus Umgebungsvariablen lesen, die über die Datei docker-compose.override.yml übergeben wurden.
public static class ServiceConfigExtensions
{
public static ServiceConfig GetServiceConfig ( this IConfiguration configuration )
{
ArgumentNullException . ThrowIfNull ( configuration ) ;
ServiceConfig serviceConfig = new ( )
{
Id = configuration . GetValue < string > ( "ServiceConfig:Id" ) ,
Name = configuration . GetValue < string > ( "ServiceConfig:Name" ) ,
ApiUrl = configuration . GetValue < string > ( "ServiceConfig:ApiUrl" ) ,
Port = configuration . GetValue < int > ( "ServiceConfig:Port" ) ,
ConsulUrl = configuration . GetValue < Uri > ( "ServiceConfig:ConsulUrl" ) ,
HealthCheckEndPoint = configuration . GetValue < string > ( "ServiceConfig:HealthCheckEndPoint" ) ,
} ;
return serviceConfig ;
}
}
Nachdem wir die Konfiguration gelesen haben, die zum Erreichen des Service Discovery Service erforderlich ist, können wir damit unseren Service registrieren. Der folgende Code ist als Hintergrundaufgabe (gehosteter Dienst) implementiert, der den Dienst in Consul registriert , indem er vorherige Informationen über den Dienst überschreibt, falls solche vorhanden waren. Wenn der Dienst heruntergefahren wird, wird er automatisch aus der Consul-Registrierung abgemeldet .
public class ServiceDiscoveryHostedService (
ILogger < ServiceDiscoveryHostedService > logger ,
IConsulClient client ,
ServiceConfig config )
: IHostedService
{
private AgentServiceRegistration _serviceRegistration ;
/// <summary>
/// Registers service to Consul registry
/// </summary>
public async Task StartAsync ( CancellationToken cancellationToken )
{
_serviceRegistration = new AgentServiceRegistration
{
ID = config . Id ,
Name = config . Name ,
Address = config . ApiUrl ,
Port = config . Port ,
Check = new AgentServiceCheck ( )
{
DeregisterCriticalServiceAfter = TimeSpan . FromSeconds ( 5 ) ,
Interval = TimeSpan . FromSeconds ( 15 ) ,
HTTP = $ "http:// { config . ApiUrl } : { config . Port } /api/values/ { config . HealthCheckEndPoint } " ,
Timeout = TimeSpan . FromSeconds ( 5 )
}
} ;
try
{
await client . Agent . ServiceDeregister ( _serviceRegistration . ID , cancellationToken ) . ConfigureAwait ( false ) ;
await client . Agent . ServiceRegister ( _serviceRegistration , cancellationToken ) . ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
logger . LogError ( ex , $ "Error while trying to deregister in { nameof ( StartAsync ) } " ) ;
}
}
/// <summary>
/// If the service is shutting down it deregisters service from Consul registry
/// </summary>
public async Task StopAsync ( CancellationToken cancellationToken )
{
try
{
await client . Agent . ServiceDeregister ( _serviceRegistration . ID , cancellationToken ) . ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
logger . LogError ( ex , $ "Error while trying to deregister in { nameof ( StopAsync ) } " ) ;
}
}
}
Sobald wir unsere Dienste im Service Discovery Service registriert haben, können wir mit der Implementierung der Gateway API beginnen.
Ocelot erfordert, dass Sie eine Konfigurationsdatei bereitstellen, die eine Liste von Routen (Konfiguration zur Zuordnung von Upstream-Anfragen) und eine globale Konfiguration (andere Konfiguration wie QoS, Ratenbegrenzung usw.) enthält. In der Datei ocelot.json unten 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 durchläuft und Anfragen an verfügbare Dienste sendet. Es ist wichtig, den Consul als Diensterkennungsdienst in GlobalConfiguration für ServiceDiscoveryProvider festzulegen.
{
"Routes" : [
{
"Servicename" : " ValueService " ,
"DownstreamPathTemplate" : " /{everything} " ,
"DownstreamScheme" : " http " ,
"UpstreamPathTemplate" : " /{everything} " ,
"UpstreamHttpMethod" : [ " GET " ],
"UseServiceDiscovery" : true ,
"RouteIsCaseSensitive" : false ,
"LoadBalancerOptions" : {
"Type" : " RoundRobin "
},
"QoSOptions" : {
"ExceptionsAllowedBeforeBreaking" : 3 ,
"DurationOfBreak" : 5000 ,
"TimeoutValue" : 2000
}
}
],
"GlobalConfiguration" : {
"RequestIdKey" : " OcelotRequestId " ,
"UseServiceDiscovery" : true ,
"ServiceDiscoveryProvider" : {
"Host" : " consul " ,
"Port" : 8500 ,
"Type" : " PollConsul " ,
"PollingInterval" : 100
}
}
}
Hier sind einige notwendige Erklärungen für ServiceDiscoveryProvider -Einstellungen im Abschnitt „GlobalConfiguration“ :
Nachdem wir unsere Konfiguration definiert haben, können wir mit der Implementierung von API Gateway beginnen. Unten sehen wir die Implementierung des Ocelot API Gateway-Dienstes, der unsere Konfigurationsdatei ocelot.json und Consul als Dienstregistrierung verwendet.
IHostBuilder hostBuilder = Host . CreateDefaultBuilder ( args )
. UseContentRoot ( Directory . GetCurrentDirectory ( ) )
. ConfigureWebHostDefaults ( webBuilder =>
{
webBuilder . ConfigureServices ( services =>
services
. AddOcelot ( )
. AddConsul < MyConsulServiceBuilder > ( )
. AddCacheManager ( x =>
{
x . WithDictionaryHandle ( ) ;
} )
. AddPolly ( ) ) ;
webBuilder . Configure ( app =>
app . UseOcelot ( ) . Wait ( ) )
. ConfigureAppConfiguration ( ( hostingContext , config ) =>
{
config
. SetBasePath ( hostingContext . HostingEnvironment . ContentRootPath )
. AddJsonFile ( "appsettings.json" , false , true )
. AddJsonFile ( $ "appsettings. { hostingContext . HostingEnvironment . EnvironmentName } .json" , true , true )
. AddJsonFile ( "ocelot.json" , false , true )
. AddEnvironmentVariables ( ) ;
} )
. ConfigureLogging ( ( builderContext , logging ) =>
{
logging . ClearProviders ( ) ;
logging . AddConsole ( ) ;
logging . AddDebug ( ) ;
} ) ;
} ) ;
IHost host = hostBuilder . Build ( ) ;
await host . RunAsync ( ) ;
Wie bereits erwähnt, werden wir alle Dienste mit Docker, einschließlich Consul , containerisieren und dabei Lightweight GNU/Linux-Distributionen für Container verwenden.
Die Datei „docker-compose.yml“ mit dem Setup für alle Container sieht folgendermaßen aus:
services :
services :
consul :
image : hashicorp/consul
container_name : consul
command : consul agent -dev -log-level=warn -ui -client=0.0.0.0
hostname : consul
networks :
- common_network
valueservice1.openapi :
image : valueservice.openapi:latest
container_name : valueservice1.openapi
restart : on-failure
hostname : valueservice1.openapi
build :
context : .
dockerfile : src/ValueService.OpenApi/Dockerfile
networks :
- common_network
valueservice2.openapi :
image : valueservice.openapi:latest
container_name : valueservice2.openapi
restart : on-failure
hostname : valueservice2.openapi
build :
context : .
dockerfile : src/ValueService.OpenApi/Dockerfile
networks :
- common_network
valueservice3.openapi :
image : valueservice.openapi:latest
container_name : valueservice3.openapi
restart : on-failure
hostname : valueservice3.openapi
build :
context : .
dockerfile : src/ValueService.OpenApi/Dockerfile
networks :
- common_network
services.gateway :
image : services.gateway:latest
container_name : services.gateway
restart : on-failure
hostname : services.gateway
build :
context : .
dockerfile : src/Services.Gateway/Dockerfile
networks :
- common_network
networks :
common_network :
driver : bridge
Beachten Sie, dass unsere Dienste keine Konfigurationsdateien enthalten. Zu diesem Zweck verwenden wir die Datei Docker-compose.override.yml :
services :
consul :
ports :
- " 8500:8500 "
valueservice1.openapi :
# Swagger UI: http://localhost:9100/index.html
# http://localhost:9100/api/values
environment :
- ASPNETCORE_ENVIRONMENT=Development
- ServiceConfig__ApiUrl=valueservice1.openapi
- ServiceConfig__ConsulUrl=http://consul:8500
- ServiceConfig__HealthCheckEndPoint=healthcheck
- ServiceConfig__Id=ValueService.OpenApi-9100
- ServiceConfig__Name=ValueService
- ServiceConfig__Port=8080
ports :
- 9100:8080
depends_on :
- consul
valueservice2.openapi :
# Swagger UI: http://localhost:9200/index.html
# http://localhost:9200/api/values
environment :
- ASPNETCORE_ENVIRONMENT=Development
- ServiceConfig__ApiUrl=valueservice2.openapi
- ServiceConfig__ConsulUrl=http://consul:8500
- ServiceConfig__HealthCheckEndPoint=healthcheck
- ServiceConfig__Id=ValueService.OpenApi-9200
- ServiceConfig__Name=ValueService
- ServiceConfig__Port=8080
ports :
- 9200:8080
depends_on :
- consul
valueservice3.openapi :
# Swagger UI: http://localhost:9300/index.html
# http://localhost:9300/api/values
environment :
- ASPNETCORE_ENVIRONMENT=Development
- ServiceConfig__ApiUrl=valueservice3.openapi
- ServiceConfig__ConsulUrl=http://consul:8500
- ServiceConfig__HealthCheckEndPoint=healthcheck
- ServiceConfig__Id=ValueService.OpenApi-9300
- ServiceConfig__Name=ValueService
- ServiceConfig__Port=8080
ports :
- 9300:8080
depends_on :
- consul
services.gateway :
# Call first available service: http://localhost:9500/api/values
environment :
- ASPNETCORE_ENVIRONMENT=Development
ports :
- 9500:8080
depends_on :
- consul
- valueservice1.openapi
- valueservice2.openapi
- valueservice3.openapi
Um die Compose-Datei auszuführen, öffnen Sie Powershell und navigieren Sie zur Compose-Datei im Stammordner. Führen Sie dann den folgenden Befehl aus: docker-compose up -d --build --remove-orphans, der alle Dienste startet und ausführt. Der Parameter -d führt den Befehl getrennt aus. Das bedeutet, dass die Container im Hintergrund laufen und Ihr Powershell-Fenster nicht blockieren. Um alle laufenden Container zu überprüfen, verwenden Sie den Befehl docker ps .
Der Consul bietet sofort eine schöne Web-Benutzeroberfläche. Sie können über Port 8500 darauf zugreifen: http://localhost:8500. Schauen wir uns einige der Bildschirme an.
Die Homepage für die Consul-UI-Dienste mit allen relevanten Informationen zu einem Consul-Agenten- und Webservice-Check.
Lassen Sie uns mehrere Aufrufe über API Gateway durchführen: http://localhost:9500/api/values. Der Load Balancer durchläuft die verfügbaren Dienste, sendet Anfragen und gibt Antworten zurück:
Microservice-Systeme sind nicht einfach aufzubauen und zu warten. Aber dieses Tutorial hat gezeigt, wie einfach es ist, eine Anwendung mit einer Microservice-Architektur zu entwickeln und bereitzustellen. HashiCorp Consul bietet erstklassigen Support für Service Discovery, Health Check, Schlüsselwertspeicherung und Multi-Datacenter. Ocelot kommuniziert als Gateway erfolgreich mit der Consul-Dienstregistrierung und ruft Dienstregistrierungen ab, der Load Balancer durchläuft verfügbare Dienste und sendet Anfragen. Die Verwendung beider erleichtert Entwicklern, die vor solchen Herausforderungen stehen, das Leben erheblich. Sind Sie einverstanden?
Genießen!
Lizenziert unter MIT. Kontaktieren Sie mich auf LinkedIn.