Über unsMediaKontaktImpressum
Dr. Annegret Junker 30. Juni 2020

Lösungsmuster für Cross-Cutting Concerns in Microservices

In diesem Artikel möchte ich erläutern, was man unter "Cross-Cutting Concerns" versteht und wie man die entsprechende Fragestellung bei Microservices unter der Verwendung von Lösungsmustern angehen kann. Der Artikel geht hierbei auf vier verschiedene Muster ein und stellt vor, wie man sich spezifischen Herausforderungen hinsichtlich nicht-funktionaler Anforderungen nähern kann.

Cross-Cutting Concerns

Unter Cross-Cutting Concerns oder auch "übergreifenden Bedenken" versteht man Probleme und Herausforderungen in Software-Systemen, die mehrere Services oder mehrere Teile der IT-Landschaft betreffen. Daher lassen sie sich nicht einfach mit dem "Separation-of-Concerns"-Ansatz lösen (Teile und Herrsche). Sie müssen in einer verteilten IT-Landschaft übergreifend gelöst werden. Dabei steht eine solche übergreifende Lösung der Gedankenwelt von Microservice-Architekturen fundamental entgegen. In diesem Artikel wird versucht, diesen Widerspruch durch Beispiele ein Stück weit aufzulösen.

Lösungsmuster

Laut Duden ist eine Lösung das Bewältigen einer schweren Aufgabe, während ein Muster eine Vorlage oder eine Zeichnung ist, nach der etwas hergestellt oder gemacht wird. Das heißt, ein Lösungsmuster ist eine Vorlage, wie eine schwierige Aufgabe gelöst werden kann.

Solche Muster wurden zuerst in der klassischen Softwarearchitektur als Designpattern angewendet. Sie lösten typische Probleme in der objektorientierten Programmierung. Z. B. wie die Entkopplung von unterschiedlichen Implementierungen für ein Problem über eine Bridge oder die Abbildung eines Datenbaumes über ein Composit [1].

Schnell wurde aber auch klar, dass sich ähnliche Prinzipien auch in der Architektur durchsetzen können. Das heißt, man kann auch Lösungsmuster für Enterprise-Architekturen finden [2].

In modernen Microservice- und Cloud-Architekturen sehen wir uns teilweise vollkommen neuen Herausforderungen gegenüber – plötzlich steckt die Komplexität nicht mehr in der Implementierung der Geschäftslogik, sondern in der Kommunikation der Services untereinander. Teilweise können sich diese Probleme mit den herkömmlichen Mustern lösen lassen, teilweise brauchen wir neue Lösungsstrategien. In diesem Artikel werden vier verschiedene Lösungsmuster vorgestellt, die typische Probleme in einer Microservice-Architektur lösen. Teilweise erinnern sie uns an bekannte Lösungen, teilweise sehen sie vollkommen neu aus.

Beiwagen

Beim Aufsetzen von Microservice-Architekturen steht man oft vor der Aufgabe, dass man kleinere Services wie ein Webservice zur SSL-Terminierung immer mit dem eigentlichen Service deployen muss. Hat man zwei Container, die immer gemeinsam deployed werden, braucht man eine verlässliche automatische Lösung für dieses Problem.

Das entsprechende Lösungsmuster heißt "Sidecar". Wie ein Beiwagen beim Motorrad fährt der kleine Service beim großen Bruder immer mit. Typische Beispiele sind

  • Webproxy (wahrscheinlich am besten bekannt)
  • Logging-Shipper
  • Stammdaten-Clients

Sidecar-Ansatz mit Kubernetes

Kubernetes (K8s) ist ein Open-Source-System zur Automatisierung der Bereitstellung, Skalierung und Verwaltung von containerisierten Anwendungen [3].

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  – z. B. einen Logging-Shipper aus.

Sidecars werden schon lange in Kubernetes benutzt – es bietet sich einfach dafür an. Allerdings ist es ohne weitere Hilfe schwierig, 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 Container [4].

Beispiel für einen K8s-Pod mit einem Container, der als Sidecar markiert ist [4]

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

Sidecar mit 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 Dienste der Konfiguration erzeugen oder starten [5].

Beispiel für einen Reverseproxy als Sidecar in Docker-Compose [6]

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

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

Service Mesh

Ähnlich wie beim Sidecar-Pattern 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 Sidecar 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 Sidecar-Proxy als typisches Muster benutzt [7]. Der Sidecar übernimmt dann die Aufgabe, den eigentlichen Service ansprechen zu können oder auch andere Services anzusprechen.

Wie schon beim Sidecar bieten solche dedizierten Layer Vorteile hinsichtlich Beobachtbarkeit, sicherer 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 Istio [8]. Weitere Implementierungen sind Consul [9] und Kuma [10].

API Gateway

Abb. 3 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 [11].

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 werden und gegebenenfalls schon hier z. B. wegen zu hoher Last abgewiesen werden.

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

Botschafter

Das Botschafter- oder auch Ambassador-Muster schafft Verlässlichkeit beim Zugriff auf Remote-Services [14]. Liegen Dienste außerhalb der eigenen Domäne, hat man in der Regel keinen Einfluss auf die Verfügbarkeit oder auch Antwortgeschwindigkeit des externen Dienstes.

Die Resilienz (Widerstandsfähigkeit) des konsumierenden Services wird durch die Entsendung eines "Botschafters" in die konsumierende Domäne erreicht. Dieses Muster ist besonders bei der Verteilung von Stammdaten empfehlenswert, da viele Services auf der einen Seite diese Daten benötigen und auf der anderen Seite der Stammdatenservice als "Single Point of Failure" zu Ausfällen des Gesamtsystems führen kann.

Der Botschafter wird als Beiwagen an die Fachanwendung gehängt. Der Beiwagen puffert die Daten und stellt sie transparent der Fachanwendung zur Verfügung. Transparent bedeutet hier, dass die Fachanwendung auf den Botschafter zugreifen kann, als wäre es der Stammdatenservice selbst. Nur der Beiwagen oder auch Botschafter kommuniziert mit der Stammdatenanwendung, die Fachanwendung kommuniziert nur mit dem Beiwagen. Dadurch können Stammdaten sehr schnell und unabhängig von der Stammdatenverwaltung selbst zur Verfügung gestellt werden.

Um Stammdatenänderungen schnell und zuverlässig den Fachanwendungen zur Verfügung zu stellen, müssen entsprechende Invalidierungsnachrichten vereinbart werden. Um hier noch unabhängiger zu werden, können diese auch über eine Event-Bus verteilt werden. Der Beiwagen enthält dann den entsprechenden Konsumenten für den Event-Bus.

Event-Busse unterscheiden sich maßgeblich von Nachrichten-Bussen, da Konsument und Produzent von Nachrichten vollkommen unabhängig sind. Während bei Nachrichten-Bussen die Nachrichten von Produzenten empfangen und an Konsumenten weitergeleitet werden, werden bei Event-Bussen die Events gespeichert und können von allen Interessierten gelesen werden (entsprechende Rechte vorausgesetzt). Obwohl ein solcher Event-Bus wiederum wie ein Single-Point-of-Failure aussieht, bietet er doch wesentliche Vorteile. Produzenten müssen sich nicht darauf verlassen, dass Konsumenten verfügbar sind. Konsumenten müssen sich nicht darauf verlassen, dass der Produzent von Events verfügbar ist. Der Bus muss die Nachrichten nicht weiterleiten, daher muss er nicht "Retry-Mechanismen" implementieren, stattdessen greift der Konsument auf die Events zu, wenn er sie braucht. Dabei stellt der Event-Bus "Lesezeichen" zur Verfügung, damit der Konsument wieder da einsetzen kann, wo er beim letzten Lesen aufgehört hat.

Diese vielen Vorteile werden mit einem zusätzlichen Element in der Infrastruktur erkauft. Dieses muss in jedem Fall hochverfügbar sein, da die Verfügbarkeitsanforderungen sowohl vom Produzenten als auch vom Konsumenten sichergestellt werden müssen.

Die bekannteste Event-Bus-Implementierung ist sicher Apache Kafka [15]. Aber auch Amazon Kinesis [16] oder Microsoft Azure Event Hubs [17] bieten ähnliche Eigenschaften.

Dr. Annegret Junker bei den IT-Tagen 2020

Zum einem ähnlichen Thema hält Dr. Annegret Junker einen Vortrag auf den diesjährigen IT-Tagen – der Jahreskonferenz der Informatik Aktuell.

Mono-, Modu- oder Micro- – welche Lithen passen für mich?

Zusammenfassung

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.

Tabelle 1: Übersicht über vorgestellte Muster und zugehörige Cross-Cutting Concerns

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

Die obige Tabelle zeigt, dass die anwendbaren Muster bestimmte zu erfüllende nicht-funktionale Anforderungen adressieren. Die vorgestellten Muster betreffen Cross-Cutting-Concerns wie Komplexität, Resilienz, Verfügbarkeit, Performance, Security und Beobachtbarkeit. Mit "Verringerung der Komplexität" ist hier die Komplexität für den konsumierenden Service gemeint – und nicht die Gesamtkomplexität des Netzwerks

Lösungsmuster können angewandt werden, um komplexe Fragestellungen in der Microservice-Welt besser zu strukturieren und zu vereinfachen.

Quellen
  1. E. Gamma, R. Helm et al.; 1994: Design Patterns. Elements of Reuseable Object-Oriented Software, Prentice Hall
  2. M. Fowler; 2002: Patterns of Enterprise Application Architecture, Addison Wesley
  3. Kubernetes
  4. M. Sereg: Sidecar container lifecycle changes in Kubernetes 1.18
  5. Overview Docker Compose
  6. K. Hong; 2020: Docker Compose: NGINX Reverse Proxy with multiple containers
  7. Wikipedia: Service Mesh
  8. Istio
  9. Consul
  10. Kuma
  11. C. Richardson: Pattern: API Gateway/ Backend for Frontend
  12. Apigee
  13. Mulesoft
  14. Cloudübergreifende API-Verwaltungsplattform
  15. Apache Kafka
  16. Kinesis
  17. Event-Hubs

Autorin
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben