Navigieren Sie die Microservice-Suche: Clientseitige und serverseitige Muster entmystifiziert
Lukas Schneider
DevOps Engineer · Leapcell

Einleitung
Der architektonische Stil der Microservices hat aufgrund seiner Flexibilität, Skalierbarkeit und Ausfallsicherheit immense Popularität erlangt. Diese verteilte Natur bringt jedoch neue Komplexitäten mit sich, von denen eine der grundlegendsten darin besteht, wie Dienste einander lokalisieren und miteinander kommunizieren. In einer dynamischen Umgebung, in der Dienstaninstanzen ständig gestartet, skaliert oder beendet werden, ist das Hardcodieren von Netzwerkstandorten einfach nicht praktikabel. Diese Herausforderung führt zum Konzept der Service-Erkennung, einem kritischen Mechanismus, der es Diensten ermöglicht, verfügbare Instanzen anderer Dienste zu finden. Das Verständnis der Nuancen zwischen clientseitigen und serverseitigen Muster der Service-Erkennung ist für den Aufbau robuster und wartbarer Microservice-Ökosysteme von größter Bedeutung. Dieser Artikel zielt darauf ab, diese beiden primären Ansätze eingehend zu vergleichen, ihre zugrunde liegenden Mechanismen, praktischen Implementierungen und geeigneten Szenarien zu untersuchen und Entwickler letztendlich bei informierten architektonischen Entscheidungen zu unterstützen.
Entschlüsselung von Service-Erkennungsmustern
Bevor wir uns mit den Besonderheiten von clientseitiger und serverseitiger Service-Erkennung befassen, wollen wir einige wichtige Begriffe klären, die in unserer Diskussion häufig vorkommen werden.
- Service-Registry: Eine zentrale Datenbank oder ein Repository, das die Netzwerkstandorte (IP-Adressen und Ports) aller verfügbaren Dienstaninstanzen speichert. Dienste registrieren sich beim Start selbst und de-registrieren sich beim Herunterfahren.
- Dienstaninstanz: Ein laufender Prozess eines bestimmten Dienstes, der durch seine Netzwerkadresse und oft eine eindeutige ID identifiziert wird.
- Dienstanbieter: Der Dienst, der eine API oder Funktionalität für andere Dienste bereitstellt.
- Dienstkonsument (Client): Der Dienst, der die vom Dienstanbieter angebotene Funktionalität verbrauchen muss.
Mit diesen Definitionen im Hinterkopf wollen wir die beiden primären Muster der Service-Erkennung untersuchen.
Clientseitige Service-Erkennung
Beim Pattern der clientseitigen Service-Erkennung ist der Client (Dienstkonsument) dafür verantwortlich, die Service-Registry abzufragen, um verfügbare Instanzen des Dienstes zu finden, mit dem er kommunizieren möchte. Sobald er die Netzwerkstandorte erhalten hat, verwendet der Client dann einen Lastausgleichsalgorithmus, um eine der verfügbaren Instanzen auszuwählen, und stellt die Anfrage direkt.
Funktionsweise:
- Dienstregistrierung: Wenn eine Dienstanbieterinstanz gestartet wird, registriert sie ihren Netzwerkstandort (IP-Adresse, Port) bei der Service-Registry. Sie sendet oft periodische Herzschläge, um ihren Zustand und ihre Verfügbarkeit anzuzeigen.
- Dienst-Erkennung: Wenn ein Dienstkonsument einen Dienstanbieter aufrufen muss, fragt er die Service-Registry nach allen verfügbaren Instanzen dieses Dienstes ab.
- Lastausgleich: Der Dienstkonsument verwendet dann einen integrierten oder externen Lastausgleicher (oder einen benutzerdefinierten Algorithmus), um eine gesunde Dienstaninstanz aus der Liste auszuwählen.
- Direkte Kommunikation: Der Dienstkonsument kommuniziert direkt mit der ausgewählten Dienstaninstanz.
Implementierungsbeispiel:
Eine gängige Implementierung für die clientseitige Service-Erkennung verwendet Netflix Eureka als Service-Registry und Netflix Ribbon als clientseitigen Lastausgleicher.
Nehmen wir an, wir haben einen ProductService, der einen OrderService aufrufen muss.
OrderService (Dienstanbieter):
// Spring Boot-Anwendung für OrderService @SpringBootApplication @EnableEurekaClient // Aktiviert Eureka-Client für die Dienstregistrierung public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } @RestController class OrderController { @GetMapping("/orders/{id}") public String getOrder(@PathVariable Long id) { return "Order details for ID: " + id; } } }
ProductService (Dienstkonsument):
// Spring Boot-Anwendung für ProductService @SpringBootApplication @EnableEurekaClient // Aktiviert Eureka-Client für die Service-Erkennung public class ProductServiceApplication { public static void main(String[] args) { SpringApplication.run(ProductServiceApplication.class, args); } @RestController class ProductController { // Verwendung von Spring Clouds RestTemplate mit Ribbon für clientseitigen Lastausgleich // Die @LoadBalanced-Annotation macht den RestTemplate mit Ribbon kompatibel private final RestTemplate restTemplate; public ProductController(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @GetMapping("/products/{productId}/order-info") public String getProductOrderInfo(@PathVariable Long productId) { // "ORDER-SERVICE" ist der logische Dienstname, der in Eureka registriert ist String orderInfo = restTemplate.getForObject("http://ORDER-SERVICE/orders/" + productId, String.class); return "Product " + productId + " order details: " + orderInfo; } } @Bean @LoadBalanced // Wesentlich für die Ribbon-Integration public RestTemplate restTemplate() { return new RestTemplate(); } }
In diesem Beispiel:
OrderServiceregistriert sich bei Eureka.ProductServiceverwendet einen@LoadBalanced RestTemplate. WennrestTemplate.getForObject("http://ORDER-SERVICE/...")aufgerufen wird, fängt Ribbon die Anfrage ab, fragt Eureka nach Instanzen von "ORDER-SERVICE" ab, wählt eine aus und schreibt die URL auf die tatsächliche IP und den Port um.
Vorteile:
- Einfachere Netzwerktopologie: Clients kommunizieren direkt mit Instanzen, wodurch ein zusätzlicher Hop vermieden wird.
- Kostengünstiger Lastausgleich: Die Nutzung von Client-Bibliotheken kann wirtschaftlicher sein als dedizierte Hardware-Lastausgleicher.
- Flexiblere Lastausgleichsregeln: Client-Bibliotheken erlauben oft anspruchsvolle Lastausgleichsalgorithmen, die spezifisch für die Bedürfnisse des Konsumenten sind.
- Reduzierte Latenz: Direkte Kommunikation kann potenziell zu geringerer Latenz im Vergleich zu einem zusätzlichen Hop über einen Proxy führen.
Nachteile:
- Sprachabhängigkeit: Die Logik für Service-Erkennung (und Lastausgleich) muss in jeder Client-Anwendung implementiert oder integriert werden, potenziell über verschiedene Programmiersprachen hinweg.
- Erhöhte Komplexität für Clients: Clients werden komplexer, da sie Erkennung, Lastausgleich und potenziell Circuit Breaking verwalten müssen.
- Schwieriger zu aktualisieren: Jede Änderung am Erkennungsmechanismus erfordert die Aktualisierung und erneute Bereitstellung aller Client-Dienste.
Anwendungsszenarien:
- Umgebungen mit einer begrenzten Anzahl von Client-Technologien (z. B. hauptsächlich Java-Dienste, die Spring Cloud verwenden).
- Wenn eine feingranulare Kontrolle über den Lastausgleich durch den Client gewünscht wird.
- Wenn Infrastrukturkosten für dedizierte Lastausgleicher ein erhebliches Problem darstellen.
Serverseitige Service-Erkennung
Beim Pattern der serverseitigen Service-Erkennung stellt der Client (Dienstkonsument) Anforderungen an einen Proxy (oft an ein API-Gateway oder einen spezialisierten Lastausgleicher) unter einer bekannten URL. Dieser Proxy ist dafür verantwortlich, die Service-Registry abzufragen, eine verfügbare Instanz auszuwählen und die Anfrage an diese Instanz weiterzuleiten. Der Client bleibt von den Details der Dienstregistrierung und des Lastausgleichs unberührt.
Funktionsweise:
- Dienstregistrierung: Ähnlich wie bei der clientseitigen Registrierung registriert die Dienstanbieterinstanz ihren Netzwerkstandort bei der Service-Registry.
- Anforderungsrouting: Wenn ein Dienstkonsument einen Dienst aufrufen muss, sendet er die Anfrage an einen bekannten Endpunkt eines Proxys (z. B. Lastausgleicher, API-Gateway).
- Erkennung durch Proxy: Der Proxy fragt die Service-Registry ab, um verfügbare Instanzen des Ziel Dienstes zu finden.
- Lastausgleich und Weiterleitung: Der Proxy wählt mithilfe eines Lastausgleichsalgorithmus eine gesunde Dienstaninstanz aus und leitet die clientseitige Anfrage an diese weiter.
- Antwort: Die Antwort von der Dienstaninstanz wird über den Proxy an den Client zurückgegeben.
Implementierungsbeispiel:
Eine gängige Implementierung beinhaltet die Verwendung eines Lastausgleichers (wie AWS ELB/ALB, Nginx oder Kubernetes Ingress) zusammen mit einer Service-Registry wie Consul oder etcd. Für Kubernetes ist die interne DNS-basierte Service-Erkennung ein Paradebeispiel für serverseitige Erkennung.
Betrachten wir die Integration mit einem API-Gateway wie Spring Cloud Gateway oder einem Reverse-Proxy.
OrderService (Dienstanbieter):
// Spring Boot-Anwendung für OrderService @SpringBootApplication public class OrderServiceApplication { // Kein @EnableEurekaClient direkt am Dienst, wenn externe Erkennung wie Consul DNS verwendet wird public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } @RestController class OrderController { @GetMapping("/orders/{id}") public String getOrder(@PathVariable Long id) { return "Order details for ID: " + id + " from instance: " + System.getenv("HOSTNAME"); // Oder eine eindeutige Kennung } } }
Hinweis: Bei einem echten serverseitigen Erkennungsszenario wie Kubernetes muss der Dienst selbst oft keine expliziten Erkennungs-Client-Annotationen haben. Er stellt einfach einen Port bereit.
API-Gateway (Serverseitiger Entdecker/Router):
// Spring Cloud Gateway-Anwendung @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { // Angenommen, "order-service" ist über DNS oder eine mit dem Gateway integrierte Service-Registry auflösbar return builder.routes() .route("order_route", r -> r.path("/api/orders/**") .uri("lb://ORDER-SERVICE")) // "ORDER-SERVICE" ist typischerweise in einer Registry wie Eureka/Consul registriert .build(); } }
In diesem Beispiel:
OrderServicewird einfach ausgeführt und stellt seinen Endpunkt bereit.GatewayApplicationfungiert als serverseitiger Entdecker. Wenn eine Anfrage unter/api/orders/**eingeht, verwendet das Gateway seinen internen Routing-Mechanismus (der oft in eine Service-Registry oder Kubernetes-DNS integriert ist), umORDER-SERVICEin eine tatsächliche Instanz aufzulösen und die Anfrage weiterzuleiten.- Der
ProductService(Client) würde dann einfachhttp://gateway-host/api/orders/{id}aufrufen, ohne selbst eine Erkennungslogik zu benötigen.
Vorteile:
- Entkoppelte Clients: Clients sind vollständig unbewusst vom Erkennungsprozess. Sie stellen einfach Anforderungen an den Proxy.
- Sprachunabhängig: Da die Erkennungslogik im Proxy verbleibt, funktioniert sie nahtlos mit Clients, die in jeder Sprache geschrieben sind.
- Zentralisierte Kontrolle: Verwaltung von Service-Erkennung, Lastausgleich und Routing an einem Ort.
- Einfachere Updates: Änderungen an der Service-Erkennungslogik oder den Lastausgleichsalgorithmen erfordern lediglich die Aktualisierung des Proxys, nicht jedes Clients.
- Verbesserte Sicherheit: Der Proxy kann als Durchsetzungsstelle für Sicherheitspolitiken, Ratenbegrenzung und andere übergreifende Belange dienen.
Nachteile:
- Zusätzlicher Netzwerk-Hop: Alle Anforderungen durchlaufen den Proxy, was einen zusätzlichen Latenz-Hop einführt.
- Einzelner Fehlerpunkt (wenn nicht richtig verwaltet): Der Proxy selbst kann zu einem Engpass oder einem einzelnen Fehlerpunkt werden, wenn er nicht hochverfügbar und skalierbar ist.
- Erhöhte Infrastrukturkomplexität: Erfordert die Bereitstellung und Verwaltung einer dedizierten Proxy-Schicht.
- Kosten: Kann zusätzliche Kosten für Proxy-Infrastruktur und Wartung verursachen.
Anwendungsszenarien:
- Microservice-Architekturen mit vielfältigen Client-Technologien (polyglotte Umgebungen).
- Notwendigkeit der zentralen Steuerung von Routing, Sicherheit und übergreifenden Belangen.
- Öffentlich zugängliche APIs, bei denen ein API-Gateway naturgemäß vorhanden ist.
- Umgebungen wie Kubernetes, in denen interne DNS-basierte Service-Erkennung und Ingress-Controller diese Funktionalität inhärent bieten.
Fazit
Sowohl clientseitige als auch serverseitige Muster der Service-Erkennung lösen das Problem der Lokalisierung von Diensten in einer dynamischen Microservice-Umgebung effektiv, unterscheiden sich jedoch grundlegend darin, wo die Erkennungslogik angesiedelt ist. Die clientseitige Erkennung legt die Verantwortung auf den Konsumenten und bietet Flexibilität und potenziell geringere Latenz auf Kosten erhöhter clientseitiger Komplexität und Kopplung. Die serverseitige Erkennung zentralisiert die Erkennung und das Routing in einem Proxy und bietet starke Entkopplung, Sprachunabhängigkeit und zentrale Steuerung, wenn auch auf Kosten eines zusätzlichen Netzwerk-Hops und erhöhter Infrastruktur. Die optimale Wahl hängt von Ihren spezifischen architektonischen Anforderungen, der Expertise des Entwicklungsteams, dem Technologie-Stack und den betrieblichen Überlegungen ab. Letztendlich sind beide Muster entscheidend für die Ermöglichung robuster und skalierbarer Microservice-Architekturen.

