Verteilte Anwendungen mit Cloud-nativen Technologien
Softwareentwicklung gewinnt durch Microservices und Container neue Freiheiten: autonome Teams, polyglotte Sprachen und Frameworks sowie mehr Resilienz.
- Matthias Haeussler
- Verteilte Anwendungen ermöglichen heterogene Umgebungen mit verschiedenen Systemen und Architekturen. Vorteile sind Plattformunabhängigkeit, Verfügbarkeit und Skalierbarkeit.
- Der Artikel zeigt die verschiedenen Möglichkeiten der Konfiguration, Architektur und modularen Gestaltung mit verschiedenen Techniken und Frameworks. Im Zentrum steht dabei Kubernetes.
- Über Service Meshs mit oder ohne Sidecar-Modell lässt sich die Kommunikation im modularen Netz überwachen, analysieren und steuern.
- Die von Adam Wiggins postulierten zwölf Grundsätze für gute Cloud-Apps gelten in hohem Maße auch für verteilte Anwendungen.
Verteilte Anwendungen und verteilte Systeme im weiteren Sinne erfreuen sich wachsender Beliebtheit, hauptsächlich dank der Entwicklungen im Bereich von Microservices und Container-Technologie. Der damit einhergehende technische Fortschritt ermöglicht unabhängiges Entwickeln in autonomen Teams, die Freiheit, Sprachen und Frameworks auszuwählen, sowie die Verbesserung der Resilienz durch Skalierung und Lastverteilung.
Dieser Artikel untersucht verschiedene Ansätze zur Implementierung verteilter Anwendungsarchitekturen mithilfe moderner, Cloud-nativer Softwaretechnologien. Dazu zählen einerseits an Programmiersprachen gebundene Frameworks, die insbesondere im Java-Umfeld sehr verbreitet sind. Andererseits gibt es Plattformen wie Kubernetes und Service-Meshes – sowohl im traditionellen Stil als auch neue Varianten ohne Sidecar.
Zunächst gilt es, die Frage zu klären, warum eine verteilte Anwendungsarchitektur überhaupt sinnvoll ist und welche Komponenten für eine erfolgreiche Umsetzung notwendig sind. Zur Orientierung dienen dabei auch die von Adam Wiggins bereits 2011 veröffentlichten Prinzipien der 12-Faktor-App (siehe Kasten "Konzept der 12-Faktor-App").
Im Jahr 2011 veröffentlichte Adam Wiggins das Konzept der 12-Faktor-App. Der Mitgründer des PaaS-Anbieters Heroku beschrieb damit eine bewährte Vorgehensweise (Best Practices) für die Entwicklung von Anwendungen, um sie effizient in der Cloud zu betreiben und deren Möglichkeiten optimal zu nutzen. Die 12-Faktor-App definiert grundlegende Prinzipien, die eine Anwendung plattformunabhängig, skalierbar und granular konfigurierbar machen sollen. Zudem enthalten die Prinzipien nicht nur Cloud-spezifische Aspekte, sondern auch allgemeine Grundsätze guter Softwareentwicklung, etwa im Hinblick auf das Verwenden eines zentralen Versionskontrollsystems pro Komponente und eine saubere Trennung von Code und Abhängigkeiten. Indem Entwickler die zwölf Prinzipien befolgen, stellen sie sicher, dass ihre Anwendungen gut auf die Cloud-Infrastruktur abgestimmt sind und von den Vorteilen der Plattform profitieren.
Besonders hervorzuheben sind folgende Faktoren:
- Faktor 3: "Config" (Trennung von Konfiguration und Code): Die Trennung von Konfiguration und Code ermöglicht einerseits eine flexible Anpassung der Anwendung ohne Neukompilierung und andererseits einen Einsatz in verschiedenen Umgebungen mit unterschiedlichen Konfigurationen.
- Faktor 6: "Processes" (Zustandslosigkeit und Skalierbarkeit): Zustandslose Prozesse erleichtern das Skalieren und Verwalten der Anwendung, da die Prozesse keine Informationen ĂĽber ihren vorherigen Zustand speichern mĂĽssen.
- Faktor 7: "Port binding" (Bindung an Ports und Netzwerkkommunikation): Das Verwenden standardisierter Netzwerkprotokolle vereinfacht die Interaktion zwischen den Komponenten und ermöglicht eine einfache Integration in verschiedene Umgebungen.
- Faktor 11: "Logs" (Logs als Streams): Die Behandlung von Logs als Streams ermöglicht eine effizientere Fehleranalyse und -behebung bei der Verwaltung verteilter Anwendungen. Die Prinzipien der 12-Faktor-App kommen jenseits von Heroku in Systemen wie Cloud Foundry, Spring Boot, Docker und Kubernetes zum Einsatz, um moderne Anwendungen erfolgreich in einer dynamischen und agilen Umgebung zu betreiben.
Warum verteilte Systeme?
Der traditionelle monolithische Ansatz in der Softwarearchitektur gilt vielen als nicht mehr zeitgemäß. Insbesondere Anhänger verteilter Systeme und Microservice-Architekturen sprechen häufig abfällig von "Big Ball of Mud". Doch auch die als modern geltenden verteilten Ansätze sind nicht frei von Problemen, wie Peter Deutsch bereits 1994 in seinen "Fallacies of distributed computing" (Irrtümer der verteilten Datenverarbeitung) zusammengefasst hatte, die bis heute ihre Gültigkeit nicht verloren haben.
Insbesondere führt das Aufteilen einer Anwendung in verschiedene Module zu einer Netzwerkabhängigkeit zwischen den Komponenten, die wiederum Einfluss auf Latenzzeiten, Konfiguration und Fehlerbehandlung hat. Dennoch ist es in bestimmten Szenarien sinnvoll – mitunter sogar unausweichlich.
Im Folgenden geht es darum, die Vorteile und die damit verbundenen Aspekte näher zu beleuchten. Das grundlegende Ziel einer verteilten Anwendung sollte es sein, sowohl Nutzerinnen und Nutzern als auch den Entwicklungsteams Vorteile zu liefern. Die liegen hierbei vor allem in nicht-funktionalen Anforderungen wie Verfügbarkeit, Ausfallsicherheit und Skalierbarkeit.
Ein solches System sollte sich für Nutzer wie eine Einheit anfühlen – ganz im Sinne von Andrew Tanenbaums in seinem Buch "Verteilte Systeme" formulierter Forderung. Wer Google Maps benutzt, den interessiert nicht, wie viele Container dahinterstehen oder welche Programmiersprachen zum Einsatz kommen, es zählen allein Verlässlichkeit und Funktion.
Faktor Heterogenität
In der Theorie verteilter Systeme spielt Heterogenität eine zentrale Rolle, etwa im Hinblick auf Parallelität und Nebenläufigkeit. Durch parallele Verarbeitung heterogener Tasks lässt sich, wie Abbildung 1 zeigt, eine höhere Effizienz erzielen.
Heterogenität spiegelt sich darüber hinaus auch in Abhängigkeiten von Betriebssystemen, Runtimes, Frameworks etc. wider (siehe Abbildung 2). In all diesen Fällen ist es nicht möglich, eine Anwendung in ein monolithisches Artefakt zu bringen, was einen verteilten Ansatz unabdingbar macht.
Abschließend sei noch die Erweiterbarkeit erwähnt. Eine verteilte Architektur bietet den Vorteil, dass sich neue Komponenten als eigenständige Module in ein bestehendes System integrieren lassen, ohne dass damit nennenswerte Auswirkungen auf die bestehenden Module einhergehen. Es ist also kein erneutes Kompilieren oder Paketieren der Komponenten erforderlich.
Faktor Resilienz
Beim Faktor Resilienz geht es hauptsächlich darum, die Anwendung hochverfügbar zu machen und sie gegen unvorhersehbare Ereignisse, wie Schwankungen in der Benutzerzahl oder Ausfälle von Teilsystemen beziehungsweise Netzwerksegmenten, widerstandsfähig zu halten. Der Ausfall einer solchen Komponente sollte kontrollierbar sein und keinesfalls zu einem Ausfall der Gesamtanwendung führen. Das Skalieren individueller Komponenten ermöglicht hierbei Ausfallsicherheit durch Redundanz. Beim Ausfall einer Instanz sollten also noch ausreichend andere Instanzen vorhanden sein, sodass der Service ohne Unterbrechung fortfährt (siehe Abbildung 3). Das dient nicht nur der Redundanz, sondern auch der Lastverteilung, um beispielsweise im Falle steigender Nutzerzahlen die Last gleichmäßig auf die einzelnen Komponenten zu verteilen und so die gewünschte Leistung des Gesamtsystems sicherzustellen (siehe Abbildung 4).
Bei einem plötzlichen Anstieg der Nutzerzahlen oder – schlimmer – einer Denial-of-Service-Attacke kann die Last rasch so stark zunehmen, dass sie sich auch durch Skalierung nicht mehr ausgleichen lässt. Um die Anwendung davor zu schützen, lässt sich eine Netzwerkkomponente platzieren, die den eingehenden Verkehr blockiert oder zumindest drosselt. Zum Einsatz kommen in solchen Fällen meist sogenannte Circuit Breaker oder Bulkheads.
Ein weiterer Aspekt von Resilienz ist die unterbrechungsfreie Verfügbarkeit während eines Updates der Anwendung. Um das sicherzustellen, stehen verschiedene Deployment- und Zero-Downtime-Techniken zur Verfügung – darunter Blue/Green Deployments und Canary Releases.
Beide Varianten funktionieren grundsätzlich nach dem Prinzip, dass eine neue Version der Anwendung deployt wird, während die alte noch läuft. Während bei Blue/Green Deployments der Wechsel in einem einzigen Schritt erfolgt, führt ein Canary Deployment die neue Version schrittweise und selektiv zunächst für eine begrenzte Gruppe von Nutzern ein, bevor das neue Release vollständig in den Produktionsbetrieb übergeht.
Kommunikation in verteilten Systemen
Um die bisher genannten Vorteile verteilter Architektur nutzen zu können, müssen eine Reihe von Voraussetzungen erfüllt sein, die ein richtiges Funktionieren sicherstellen. Gemäß des 12-Faktor-Modells sollte die Kommunikation nahezu vollständig über Netzwerkprotokolle und nicht über das Dateisystem erfolgen (vergleiche Faktor 7: Ports). Nur so lässt sich eine konsistente und standardisierte Kommunikation zwischen den verschiedenen Teilen des Systems gewährleisten. Ein Schlüsselfaktor dafür sind standardisierte Sprachen und Protokolle wie HTTP, REST oder gRPC. Sie schaffen eine einheitliche Schnittstelle und gewährleisten, dass sämtliche Teile "die gleiche Sprache sprechen".
Dieser Ansatz entspricht dem aus dem Domain-Driven Design (DDD) abgeleiteten Konzept der "Ubiquitous Language", das zur einfacheren Entwicklung und Wartung verteilter Systeme beitragen soll. Standardisierung ermöglicht darüber hinaus die Integration von Komponenten, die in verschiedenen Programmiersprachen entwickelt wurden, da die Kommunikation unabhängig davon erfolgt. Dies trägt zu mehr Flexibilität bei der Auswahl der geeigneten Technologie für jede einzelne Komponente bei.
Service Registry als zentrale Vermittlungsstelle
In verteilten Anwendungen spielt die Service Registry eine wichtige Rolle, um eine effiziente Kommunikation zwischen den Komponenten zu ermöglichen. Als zentrales Verzeichnis speichert sie Informationen über die verfügbaren Dienste und deren Standorte (siehe Abbildung 5). Sie fungiert quasi als Telefonbuch, in dem sich einzelne Dienste registrieren und ihre Informationen wie Hostnamen, IP-Adressen und Ports hinterlegen.
Ăśber die Service Registry entdecken sich die Komponenten einer verteilten Anwendung dynamisch gegenseitig und interagieren miteinander, ohne sich vorab explizit ĂĽber ihre Standorte und Schnittstellen ausgetauscht haben zu mĂĽssen (siehe Abbildung 6). In verteilten Umgebungen ist das hilfreich, da die Anzahl der Instanzen und die Auslastung der einzelnen Dienste variieren.
Das Verwenden einer Service Registry erleichtert nicht nur die Skalierung und Lastverteilung, sondern trägt auch zu erhöhter Ausfallsicherheit und Resilienz bei. Fällt eine Instanz eines Dienstes aus, kann die Service Registry automatisch erkennen, dass dieser nicht mehr verfügbar ist, und andere Instanzen als Ersatz einbinden.
Externe Konfiguration
In verteilten Architekturen ist es entscheidend, dass die Konfiguration der einzelnen Komponenten nicht fest im Anwendungscode verankert ist, sondern sich dynamisch und extern verwalten lässt – wie im 12-Faktor-Konzept beschrieben (siehe Faktor 3 im Kasten). Jede Komponente lässt sich dann, wie in Abbildung 7 dargestellt, zur Laufzeit konfigurieren, ohne dass eine erneute Kompilierung oder Bereitstellung erforderlich ist.
Entwicklerinnen und Entwickler profitieren dabei von erhöhter Portabilität und Flexibilität sowie einfacherer Wartbarkeit, da sich Änderungen an der Konfiguration ohne Unterbrechung des Betriebs vornehmen lassen. Die externe Konfiguration ermöglicht auch das Anpassen der Anwendung an unterschiedliche Umgebungen, wie Entwicklung, Staging und Produktion, ohne dass Änderungen am Code erforderlich sind. Zu den weiteren Voraussetzungen für das erfolgreiche Umsetzen einer verteilten Architektur zählen unter anderem auch verteiltes Monitoring, Logging und Tracing. Der Artikel beschränkt sich jedoch auch im Folgenden hauptsächlich auf Service Registry und Discovery sowie die externe Konfiguration.
Umsetzung mit Frameworks
Spring Cloud zählt im Java-Umfeld zu den verbreitetsten Frameworks, mit denen sich verteilte Anwendungen und Microservices umsetzen lassen. Es geht zurück auf Netflix OSS, eine Open-Source-Software-Sammlung, die aufgrund ihrer erfolgreichen Umsetzung des Streaming-Dienstleisters eine gewisse Berühmtheit erlangt hat. Das Framework stellt verschiedene Bibliotheken und Abhängigkeiten bereit, um unterschiedlichen Anwendungen individuelle Funktionen zu verleihen. Damit lässt sich ein Großteil der angesprochenen Aspekte verteilter Anwendungen abdecken.
Netflix Eureka heißt die Komponente für Service Registry und Discovery, die das dynamische Registrieren und Entdecken von Diensten ermöglicht. Ribbon ist der Dienst für clientseitiges Load Balancing: Mit dieser Komponente lassen sich Anfragen auf verschiedene Instanzen eines Dienstes verteilen, um eine gleichmäßige clientseitige Lastverteilung zu gewährleisten und die Ausfallsicherheit zu verbessern.
Fehlertoleranz und Resilienz sind Funktionen, die ursprünglich das Modul Hystrix beigesteuert hat, das sich aber seit geraumer Zeit im Wartungsmodus befindet. Alternativ bietet das neue Modul Circuit Breaker eine Abstraktion, die sich mit Implementierungen von Spring Retry und Resilience4J umsetzen lässt. Diese Mechanismen bieten Schutz vor Ausfällen durch das Isolieren fehlerhafter Dienste sowie das Ausführen von Fallback-Operationen, Retry-Implementierung und Rate Limiting.
Dem Konfigurationsmanagement dient die Komponente Spring Cloud Config. Sie verwaltet extern Konfigurationen verteilter Anwendungen, um hohe Flexibilität und Skalierbarkeit zu gewährleisten.
Mit dem Spring Cloud Gateway (API Gateway) lässt sich ein zentraler Einstiegspunkt für verschiedene Microservices bereitstellen, der die Komplexität der Kommunikation reduziert und die Sicherheit erhöht. Die Komponente ermöglicht granulare Netzwerkkontrolle und Funktionen wie zum Beispiel Canary Deployments und Rate Limiting.
Die Umsetzung ist für alle Komponenten weitgehend identisch: Jede benötigt zusätzliche Abhängigkeiten in der Projektverwaltung sowie externe Konfigurationsparameter und Annotationen im Code. Die Komplexität variiert je nach Komponente, aber der Spring-Cloud-Ansatz ermöglicht es, dass sich jede Komponente individuell für ihre Bedürfnisse und Rolle in der verteilten Anwendung konfigurieren lässt.
Am Beispiel des API-Gateways lässt sich die Vorgehensweise leicht nachvollziehen (siehe Listings 1 und 2). Die Code-Abhängigkeit wird via Maven oder Gradle deklariert, eine externe Konfigurationsdatei (application.yaml) spezifiziert das Verhalten.
Listing 1: Dependency in pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
Listing 2: Routing zu definierten Endpunkten auf Basis eingehender Requests
spring:
cloud:
gateway:
routes:
- id: my_route
uri: http://www.example.com
predicates:
- Path=/
Ein Vorteil des Framework-Ansatzes besteht darin, dass jede Komponente nur genau die Funktionen implementieren kann, die sie auch benötigt. In einer verteilten Anwendung treten in der Praxis nahezu alle Komponenten in irgendeiner Form mit der Service-Registry in Kontakt, allerdings benötigt nicht jede auch Load Balancing, Fehlertoleranz-Mechanismen oder die Funktionen eines API-Gateways, wie beispielsweise des Spring Cloud Gateways. Erzeugter Overhead bleibt im Spring-Cloud-Ansatz granular konfigurierbar und er vermeidet unnötige Abhängigkeiten.
Ein weiterer Vorteil besteht darin, dass die Architektur unabhängig von der darunterliegenden Plattform oder Runtime funktioniert. Es spielt keine Rolle, ob die Komponenten auf einem einzelnen Knoten oder verteilten Maschinen, Containern, virtuellen Maschinen (VMs) oder Bare-Metal-Instanzen laufen.
Ein Nachteil des Spring-Cloud-Ansatzes liegt jedoch darin, dass eine Veränderung an der Codebasis zwingend erforderlich ist. Das führt dazu, dass Entwicklerinnen und Entwickler sämtliche Komponenten neu bauen, packen und deployen müssen. Insbesondere bei umfangreichen Anwendungen sind damit Zeitaufwand und eine höhere Komplexität verbunden.
Ähnliche Funktionen wie Spring Cloud bietet SmallRye, eine Kollektion von APIs und Implementierungen, die hauptsächlich im Umfeld von Eclipse MicroProfile, Quarkus und Wildfly Verwendung findet. Die für Spring Cloud genannten Vor- und Nachteile gelten auch für diese Frameworks.
Umsetzung mit Kubernetes
Die Open-Source-Plattform Kubernetes, die zum skalierbaren Betrieb verteilter, containerisierter Applikationen ausgelegt ist, eignet sich per se fĂĽr das Umsetzen verteilter Anwendungen. Sie bietet von Haus aus integrierte Funktionen wie eine Service Registry, einfache Skalierung von Komponenten, dynamische Lastverteilung und Mechanismen fĂĽr Updates ohne Downtime. Im Detail bietet die Plattform Folgendes:
Service: Ein Service in Kubernetes stellt eine IP-Adresse und einen DNS-Namen für eine Gruppe von Pods bereit, die die gleiche Funktion ausführen (siehe Abbildung 8). Dadurch können andere Komponenten leicht auf die Dienste zugreifen, unabhängig davon, auf welchen physischen Knoten die Pods laufen.
Ein Deployment in Kubernetes beschreibt den gewünschten Zustand für eine Anwendung, einschließlich der Anzahl der replizierten Pods. Es ermöglicht einfaches Skalieren, Rollbacks und Updates der Anwendungen. ConfigMaps und Secrets dienen dazu, Konfigurationsdaten wie Umgebungsvariablen, Konfig-Dateien oder Befehlszeilenargumente für einen oder mehrere Pods bereitzustellen. Sie bieten eine zentrale Möglichkeit, Konfigurationsänderungen vorzunehmen, ohne den Pod neu erstellen oder ändern zu müssen (siehe Listing 3). Grundsätzlich sind sie sich in ihrer Struktur sehr ähnlich, ConfigMaps legen die Key-Value-Paare in lesbarem Text ab, während Secrets den Value-Anteil mit Base64 encodieren.
Listing 3: Beispiel einer ConfigMap fĂĽr eine Datenbankkonfiguration
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-config
data:
DB_NAME: my-postgres-db
DB_PORT: "5432"
Die Konfiguration einer Kubernetes-Umgebung erfolgt nach dem Client-Server-Prinzip, bei dem der Client Befehle durch die API des Kubernetes-Clusters ausführt. Die hauptsächlich verwendete Definitionssprache ist hierbei YAML, die sich auf Basis einer Spezifikation für die meisten Kubernetes-Objekte gleich verhält. Der Vorteil dieses Ansatzes besteht darin, dass die verteilten Komponenten der Anwendung sich komplett ohne jegliche Veränderung in den Kubernetes-Cluster integrieren lassen. Die einzige notwendige Anforderung besteht darin, die Anwendung in Container zu verpacken und in Kubernetes bereitzustellen. Da Kubernetes seine Dienste für alle Container im gleichen Stil anbietet, können die Komponenten unabhängig von Programmiersprachen und Frameworks miteinander interagieren.
Ein Nachteil liegt hingegen in der Abhängigkeit von der Plattform. Der Umgang mit Kubernetes ist nicht einfach zu erlernen und es bedarf einer Menge Erfahrung, um die Technik zu meistern. Im Vergleich mit den Software-Frameworks ist Kubernetes außerdem hinsichtlich der Resilienz limitiert. Die Plattform bietet keinerlei Mechanismen wie Circuit Breaker, Bulkhead oder Retries an, solche Funktionen muss die jeweilige Anwendung übernehmen. Darüber hinaus hat Kubernetes in der Standardkonfiguration Defizite bei der Netzwerkinteraktion. Einfaches Load Balancing steht zwar zur Verfügung, aber es gibt keine direkten Möglichkeiten, den Netzwerkverkehr zu kontrollieren oder zu überwachen.
Service-Meshes
Neben Kubernetes und den Software-Frameworks helfen Service-Meshes beim Umsetzen verteilter Anwendungen. Sie sind im eigentlichen Sinne keine weitere Methode, sondern dienen eher als Ergänzung zu Kubernetes, um dessen Netzwerk-Defizite zu beheben. Das Wort "Mesh" bedeutet übersetzt so viel wie "Geflecht" oder "Gewebe" – im Kontext verteilter Architekturen geht es im Detail darum, wie Services, insbesondere in einem Kubernetes-Cluster, miteinander verflochten sind und ineinandergreifen. Dabei übernehmen sie hauptsächlich folgende Aufgaben:
- Lastverteilung und Traffic-Routing: Während Kubernetes einfaches Load Balancing nach einem Round-Robin-Mechanismus umsetzt, bietet ein Service-Mesh granulare Kontrolle über den Netzwerkverkehr. So lässt sich beispielsweise die Last prozentual auf unterschiedliche Versionen einer Anwendung verteilen oder basierend auf Header-Informationen oder Cookies.
- Verschlüsselung und Sicherheit: Ein Service-Mesh ermöglicht die Implementierung spezifischer Netzwerkrichtlinien zwischen einzelnen Komponenten. Es verschlüsselt außerdem die Kommunikation einzelner Teilsegmente innerhalb eines Clusters.
- Fehlertoleranz: Mit einem Service-Mesh lassen sich Defizite bei der Resilienz beheben: Mechanismen wie Retries, Rate-Limiting, Circuit-Breaking und andere erlauben eine bessere Kontrolle des Netzwerks.
- Observability: Durch die Kontrolle ĂĽber den gesamten Netzwerkverkehr des Clusters bietet ein Service-Mesh umfassende Einsichten, Beobachtungen und Analyse.
Service-Meshes lassen sich auf zwei Arten implementieren: Traditionell über das Sidecar-Modell oder in einer neueren Variante mit Extended Berkeley Packet Filter (eBPF) ohne Sidecars. Beide Ansätze werden im Folgenden kurz vorgestellt.
Sidecar-basiertes Service-Mesh
Das Konzept des Sidecars (Beiwagen) in Kubernetes sieht vor, dass in der "Hülle" eines Pods ein oder mehrere Container laufen können, die sich sowohl eine Netzwerkkonfiguration als auch die IP-Adresse teilen. Genau diese Funktion macht sich das Sidecar-Modell zunutze, indem es zu jedem Applikations-Container einen zusätzlichen Proxy-Container injiziert, der den Netzwerkverkehr zum Application-Pod permanent überwacht und steuert.
Um Zugriff auf den gesamten Netzwerkverkehr zu bekommen, muss jede einzelne Komponente einen solchen Proxy erhalten. Die Proxy-Seite eines Service-Meshes heißt Data Plane. Im Sidecar-Modell senden sämtliche Data-Plane-Proxies ihre gesammelten Informationen zur Auswertung an eine zentrale Control Plane. Diese kann auf Basis der Auswertung dann korrespondierende Aktionen ausführen. Die Control Plane sammelt demnach nicht nur Infos, sondern steuert und konfiguriert die Proxies über eine externe Schnittstelle.
Der wesentliche Vorteil dabei ist, dass das Sidecar-basierte Service-Mesh komplett transparent für die Anwendungscontainer arbeitet. Sämtliche Funktionen sind in den separaten Proxy-Containern implementiert und Änderungen am Code der eigentlichen Anwendungen sind nicht erforderlich. Da der Anwendungscode in einem separaten Container läuft, ist dieser unabhängig von der für die Applikation verwendeten Programmiersprache.
Service-Mesh-Funktionen lassen sich durch einen simplen Neustart des jeweiligen Pods aktivieren oder deaktivieren. Diese Flexibilität erweist sich insbesondere in dynamischen Umgebungen als wertvoll, in denen sich Anforderungen und Anwendungen häufig ändern. Zusätzliche Services oder Komponenten lassen sich mühelos durch das Hinzufügen entsprechender Proxy-Container integrieren. Andererseits können Entwicklerinnen und Entwickler nicht mehr benötigte Service-Mesh-Konfigurationen leicht entfernen, um Ressourcen zu optimieren und Overhead zu minimieren.
Die Konfiguration der einzelnen Objekte erfolgt, wie beispielhaft in Listing 4 gezeigt, ĂĽber die Kubernetes-API mit Custom Resource Definitions (CRDs).
Listing 4: Virtual Service von Istio mit einer Lastverteilung 75/25 zwischen zwei Versionen eines Services
kind: VirtualService
metadata:
name: my-service
spec:
hosts:
- my-service.example.com
http:
- route:
- destination:
host: my-service-v1
weight: 75
- destination:
host: my-service-v2
weight: 25
Unnötigen Overhead vermeiden
Nachteil dieses Service-Meshes ist der zusätzliche Overhead der Proxy-Container. Jeder beansprucht CPU- und Speicherressourcen und trägt zu erhöhter Latenz durch zusätzliche Netzwerk-Hops zwischen den Diensten bei. Vereinfacht gesagt, wird zu jedem Container ein Container mit dem potenziellen Funktionsumfang eines API Gateways hinzugefügt. Der Overhead wächst mit der Zahl der Service-Mesh-Instanzen. Um diesen zu minimieren, sollten Entwicklerinnen und Entwickler auf eine sorgfältige Konfiguration der Service-Meshes achten. Sie sollten nur die benötigten Funktionen aktivieren und Proxies nur dort einsetzen, wo sie unverzichtbar sind.
Trotz der Overhead-Problematik liefert ein Sidecar-basiertes Service-Mesh entscheidende Vorteile in Bezug auf Netzwerkkontrolle, Sicherheit und Observability. Es ist dennoch wichtig, die Kosten und den Nutzen sorgfältig abzuwägen und die Implementierung den Anforderungen der Anwendung und des Clusters entsprechend anzupassen. Zu den bekanntesten Open-Source-Projekten mit Sidecar-basiertem Ansatz zählen Linkerd und Istio. Zu beiden hält die Cloud Native Computing Foundation (CNCF) Benchmark-Tests mit aussagekräftigen Ergebnissen parat.
"Sidecarless" – Service-Mesh ohne Sidecars
Eine Alternative zum Service-Mesh-Modell mit Sidecar ist die Sidecarless-Variante. Die Idee dahinter ist, Proxy-Funktionen nicht bei den Pods, sondern auf Ebene der Kubernetes-Knoten umzusetzen. Das Open-Source-Projekt Cilium verfolgt genau diesen Ansatz auf Basis des Extended Berkeley Packet Filter (eBPF), eine Erweiterung des Linux-Kernels, mit der sich benutzerdefinierte Code-Snippets auf Systemebene ausführen lassen, ohne den Kernel-Code zu ändern. Via eBPF lässt sich somit der Netzwerkverkehr auf Kernel-Ebene verarbeiten (mehr zum Einsatz von eBPF bietet der Artikel "Debugging in Produktion" auf Seite 98).
Proxy-Funktionen dieses Service-Meshes lassen sich im Vergleich zu dem Sidecar-Modell mit geringerem Overhead umsetzen: Anstatt je eines Proxys pro Pod genügt ein Proxy pro Kubernetes-Knoten. Darüber hinaus lässt es sich leicht in bestehende Kubernetes-Cluster integrieren, ohne Änderungen an den Anwendungscontainern vornehmen zu müssen. Da eBPF auf der Kernel-Ebene arbeitet, ist es unabhängig von den für die Anwendungen verwendeten Programmiersprachen und Frameworks – auch polyglotte Anwendungen stellen kein Hindernis dar.
Beim Funktionsumfang liegen beide Service-Mesh-Varianten auf Augenhöhe: Der Netzwerkverkehr lässt sich in Echtzeit überwachen, Sicherheitsregeln anwenden und die Lastverteilung steuern. Funktionen wie Load Balancing, Traffic-Routing und Resilienz sind auf granularer Ebene möglich. Wie in Listing 5 zeigt, erfolgt auch die Konfiguration der API-Objekte im Cluster auf ähnliche Weise.
Listing 5: Beispiel einer Cilium-Konfiguration, die fĂĽr einen Service X explizit nur http-GET-Aufrufe via Port 8090 erlaubt
kind: CiliumNetworkPolicy
metadata:
name: "lock-down-service-X"
spec:
description: "Allow only to GET / on service-x"
endpointSelector:
matchLabels:
app: service-x
ingress:
- toPorts:
- ports:
- port: "8090"
protocol: TCP
rules:
http:
- method: "GET"
path: "/"
Service-Meshes ohne Sidecars sind eine vielversprechende Alternative. Das Cilium-Projekt hat hier Pionierarbeit geleistet und neue Möglichkeiten aufgezeigt. Das Entwicklungsteam hinter Istio verfolgt seit der Ankündigung von "Ambient Mesh" 2022 einen ähnlichen Weg, jedoch nicht auf Basis von eBPF, sondern der Eigenentwicklung namens zero trust tunnel.
Fazit und Ausblick
Wer vor der Entscheidung steht, eine individuelle Implementierung für eine verteilte Architektur zu wählen, sollte sich Gedanken darüber machen, welchen Grad an Heterogenität die eigene Umgebung aufweist und welche Vorteile sich durch die verteilte Architektur ergeben.
Abhängig von dieser Entscheidung gibt es verschiedene Vorgehensweisen. Im Umfeld der Java Virtual Machine stehen Frameworks zur Auswahl, mit denen sich die gewünschten Funktionen individuell kombinieren lassen. Soll der Code der Anwendung nicht modifiziert werden oder handelt es sich um eine Anwendung aus vielen verschiedenen Frameworks und Programmiersprachen, ist es ratsam, die Funktionen nicht in die Komponenten einzubauen, sondern über eine Plattform zu beziehen.
Kubernetes hat sich dafür zum De-facto-Standard entwickelt und bietet alle grundlegenden Funktionen für eine verteilte Architektur. Wer in Bezug auf Netzwerkkontrolle, Verschlüsselung oder Resilienz mehr Fähigkeiten benötigt, muss die weitergehenden Funktionen dann punktuell in die einzelnen Komponenten einbauen oder den betreffenden Cluster um ein Service-Mesh erweitern. Das Sidecar-Service-Mesh bietet derzeit das Optimum an Kontrolle, Übersicht und Sicherheit. Angesichts des durch Sidecars verursachten Overhead verlangt ein solches Service-Mesh aber auch einen hohen Preis.
Die Entwicklungen rund um Service-Meshes ohne Sidecars zeigen unterdessen, dass der Bedarf an den damit zugänglichen Funktionen groß ist und es in der Entwicklung dieser Architektur noch mehr Spielraum für alternative Ansätze gibt. Die kontinuierliche Weiterentwicklung und Forschung sowohl auf den Gebieten der Frameworks als auch der Plattformen verspricht interessante Zukunftsaussichten für die Welt der verteilten Anwendungen.
(map)