Cross-Cutting Concerns durch Muster lösen

Gastbeitrag von | 30.11.2020

Die Idee immer wiederkehrende Probleme durch ähnliche Lösungsmuster anzugehen, ist wahrscheinlich so alt wie die Menschheit selbst. Sucht man bei Google nach “Lösungsmuster” findet man Beiträge aus Bereichen wie Controlling, Sozialwissenschaften und natürlich auch der Softwareentwicklung. Lösungsmuster benutzen wir als Modelle, um Lösungen für Probleme zu finden. Wir sehen ein Problem und überlegen, ob es nicht einem Problem ähnelt, das wir bereits gelöst haben. Daraus entstehen Modelle oder eben Muster, wie man bestimmte Probleme angehen kann.

In der objektorientierten Programmierung haben sich Anfang der 90er Jahre des letzten Jahrhunderts sogenannte Design Patterns oder auch Entwurfsmuster herausgebildet, die die Lösung immer wiederkehrender Probleme, wie z.B. die Entkopplung von unterschiedlichen Implementierungen, boten. In der Regel bezogen sich diese Probleme auf die Anordnung und Anwendung bestimmter Klassen zueinander und die Organisation ihrer Beziehungen1. Die Entwurfsmuster konnten immer wieder genutzt werden, da sie sich bereits in unterschiedlichen Zusammenhängen bewährt hatten. In der Folge stieg die Qualität der Implementierung wesentlich.

In der modernen Welt der Microservices und Cloud-Architekturen muss man nicht mehr einzelne Klassen organisieren. Aber die Organisation einzelner Services und ihrer Kommunikation untereinander ist ebenso komplex und bedarf entsprechender Musterlösungen. Die auftretenden Probleme gleichen sich oft und insofern liegt die Anwendung von Lösungsmustern nah. Um diese Lösungsmuster von den ursprünglichen Entwurfsmustern zu unterscheiden, bezeichnet man sie als Architekturmuster.

Solche Architekturmuster werden insbesondere dann interessant, wenn sie Querschnittsthemen – oder auch Cross-Cutting Concerns – betreffen. Querschnittsthemen betreffen Architekturen immer wieder und betreffen alle Dienste, die in einer Anwendung benötigt werden. Insbesondere nicht funktionale Anforderungen wie

  • Verringerung der Gesamtkomplexität,
  • Resilienz,
  • Sicherheit und
  • Beobachtbarkeit

sind solche Cross-Cutting Concerns, die durch Architekturmuster adressiert werden.

Verringerung der Komplexität

Komplexität zu beherrschen, ist eine immer wiederkehrende Aufgabe in der Softwareentwicklung. Insbesondere die Beherrschung von vielen Services in einer Microservice-Architektur bringt neue nicht zu unterschätzende Herausforderungen. Ein Muster diese Komplexität zu verringern ist der Beiwagen. Dabei wird wie bei einem Motorrad ein Beiwagen ein zusätzlicher Service an einen Docker-Container gehängt. Typische Anwendungen für ein solches Muster sind:

  • Webproxy,
  • Logging-Shipper und
  • Stammdaten-Clients.

Schauen wir uns zwei Beispiele für Beiwagen – in Englisch als Sidecar bezeichnet – an:

Beiwagenansatz mit Kubernetes

Kubernetes (K8s) ist ein Open-Source-System zur Automatisierung der Bereitstellung, Skalierung und Verwaltung von containerisierten Anwendungen2. In Kubernetes ist die Lösung recht einfach, da die Kubernetes-Pods ohnehin einen Abstraktionslayer für Container bereitstellen. Das heißt, man liefert die Container immer innerhalb eines Pods aus.

Beiwagen werden schon lange in Kubernetes benutzt – es bietet sich einfach dafür an. Allerdings ist es schwierig ohne weitere Hilfe, die Lebensdauer der einzelnen Container zu steuern. Daher bietet Kubernetes seit der Version 1.18 eine Identifikation eines Pods als Sidecar an. Werden Container so markiert, werden sie vor allen anderen gestartet und nach allen anderen gestoppt. In allem anderen verhalten sie sich wie normale Container3.

Beiwagen mit Kubernetes

Abbildung 1 Beiwagenmuster mit Kubernetes

Beispiel:

apiVersion: v1
kind: Pod
metadata:
  name: bookings-v1-b54bc7c9c-v42f6
  labels:
    app: demoapp
spec:ss
  containers:
  - name: bookings
    image: banzaicloud/allspark:0.1.1
    ...
  - name: istio-proxy
    image: docker.io/istio/proxyv2:1.4.3
    lifecycle:
      type: Sidecar
    ...

Abbildung 2 Beispiel für einen Beiwagen (Sidecar) unter Benutzung von Kubernetes

Beiwagen in Docker-Compose

Auch mit Docker-Compose lassen sich Sidecar-Strukturen gut abbilden. Compose ist hierbei ein Werkzeug, um Docker-Applikationen mit mehreren Containern zu definieren und ablaufen zu lassen. Zur Konfiguration kann ein YAML-File benutzt werden. Mit einfachen Kommandos kann man dann alle Services der Konfiguration erzeugen oder starten4 .

Beispiel:

services:
    reverseproxy:
        image: reverseproxy
        ports:
            - 8080:8080
            - 8081:8081
        restart: always
 
    nginx:
        depends_on:
            - reverseproxy
        image: nginx:alpine
        restart: always
 
    apache:
        depends_on:
            - reverseproxy
        image: httpd:alpine
        restart: always

Abbildung 3 Beispiel für einen Reverseproxy als Beiwagen in Docker-Compose5

Das Beiwagenmuster wird vor allem benutzt, um das operative Ergebnis einer laufenden Software zu stabilisieren und verlässlich zu betreiben. Durch vorbereitete Beiwagen-Container werden typische Aufgaben, wie Bereitstellung von Monitoring und Logging vereinfacht und standardisiert. Die gesamte operationale Komplexität wird bei Anwendung des Beiwagenmusters vereinfacht.

Resilienz

Resilienz ist aus dem Lateinischen resilere für zurückspringen, abprallen abgeleitet. Bei technischen Systemen verstehen wir darunter auch bei Teilausausfällen nicht komplett zu versagen. Ein Muster, das diese Eigenschaft nachhaltig unterstützt ist der Botschafter oder Ambassador6.

Liegen Services außerhalb der eigenen Domäne, hat man in der Regel keinen Einfluss auf die Verfügbarkeit oder Antwortwortzeiten der externen Services. Die Resilienz des konsumierenden Services wird durch die Entsendung eines “Botschafters” in die konsumierende Domäne erreicht.

Botschafter-Muster

Abbildung 4 Botschafter-Muster

Dieses Muster ist insbesondere für die Verteilung von Stammdaten interessant. Viele Services greifen auf einen zentralen Service mit zentralen Daten zu. Ein solcher “Single Point of Failure” kann schnell zu Ausfällen des Gesamtsystems führen.

Der Botschafter wird als “Beiwagen” an den konsumierenden Service gehängt (siehe Abbildung 1). Er puffert die Daten und stellt sie transparent der Fachanwendung zur Verfügung. Transparent bedeutet hier, dass die Fachanwendung auf den Botschafter zugreift, als ob sie direkt auf den Stammdatenservice zugreift. Die Schnittstellen sind gleich. Die Fachanwendung greift nur auf den Botschafter zu und die Daten können schnell und unabhängig von Stammdatenservice zur Verfügung gestellt werden. Um auch Änderungen schnell und konsistent zur Verfügung stellen zu können, müssen entsprechende Invalidierungsnachrichten vereinbart werden. Um noch unabhängiger zu werden, können die Daten auch über einen Eventbus verteilt werden.

Eventbusse unterscheiden sich maßgeblich von Nachrichtenbussen, da Erzeuger und Konsument von Nachrichten vollkommen unabhängig sind. Während ein Nachrichtenbus, Nachrichten an den Konsumenten weiterleitet, werden die Nachrichten beim Eventbus gespeichert. Die so gespeicherten Nachrichten können vom Konsumenten gelesen werden. Das heißt, der Konsument muss nicht verfügbar sein, wenn Nachrichten eintreffen, wie dies bei Nachrichtenbussen der Fall ist.

Botschafter-Muster mit Eventbus

Abbildung 5 Botschafter-Muster mit Eventbus

Obwohl ein solcher Eventbus wiederum wie ein Single-Point-of-Failure aussieht, wiegen die Vorteile die Nachteile auf. Erzeuger von Nachrichten müssen sich nicht darauf verlassen, dass Konsumenten verfügbar sind. Entsprechende “Retry-Mechanismen” entfallen. Allerdings werden die Vorteile durch ein zusätzliches Element in der Infrastruktur erkauft.

Sicherheit

Auch die Sicherheit bedarf einer neuen Bewertung in einer Microservice-Architektur. Da die Microservices in der Regel REST-Schnittstellen zur Verfügung stellen, muss verhindert werden, dass diese von Unbefugten angegriffen werden können. Hier kann das Muster eine API-Gateways helfen.

Prinzip eines API Gateways
Abbildung 6 Prinzip eines API Gateways

Das obige Bild zeigt das Prinzip eines API-Gateways. In großen Microservice-Architekturen werden die Anzahl und das Verhalten von APIs (Application Programming Interface) schnell unübersichtlich.

Die APIs, die durch Microservices zur Verfügung gestellt werden, sind oft feingranular – und nicht wirklich das, was ein Konsument benötigt. Der Konsument muss mit vielen Services interagieren, um die Informationen zu bekommen, die er braucht. Um z.B. die Detailbeschreibung für ein Produkt dem Benutzer zur Verfügung zu stellen, muss der Client Beschreibung, Foto und Preis von unterschiedlichen Services abholen, deren Adresse jeweils bekannt sein muss.7

Daher macht es Sinn, APIs von Microservices, die zu einer Domäne gehören, über einen Zugangsweg zusammenzufassen. Diese Zusammenfassung wird durch ein API Gateway zur Verfügung gestellt. Diese Zusammenfassung macht das Erstellen von Konsumenten und das Beobachten wesentlich einfacher, da nur noch ein Eintrittspunkt in die jeweilige Domäne bekannt sein muss. Über diese Gateways können dann Zugriffe auf die Domäne durch externe Services abgesichert und gesteuert werden. Zugriffe können beobachtet und gegebenenfalls schon hier z.B. wegen zu hoher Last abgewiesen werden. Das API-Gateway übernimmt zusätzlich auch die Funktion eines Torwächters, der nur Zugriffe zulässt, die erwartet werden und vorher konfiguriert wurden.

API Gateways werden von unterschiedlichen Cloudanbietern und Softwareherstellern angeboten. Als Beispiele seien hier Apigee (Google)8 und Mulesoft9 genannt. Dabei bieten diese Plattformen wesentlich mehr als das eigentliche API-Gateway, sondern umfassen ganze API-Management-Aufgaben inklusive die Veröffentlichung, das Testen und das Abonnieren von APIs.

Beobachtbarkeit

Das Beobachten von Microservices spielt eine herausragende Rolle, um Architekturen stabil und betriebsbereit zu halten. Um eine höhere Beobachtbarkeit zu erreichen, können entsprechende Services jedem Geschäftsservice im Geflecht der Microservices zugeordnet werden. Dies führt zum Muster des Dienstgeflechts oder auch Service-Mesh.

Ähnlich wie beim Beiwagenmuster wird auch beim Service-Mesh versucht, die bei Microservice-Architekturen auftretenden höheren Infrastruktur-Komplexitäten durch vorherige Definition von typischen Installationsmustern zu vereinfachen. Und ähnlich wie beim Beiwagen wird auch hier ein Bild benutzt, um die Funktionalität zu verdeutlichen. Mesh beschreibt das Geflecht von Service- zu Service-Kommunikationen in einer Microservice-Architektur.

Ein Service Mesh ist ein dedizierter Infrastruktur-Layer, um Service-zu-Service-Kommunikation zwischen Microservices zu unterstützen. Oft wird der Beiwagen-Proxy als typisches Muster benutzt.10 Er übernimmt dann die Aufgabe, den eigentlichen Service ansprechen zu können oder auch andere Services anzusprechen, und stellt dabei auch die Funktionalitäten zur Beobachtung zur Verfügung.

Prinzip eines Dienstegeflechts oder auch Service-Mesh
Abbildung 7 Prinzip eines Dienstegeflechts oder auch Service -Mesh

Wie schon beim Beiwagen bieten solche dedizierten Layer Vorteile hinsichtlich Beobachtbarkeit, sichere Verbindungen, oder auch automatisches Wiederholen von fehlerhaften Aufrufen.

Diese Cross-Cutting-Concerns müssen nicht mehr individuell in der Applikations-Ebene z.B. durch die Anwendung von Bibliotheken gelöst werden, sondern können innerhalb der Infrastruktur standardisiert für alle Microservices der jeweiligen Applikation gelöst werden.

Für Service-Meshes gibt es verschiedene Implementierungen. Der bekannteste ist wohl Istio11. Weitere Implementierungen sind Consul12 und Kuma13.

Zusammenfassung: Muster für Cross-Cutting Concerns

Im vorliegenden Beitrag werden vier Lösungsmuster für Cross-Cutting Concerns der Software-Architektur wie Verringerung der Komplexität, Resilienz, Sicherheit und Beobachtbarkeit vorgestellt.

Diese Muster bieten uns Lösungen für immer wiederkehrende Probleme. Diese Probleme können wir mit gleichen oder ähnlichen Mitteln lösen und können uns schon auf entsprechende Erfahrungen verlassen. Dabei adressieren die vier vorgestellten Muster nicht nur die angesprochenen Themen, sondern unterstützen multiple nicht-funktionale Anforderungen. Zu den vorgestellten Themen kommen Themen wie Performanz und Verfügbarkeit hinzu.

Verringerung der Komplexität14 Resilienz Verfügbarkeit Performance Security Beobachtbarkeit
Sidecar x x x
Service Mesh x x
API-Gateway x x x x
Botschafter x x x x

Tabelle 1 Übersicht über vorgestellten Muster und zugehörige Querschnittsthemen.

Die obige Tabelle zeigt, dass die anwendbaren Muster bestimmte zu erfüllende nicht-funktionale Anforderungen adressieren. Lösungsmuster können angewandt werden, um komplexe Fragestellungen in der Microservice-Welt besser zu strukturieren und zu vereinfachen.

 

Hinweise:

Interessieren Sie sich für weitere Erfahrungen aus der Praxis? Testen Sie unseren wöchentlichen Newsletter mit interessanten Beiträgen, Downloads, Empfehlungen und aktuellem Wissen.

[1] E. Gamma, R. Helm et al.; 1994: Design Patterns. Elements of Reuseable Object-Oriented Software, Prentice Hall
[2] Siehe https://kubernetes.io/de/, abgerufen am 09.11.2020
[3] Sereg, Marton: Sidecar container lifecycle changes in Kubernetes 1.18, 404.02.2020
[4] NN: Overview Docker Compose, https://docs.docker.com/compose/, abgerufen 09.11.2020
[5] Hong, K.: Docker Compose: NGINX Reverse Proxy with multiple containers, https://www.bogotobogo.com/DevOps/Docker/Docker-Compose-Nginx-Reverse-Proxy-Multiple-Containers.php, abgerufen 17.05.2020
[6] NN: Botschaftermuster, https://learn.microsoft.com/de-de/azure/architecture/patterns/ambassador, 23.06.2017, abgerufen 08.11.2020
[7] Vergleiche Richardson, C.: Pattern: API Gateway/ Backend for Frontend, https://microservices.io/patterns/apigateway.html, abgerufen 09.11.2020
[8]  https://apigee.com/about/cp/api-management-platform, abgerufen 09.11.2020
[9] https://www.mulesoft.com/, abgerufen 09.11.2020
[10] NN: Service mesh, https://en.wikipedia.org/wiki/Service_mesh, abgerufen 09.11.2020
[11] https://istio.io/, abgerufen 09.11.2020
[12] https://www.consul.io/, abgerufen 09.11.2020
[13] https://kuma.io/, abgerufen 09.11.2020
[14] Gemeint ist hier die Komplexität für den konsumierenden Service – und nicht die Gesamtkomplexität des Netzwerks

Dr. Annegret Junker hat im t2informatik Blog weitere Beiträge veröffentlicht:

t2informatik Blog: Sinnvolle Grenzen für selbstverantwortliche Teams

Sinnvolle Grenzen für selbstverantwortliche Teams

t2informatik Blog: Warum ich als Architektin nicht überflüssig bin

Warum ich als Architektin nicht überflüssig bin

t2informatik Blog: Vom Monolithen zu Microservices

Vom Monolithen zu Microservices

Dr. Annegret Junker
Dr. Annegret Junker

Dr. Annegret Junker arbeitet als Chief Software Architect bei der codecentric AG. Seit über 30 Jahren ist sie in der Software-Industrie in verschiedensten Rollen und unterschiedlichen Domänen wie Automotive, Versicherungen und Finanzdienstleistungen tätig. Besonders interessiert sie sich für DDD, Microservices und alles, was damit zusammenhängt.