Selbstheilende Anwendungen und ihre Geheimnisse Resiliente Cloud-Native-Architekturen realisieren

Von Filipe Martins & Anna Kobylinska 11 min Lesedauer

Angesichts unzuverlässiger und hochskalierbarer Cloud-Infrastrukturen ist der Software-Betrieb mit Kubernetes im Selbstfahrmodus unterm Strich fast immer zu teuer. Selbstheilende Anwendungsarchitekturen verbessern die Resilienz, ohne dabei den Geldbeutel zu sprengen.

Ablauf des typischen CrashLoopBackOff-Zustands in Kubernetes.
Ablauf des typischen CrashLoopBackOff-Zustands in Kubernetes.
(Bild: Groundcover)

Für die Gewährleistung kontinuierlicher Dienstbereitschaft sind selbstheilende Architekturen genauso unverzichtbar wie für die zuverlässige Finanzkontrolle ihres Langzeitbetriebs. Eine Studie des CISQ-Konsortiums (engl.: Consortium for Information & Software Quality) beziffert die betrieblichen Kosten von Softwareausfällen im Jahr 2022 allein in den Vereinigten Staaten auf eine astronomische Summe von rund 1,56 Billionen US-Dollar (knapp 1,46 Billionen Euro) oder das 25-fache der gesamten Steuereinnahmen des U.S.-Bundesstaates Texas. Zum Vergleich: Die Summe entspricht rund 144 Prozent der gesamten jährlichen Steuereinnahmen der Bundesrepublik Deutschland.

Wie können Software-Fehler so teuer sein? Ganz einfach: Die physische Realität ist zunehmend softwaregesteuert. Fällt eine kritische Softwarekomponente etwa in der Industrie aus, kommen womöglich ganze Fertigungslinien zum Stilstand. Oder noch schlimmer: Sie produzieren Mangelware und verschwenden Ressourcen, wenn man die Produktion nicht mal eben anhalten kann. Der Fehler propagiert sich dann durch die nachgelagerte Lieferkette oder manifestiert sich in Versorgungsengpässen. Auch andere Branchen sind gegen Softwarestörungen nicht wirklich immun.

„Selbstfliegende“ Software?

Betriebsprobleme mit einer Softwarebereitstellung können vielfältige Gründe haben – von Qualitätsproblemen über Leistungsengpässe (Stichwort: unerwartete Bedarfsspitzen) bis hin zu Hardware-Pannen und Ausfällen von externen Diensten oder Infrastrukturen.

Eine Software, die ihre Komponenten automatisch zurücksetzt oder sich selbst (möglicherweise in Endlosschleife) neu startet, um einen kritischen Fehler zu „handhaben“, ist nicht gerade der Idealfall. Weder maßlose Überprovisionierung noch eine Failover-Replika sind selbstheilenden Anwendungen ebenbürtig.

Das haben unter anderem Flugpassagiere im vergangenen Jahr auf die harte Tour gelernt, als Ende August 2023 in der Flugkontrollsoftware der nationalen Behörde Großbritanniens (NATS) ein altbekannter Bug auftauchte. Infolge einer fehlgeratenen Dateneinspeisung verlor das System mal eben die Fähigkeit, Flugpläne zu verarbeiten. Der Flugverkehr kam zum Stillstand.

Der Auslöser hört sich fast schon trivial an: Ein einzelner Flugplan enthielt zwei identisch benannte, aber separate Wegpunkte außerhalb des britischen Luftraums – darauf hatte die britische Behörde keinen Einfluss. Die Software hat die Daten ungeprüft übernommen. Die Einspeisung habe zu einer kritischen Ausnahme geführt. Diese setzte zuerst das primäre System und dann auch noch die Replika außer Betrieb.

Der Bug war seit Jahren bekannt, nur halt nicht behoben worden. Einem Totalausfall des Flugverkehrs hatte man eine geringe Wahrscheinlichkeit eingeräumt – eins zu fünfzehn Millionen, hieß es –, sodass es offenbar nicht lohnend schien, die Anwendung resilienter zu gestalten. Jemand hat sich da verkalkuliert. Jeder vierte Flug musste storniert werden. Eine Viertelmillion Passagiere waren plötzlich gestrandet. Nicht eine einzige Fluglinie kam ungeschoren davon. Das wäre vermeidbar wohl gewesen.

Die Qual der Wahl: Automatisierung im offenen Loop-System wartet auf menschliche Intervention; ein geschlossenes Loop-System handelt autark anhand von Rückmeldedaten.
Die Qual der Wahl: Automatisierung im offenen Loop-System wartet auf menschliche Intervention; ein geschlossenes Loop-System handelt autark anhand von Rückmeldedaten.
(Bild: Red Hat)

Wenn kritische Teile des Funktionsumfangs einer Anwendung ausfallen, mag es zweitrangig sein, ob sie als ein Monolith oder in einer Microservices-Architektur implementiert wurde. Jede dritte Kubernetes-Bereitstellung ist im Übrigen laut Intel „unnötig überprovisioniert“. Kubernetes macht es nicht zwangsweise besser. Es sei denn …

Selbstheilende Resilienz verteilter Anwendungen

Bugs sind ähnlich unvermeidlich wie Ausfälle der Konnektivität oder der Energieversorgung. Es kommt darauf an, dass man sie angemessen handhaben kann. Eine verteilte Softwarebereitstellung kann sich selbst überwachen, konkret: Metriken wie Latenz, Durchsatz, Fehlerrate und Ressourcennutzung extern beobachten. So lassen sich auftretende Probleme zeitnah diagnostizieren (Stichwort: Anomalienerkennung) oder sogar vorwegnehmen und entsprechende Korrekturmaßnahmen automatisch einleiten.

Selbstadaptive Softwarearchitekturen können sich dynamisch an neue Betriebsbedingungen und Anforderungen anpassen, indem sie beispielsweise ihre Konfigurationseinstellungen selbsttätig verändern. Man muss nicht erst warten, bis es im großen Stil schiefgeht.

Minimalismus: Mit einem ReplicaSet-Controller in Kubernetes ist es möglich, eine Reihe identischer Pods zu definieren, um grundlegende Selbstheilung zu ermöglichen.
Minimalismus: Mit einem ReplicaSet-Controller in Kubernetes ist es möglich, eine Reihe identischer Pods zu definieren, um grundlegende Selbstheilung zu ermöglichen.
(Bild: Dev.to)

In Microservice-Architekturen lassen sich die benötigten Selbstheilungsfähigkeiten durch den Einsatz spezialisierter Frameworks und Tools nachrüsten. Diese Lösungen können Probleme wie Leistungsverschlechterung, Ausfälle und Sicherheitslücken erkennen und automatisch korrigierende Sofortmaßnahmen einleiten.

In diese Kategorie fallen Monitoring-Tools wie Prometheus, Grafana und Elastic Stack mit ihren Fähigkeiten zur Überwachung von Systemmetriken und Protokollen. Incident-Response-Werkzeuge wie PagerDuty oder OpsGenie erlauben dann die automatisierte Reaktion auf zuvor erkannte Vorfälle. Eine Beispielimplementierung von Prometheus und Alert Manager könnte etwa wie folgt aussehen:

Jetzt Newsletter abonnieren

Täglich die wichtigsten Infos zu Softwareentwicklung und DevOps

Mit Klick auf „Newsletter abonnieren“ erkläre ich mich mit der Verarbeitung und Nutzung meiner Daten gemäß Einwilligungserklärung (bitte aufklappen für Details) einverstanden und akzeptiere die Nutzungsbedingungen. Weitere Informationen finde ich in unserer Datenschutzerklärung.

Aufklappen für Details zu Ihrer Einwilligung
# Alerting-Regeln in Prometheus
alert: HighErrorRate
expr: rate(http_requests_total{status="500"}[5m]) > 1
for: 10m
labels:
  severity: critical
annotations:
  summary: High request error rate detected on {{ $labels.instance }}
  description: "{{ $labels.instance }} has a 5xx error rate higher than 1 per second."
# Konfiguration des Alert Managers für das
# automatische Neustarten oder die automatische
# Skalierung des betreffenden Dienstes
receivers:
- name: 'webhook_receiver'
  webhook_configs:
  - url: 'http://autoscaler-service/api/scale'
    send_resolved: true

Über die Selbstheilungsfähigkeiten des Systems entscheidet in diesem Fall die Konfiguration des Alert Managers von Prometheus.

Self Healing unter Kubernetes? Theorie und Praxis

Die vier wichtigsten Kubernetes-Features für eine erhöhte Resilienz: Self-Healing, Automatische Rollbacks, Auto-Scaling und Load-Balancing.
Die vier wichtigsten Kubernetes-Features für eine erhöhte Resilienz: Self-Healing, Automatische Rollbacks, Auto-Scaling und Load-Balancing.
(Bild: Balvinder Singh)

Selbstheilende Kubernetes-Anwendungen stützen sich auf eine Reihe von integrierten Funktionen des Orchestrierers wie automatische Skalierung, Selbstreparatur, Rollback oder Lastenausgleich – und ggf. auf erweiterte Fähigkeiten der umliegenden Plattform. Kubernetes stellt Microservices als Netzwerkdienste bereit und übernimmt automatisch die Lastenverteilung der Kommunikation. Eine manuelle Konfiguration der Konnektivität zwischen Pods oder Containern ist weder erforderlich noch erwünscht. Das allein spart eine Menge Ingenieurzeit.

Um regelmäßig überprüfen zu können, ob die Anwendung wie erwartet funktioniert, macht sich der Orchestrierer zwei sogenannte Probes zu Nutze: Liveness und Readiness:

  • Die Liveness-Probe untersucht, ob ein Pod oder Container seine Aufgaben ordnungsgemäß verrichtet; bei Bedarf kann Kubernetes ggf. einen Neustart der betreffenden Objekte auslösen.
  • Die Readiness-Probe gibt an, ob ein Pod bereit ist, eingehenden Datenverkehr zu akzeptieren (dies trifft nur dann ein, wenn alle Container des Pods dienstbereit sind). Ist ein Pod nicht einsatzbereit, fliegt er aus den Service-Load-Balancern heraus.

Kubernetes unterstützt drei verschiedene Implementierungen beider Probes, und zwar:

  • exec: Diese Art von Probe führt Bash-Anweisungen in einem Container aus; sie kann zum Beispiel prüfen, ob eine bestimmte Datei existiert. Gibt die Bash-Anweisung einen Fehlercode zurück, schlägt die Probe fehl.
  • tcpSocket: Diese Probe versucht, eine TCP-Verbindung zum Container unter Verwendung des angegebenen Ports herzustellen; ist keine Verbindung möglich, schlägt die Probe fehl.
  • httpGet: Diese Probe sendet eine HTTP-GET-Anfrage an den im Container ausgeführten Server, der auf dem angegebenen Port lauscht. Ein HTTP-Statuscode größer oder gleich 200 und kleiner als 400 deutet auf Erfolg hin.

Ist ein Node nicht mehr erreichbar, geht Kubernetes automatisch hin und versucht, alle Workloads, die darauf ausgeführt wurden, auf andere gesunde Nodes zu verschieben. Konnte ein Kubernetes-Container aus irgendeinem Grund nicht ordnungsgemäß gestartet werden und blieb er stattdessen in der Neustartschleife feststecken (mit der Abfolge von Zuständen „Warten/Ausführen/Beendet”), wechselt der Pod in den CrashLoopBackOff-Zustand. Der BackOff-Algorithmus verzögert die Neustartversuche, um dem System oder dem Netzwerk etwas Zeit zu verschaffen und eine Überlastung zu verhindern.

Die Wartezeit zwischen den Neustarts erhöht sich exponentiell (10 Sekunden, 20 Sekunden, 40 Sekunden) bis zum Maximum von fünf Minuten. Dieser exponentielle BackOff verhindert übermäßige Anfragen und gibt der Anwendung Zeit, sich zu erholen. Gelingt dies nicht, ist ggf. Nachbessern der Anwendung angesagt.

Ablauf des typischen CrashLoopBackOff-Zustands in Kubernetes.
Ablauf des typischen CrashLoopBackOff-Zustands in Kubernetes.
(Bild: Groundcover)

Dank der integrierten Funktion für rollende Updates in Kubernetes können Entwickler/innen die erforderlichen Aktualisierungen verteilter Anwendungen schrittweise durchführen, indem man ältere Versionen von Pods durch neuere ersetzt, bis alle auf dem neuesten Stand sind. Statt alle Instanzen der alten Version gleichzeitig durch die neue zu ersetzen (Stichwort: Downtime!), verschieben rollende Updates den Datenverkehr schrittweise von der alten zur neuen Version, um einen möglichst reibungslosen Übergang zu gewährleisten.

Rollbacks, also Prozeduren zur Wiederherstellung einer früheren Version der Anwendung, erfolgen ebenfalls orchestriert. Dies ermöglicht nahtlose Code-Aktualisierungen, ohne Ausfallzeiten zu verursachen.

Overlay-Storage für selbstheilende Stateful-Anwendungen auf Kubernetes.
Overlay-Storage für selbstheilende Stateful-Anwendungen auf Kubernetes.
(Bild: Groundcover)

Selbstheilung von Anwendungen in Kubernetes hat jedoch ihre Grenzen. Probleme größeren Kalibers wie die Dateikorruption in Container-Images oder den Ausfall aller Steuerknoten eines Clusters kann Kubernetes im Alleingang nicht adressieren.

Die Multi-Cloud-Orchestrierung von Anwendungen wirft natürlich noch weitere Herausforderungen auf. Einige Kubernetes-Distributionen oder -Dienste, wie EKS und AKS, funktionieren nicht mit den Clouds anderer Anbieter. Die Verwaltung von Kubernetes-Clustern in mehreren Clouds mit einer einzigen Steuerebene erfordert dann doch eine spezielle Netzwerkkonfiguration und dann ist es auch mit der reibungslosen Selbstheilung meist nicht so weit her.

Die Fähigkeiten von Kubernetes-Anwendungen lassen sich mit zusätzlichen Frameworks wie Spring Boot oder Quarkus um fortgeschrittene Fähigkeiten der Selbstheilung erweitern.

Spring Boot Actuator mit Kubernetes

„Selbstfahrende“ Telekommunikationsdienste: Ein Closed-Loop-Orchestrierer übernimmt hier die ganzheitliche Kontrolle über alle Aspekte der Infrastruktur und Bereitstellung, einschließlich der Ausführung von CNF-Mobilfunkdiensten im Rundum-glücklich-Paket der Selbstheilung.
„Selbstfahrende“ Telekommunikationsdienste: Ein Closed-Loop-Orchestrierer übernimmt hier die ganzheitliche Kontrolle über alle Aspekte der Infrastruktur und Bereitstellung, einschließlich der Ausführung von CNF-Mobilfunkdiensten im Rundum-glücklich-Paket der Selbstheilung.
(Bild: Red Hat)

Mit Spring Boot Actuator, einem Feature von Spring Boot, können Dev-Teams während der Entwicklung und nach der Bereitstellung ihrer Anwendungen ausgefuchste Überwachungsfunktionen implementieren, die deutlich über reines Kubernetes hinausgehen. Er ermöglicht die Überwachung operativer Metriken einer Anwendung über HTTP-Endpunkte oder JMX-Beans. Kubernetes kann dann diese Endpunkte nutzen, um die Bereitschaft (Readiness) und Lebensfähigkeit (Liveness) der Anwendungen zu prüfen. Zum Beispiel:

// Maven-Abhängigkeit für Spring Boot Actuator
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
// application.properties: Konfiguration
management.endpoints.web.exposure.include=health,info

Die Konfiguration der Kubernetes-Proben könnte dann zum Beispiel so aussehen:

apiVersion: v1
kind: Pod
metadata:
  name: my-spring-app
spec:
  containers:
  - name: my-spring-app
    image: my-spring-app:latest
    ports:
    - containerPort: 8080
    livenessProbe:
      httpGet:
        path: /actuator/health/liveness
        port: 8080
      initialDelaySeconds: 10
      periodSeconds: 5
    readinessProbe:
      httpGet:
        path: /actuator/health/readiness
        port: 8080
      initialDelaySeconds: 15
      periodSeconds: 5

Auf Basis von Metriken, die Spring Boot Actuator bereitstellt, ist es dann möglich, Kubernetes-Features wie die automatische Skalierung mit dem HPA (Horizontal Pod Autoscaler) zu steuern:

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: dev-insider-spring-app-scaler
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: dev-insider-spring-app
  minReplicas: 1
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70

Die Skalierbarkeit von Kubernetes kennt natürlich ihre Grenzen. Ein Cluster kann beispielsweise nicht mehr als 5.000 Knoten haben und man ist auf 300.000 Container begrenzt. Aber es ist eine sichere Wette, dass mindestens 99,5 Prozent der Kubernetes-Anwendungsfälle innerhalb dieser Ska­lier­bar­keits­beschrän­kungen funktionieren sollten.

Bis ins kleinste Detail: Diese ereignisgetriebene Architektur stützt sich für die selbstheilenden Fähigkeiten einer OpenShift-basierten Kubernetes-Bereitstellung auf die KI-getriebene Observability-Plattform Dynatrace.
Bis ins kleinste Detail: Diese ereignisgetriebene Architektur stützt sich für die selbstheilenden Fähigkeiten einer OpenShift-basierten Kubernetes-Bereitstellung auf die KI-getriebene Observability-Plattform Dynatrace.
(Bild: Red Hat)

Mit Kubernetes ConfigMaps und Secrets lassen sich dann auch die Konfigurationseinstellungen der Spring Boot-Anwendung dynamisch verwalten. Actuator kann diese Konfigurationen überwachen und auf Änderungen ereignisgetrieben reagieren.

Wenn bestehende Frameworks zu kurz greifen …

Wer darauf angewiesen ist, Selbstheilungsmechanismen direkt im Code der eigenen Anwendung zu implementieren, steht vor einer verzwickten Aufgabe. Kurz auf den Punkt gebracht:

  • Mechanismen zur Fehlerbehandlung und retry-Wiederholung gehören direkt in den Anwendungscode,
  • Health Checks und Selbstwiederherstellung sollten skriptgesteuert ablaufen,
  • Circuit-Breaker-Muster helfen, Fehlerkaskaden zu verhindern.

Eine einfache Implementierung in Python könnte zum Beispiel wie folgt aussehen:

import requests
from retrying import retry
@retry(stop_max_attempt_number=3, wait_fixed=2000)
def fetch_data():
    response = requests.get("https://dev-insider.de")
    response.raise_for_status()
    # Gibt bei fehlerhaften Antworten einen HTTPError aus
    return response.json()
try:
    data = fetch_data()
except requests.exceptions.HTTPError as e:
    recover_system(e)

Moderne selbstheilende Microservice-Anwendungen machen sich noch weitere Ansätze zu Nutze, darunter auch Techniken der künstlichen Intelligenz, maschinellen Lernens und prädiktive Analysen. Diese Anwendungsarchitekturen sind dafür wie geschaffen.

Bayessche Netze

Aktuelle Ansätze zur Selbstheilung konzentrieren sich eher auf reaktive als auf proaktive Heilung. Obwohl dieser reaktive Ansatz die Anwendungsbereitstellung repariert, kann er ihre Instabilität wohl kaum dauerhaft verhindern. Proaktive Lösungen sind bereits in Entwicklung.

Eine vielversprechende Methode zur Erstellung von Fehlermodellen für selbstheilende Software stellen Bayessche Netzwerke dar. Diese probabilistischen Modelle repräsentieren die bedingten Abhängigkeiten zwischen einer Gruppe von Variablen. Auf der Basis Bayesscher Netzwerke lassen sich fortgeschrittene Fehlermodelle für selbstheilende Software entwickeln.

Ein Softwaresystem könnte sich zum Beispiel aus einer clientseitigen Anwendung, einem Applikationsserver und einem Backend-Dienst zusammensetzen. Das Backend könnte etwa auf eine Datenbank und einen externen Cloud-Dienst über entsprechende APIs zugreifen.

Die Kommunikation zwischen dem Backend-Dienst, der API und der Datenbank würde in diesem Szenario (unter anderem) von der Qualität der Netzwerkverbindung abhängen. Ein selbstheilendes System könnte anhand eines Modells dieser Zusammenhänge ein bevorstehendes Fehlverhalten des Gesamtsystems voraussagen und selbsttätig Korrekturmaßnahmen ergreifen, noch bevor sich die Anwendung mit Fehlermeldungen zu Wort meldet.

Künstliche Immunsysteme

Ein weiterer vielversprechender Ansatz für die Gestaltung selbstheilender Software macht Anleihen bei der Funktionsweise des menschlichen Immunsystems. Künstliche Immunsysteme (kurz: AIS für Artificial Immune Systems) können nämlich Software befähigen, sich an neue betriebliche Anforderungen anzupassen, aus ihren Erfahrungen zu lernen und sich an die so gewonnenen Erkenntnisse zu erinnern.

Das menschliche Immunsystem verfügt über angeborene und adaptive Immunität. Die angeborene Immunität ist nicht spezifisch auf einen Krankheitserreger abgestimmt, sondern bietet allgemeinen Schutz. Aktuelle selbstheilende Softwaremodelle ähneln stark der angeborenen Immunität. Die adaptive Immunität kann aus aktuellen Bedrohungen lernen und dieses Wissen auf zukünftige Situationen anwenden.

Im Kern ahmen diese Systeme die Fähigkeit des natürlichen Immunsystems nach, zwischen Selbst- und Nicht-Selbst zu unterscheiden. Die Standardbetriebsbedingungen gelten als Selbst, und alles, was nicht diesen Bedingungen entspricht, betrachtet das Softwaresystem als Nicht-Selbst.

Künstliche Intelligenz-Systeme (AIS) arbeiten mit drei künstlichen Zelltypen: Detektoren, Gedächtniszellen und Antikörpern. Detektoren sind das Softwareäquivalent von Immunzellen, die das System ständig überwachen. Sie erkennen Muster und identifizieren Unregelmäßigkeiten wie Bugs in Softwarecode oder Sicherheitslücken.

Gedächtniszellen fungieren als der Aufzeichnungsmechanismus des Systems; sie speichern Informationen über vergangene “Infektionen” und die Reaktionen darauf.

Antikörper sind die korrigierenden Reaktionen, die das System erzeugt, wenn ein Detektor über ein definiertes Problem Alarm schlägt.

Mit seinen Detektorzellen, Gedächtniszellen und Antikörpern kann ein AIS die drei Hauptmerkmale des Immunsystems nachbilden: seine Anpassungs- und Lernfähigkeit und die Fähigkeit, sich an vergangene Vorfälle zu „erinnern“.

Am Ende der Fahnenstange: übers Ziel hinaus

In selbstheilenden Systemen, die auf vordefinierten Fehlermodellen beruhen, können unerwartete Ausfälle, die nicht zu den vorhandenen Modellen passen, unentdeckt bleiben und zu Systeminstabilität führen. Darüber hinaus erfordern diese Methoden oft menschliches Eingreifen, beispielsweise bei der Gestaltung von Wiederherstellungsverfahren oder der Behandlung von Fehlern, die das System nicht autonom beheben kann.

AIOps-Plattformen (Artificial Intelligence for IT Operations) wollen Abhilfe schaffen. Sie zielen darauf ab, anhand bestehender Erfahrungswerte Probleme vorherzusagen und proaktiv zu beheben. AIOps-Plattformen bauen eine ganzheitliche Darstellung der IT-Umgebung auf, indem sie Infrastruktur und Softwareressourcen (wie Server, Datenbanken, Anwendungen und Netzwerkgeräte) untersuchen, um zu verstehen, wie diese Komponenten der Bereitstellung miteinander in Beziehung stehen (Stichwort: Topology assembly).

Diese holistische Sichtweise zielt darauf ab, eine intelligente Optimierung der Anwendungsbereitstellung in einem dynamischen Betriebsumfeld zu ermöglichen. Derzeit fehlt AIOps-Plattformen allerdings noch der direkte Bezug zum Anwendungscode und den -Abhängigkeiten. Für die meisten Bereitstellungen sind diese Plattformen ein paar Nummern zu groß.

Fazit

Die Kombination von Frameworks wie Spring Boot oder Quarkus mit Kubernetes bietet eine leistungsfähige Lösung für die Entwicklung und Bereitstellung selbstheilender verteilter Anwendungen. Features wie Health Checks, bedarfsgerechte Skalierung und ereignisgetriebenes Konfigurationsmanagement versprechen eine höhere Resilienz und Zuverlässigkeit in Produktionsumgebungen der realen Welt.

(ID:50027994)