1. Einführung

1.1. Was ist Health Monitoring?

Unter Health Monitoring versteht man den Prozess der Überprüfung des Zustands einer Anwendung, um sicherzustellen, dass sie ordnungsgemäß und zuverlässig funktioniert. Dabei wird die Fähigkeit der Anwendung verfolgt, zu starten, auszuführen und den Datenverkehr effektiv zu verarbeiten. Health Monitoring ist ein Eckpfeiler des modernen Anwendungsmanagements, insbesondere in verteilten und Cloud-nativen Umgebungen.

1.2. Warum ist Health Monitoring wichtig?

  • Reliability (Zuverlässigkeit): Erkennt Probleme frühzeitig, um Ausfallzeiten zu verhindern.

  • Scalability (Skalierbarkeit): Stellt sicher, dass Systeme skaliert werden können, ohne dass kritische Komponenten beschädigt werden.

  • Integration: Funktioniert mit Tools wie Kubernetes, um die Wiederherstellung zu automatisieren und die Ressourcennutzung zu verbessern. → Probes

1.3. Eclipse MicroProfile Health

  • Spezifikation im MicroProfile-Ökosystem, bietet standardisierte Möglichkeit zur Implementierung von Health-Checks

microrprofile standards
Figure 1. MicroProfile Specifications (https://microprofile.io/2021/12/07/microprofile-5-0-release/)

1.4. SmallRye Health

  • konkrete Implementierung der Eclipse MicroProfile Health API

  • von Quarkus verwendet

2. Praxisbeispiel: SmallRye Health in Quarkus

2.1. Standard Health Checks ausprobieren

2.1.1. Maven-Projekt erstellen

Details
mvn io.quarkus.platform:quarkus-maven-plugin:3.18.1:create \
    -DprojectGroupId==at.htlleonding \
    -DprojectArtifactId==microprofile-health-demo \
    -Dextensions=='smallrye-health' \
    -DnoCode
cd microprofile-health-demo

2.1.2. Oder Extension zu bestehendem Projekt hinzufügen

Details
Maven-Wrapper
./mvnw quarkus:add-extension -Dextensions=='smallrye-health'

oder

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-health</artifactId>
</dependency>

2.1.3. Maven-Projekt starten

./mvnw clean quarkus:dev

2.1.4. Standard-Endpoints

Der Import der smallrye-health Extension stellt direkt 3 REST-Endpunkte bereit:

Liveness (Live)
  • GET /q/health/live

  • Zeigt an, dass die Anwendung ausgeführt wird und kein kritischer Fehler aufgetreten ist, der einen Neustart erforderlich macht.

  • Falls die JVM oder der Container abstürzt oder sich in einem fehlerhaften Zustand befindet, schlägt der Liveness-Check fehl.

Readiness (Ready)
  • GET /q/health/ready

  • Zeigt an, dass die Anwendung zur Bearbeitung von Anfragen bereit und voll funktionsfähig ist.

  • Falls Quarkus eine Abhängigkeit erkennt (z. B. Datenbank oder externe API), kann es automatisch bestimmen, dass die Anwendung nicht bereit ist, falls diese Abhängigkeit nicht verfügbar ist.

Started
  • GET /q/health/started

  • Zeigt an, ob der vollständige Startprozess abgeschlossen ist.

  • Dies geschieht, nachdem alle Startup-Beans und StartupEvent-Listener vollständig ausgeführt wurden.

Mit GET /q/health werden alle Health-Checks angezeigt. Du kannst diese Endpoints nun ausprobieren. /q/health-ui zeigt eine UI zur Darstellung der Checks an.

Anzeige aller Checks:

Details
/q/health
{
    "status": "UP", (1)
    "checks": [ (2)
    ]
}
/q/health/live
{
    "status": "UP", (1)
    "checks": [ (2)
    ]
}
/q/health/ready
{
    "status": "UP", (1)
    "checks": [ (2)
    ]
}
/q/health/started
{
    "status": "UP", (1)
    "checks": [ (2)
    ]
}
  1. status gibt an, ob alle Health-Checks erfolgreich waren.

  2. checks ist ein Array von individuellen Health-Checks (dazu später mehr).

2.2. Automatischer Health-Check für JDBC-Verbindungen

Wenn wir zu unserem Projekt eine JDBC-Datasource hinzufügen, wird automatisch ein Health Check (Readiness) erstellt, der die Verbindung zur Datenbank prüft. Hier wird dies mit einer PostgreSQL-DB demonstriert.

Nach der Konfiguration einer JDBC-Datasource ist bei den Readiness-Checks ist nun auch sichtbar, ob die Datenbank-Verbindung aufrecht ist:

Details
/q/health/ready
{
    "status": "UP",
    "checks": [
        {
            "name": "Database connections health check",
            "status": "UP",
            "data": {
                "<default>": "UP"
            }
        }
    ]
}

Stoppe nun die Datenbank und sieh dir die Readiness-Checks erneut an:

Details
/q/health/ready
{
    "status": "DOWN",
    "checks": [
        {
            "name": "Database connections health check",
            "status": "DOWN",
            "data": {
                "<default>": "Unable to execute the validation check for the default DataSource: Connection to localhost:5432 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections."
            }
        }
    ]
}

2.3. Eigene Health-Checks erstellen

Quarkus bietet die Möglichkeit, eigene Health-Checks zu erstellen. Dabei implementiert deine Health-Check-Klasse das Interface HealthCheck und trägt die @Liveness, @Readiness oder @Startup Annotation - je nach dem welche Art von Health-Check gefordert ist.

2.3.1. Beispiel mit einer Readiness Health-Check Prozedur

package at.htlleonding.healthchecks;

import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Readiness;

import java.util.Random;

@Readiness (1)
@ApplicationScoped (2)
public class CustomReadinessCheck implements HealthCheck { (3)
    @Override
    public HealthCheckResponse call() { (4)
        boolean b = new Random().nextBoolean(); (5)

        if(b) {
            return HealthCheckResponse.up("custom readiness check"); (6)
        } else {
            return HealthCheckResponse.down("custom readiness check"); (7)
        }
    }
}
  1. Die Klasse soll einen Readiness-Check durchführen, daher die Annotation @Readiness.

  2. @ApplicationScoped wird empfohlen, damit nur eine Instanz für alle Requests verwendet wird.

  3. Die Klasse implementiert das Interface HealthCheck

  4. call() führt den Check durch und liefert ein HealthCheckResponse zurück

  5. Hier wird durch einen zufälligen Boolean das Ergebnis des Health-Checks simuliert. Dies ist dann in einem Produktivsystem durch eine sinnvolle Methode zu erstzen (z.B. Ist die DB-Verbindung aufrecht?)

  6. Im positiven Falle wird HealthCheckResponse.up mit dem Namen des Health-Checks zurückgegeben.

  7. Im negativen Falle wird HealthCheckResponse.down mit dem Namen des Health-Checks zurückgegeben.

2.4. SmallRye Health für Probes in Kubernetes verwenden

Kubernetes nutzt Probes zur Überwachung von Containern: Liveness für Neustarts, Readiness für die Traffic-Steuerung und Startup für lange Startprozesse. SmallRye Health stellt dafür die Endpunkte /q/health/live, /q/health/ready und /q/health/started bereit, die Kubernetes direkt nutzen kann, um den Zustand der Anwendung automatisch zu verwalten.

2.4.1. Docker Image bauen

2.4.2. LeoCloud CLI & kubectl installieren

Siehe auch: LeoCloud User Manual

2.4.3. LeoCloud Dashboard starten

2.4.4. Credentials für GitHub Container Registry einpflegen (falls Image privat)

kubectl create secret docker-registry regcred \
  --docker-server=ghcr.io \
  --docker-username=YOUR_GITHUB_USERNAME \
  --docker-password=YOUR_GITHUB_PAT \ (1)
  --docker-email=YOUR_EMAIL
  1. Hier ist ein Token erforderlich, die einfache Eingabe des Passwortes funktioniert nicht

2.4.5. Konfigurationsdateien erstellen

1. PVC, Service & Deployment für die PostgreSQL-Datenbank

Details
k8s/postgres.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

---
apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  selector:
    app: postgres
  ports:
    - protocol: TCP
      port: 5432
      targetPort: 5432

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:17-alpine
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_USER
              value: app
            - name: POSTGRES_PASSWORD
              value: app
            - name: POSTGRES_DB
              value: db
          volumeMounts:
            - mountPath: /var/lib/postgresql/data
              name: postgres-storage
          livenessProbe:
            exec:
              command:
                - pg_isready
                - -U
                - app
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            exec:
              command:
                - pg_isready
                - -U
                - app
            initialDelaySeconds: 3
            periodSeconds: 5
      volumes:
        - name: postgres-storage
          persistentVolumeClaim:
            claimName: postgres-pvc

2. Deployment & Service für die Quarkus-Applikation

Details
k8s/quarkus-app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: quarkus-app
spec:
  replicas: 4
  selector:
    matchLabels:
      app: quarkus
  template:
    metadata:
      labels:
        app: quarkus
    spec:
      imagePullSecrets: (1)
        - name: regcred
      containers:
        - name: quarkus-app
          image: ghcr.io/2425-5bhif-wmc/01-referate-marksuus/mp-health-demo:latest (2)
          ports:
            - containerPort: 8080
          env:
            - name: QUARKUS_DATASOURCE_JDBC_URL
              value: jdbc:postgresql://postgres:5432/db
            - name: QUARKUS_DATASOURCE_USERNAME
              value: app
            - name: QUARKUS_DATASOURCE_PASSWORD
              value: app
          livenessProbe: (3)
            httpGet:
              path: /q/health/live
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 2
            timeoutSeconds: 2
            failureThreshold: 3
          readinessProbe: (4)
            httpGet:
              path: /q/health/ready
              port: 8080
            initialDelaySeconds: 3
            periodSeconds: 2
            timeoutSeconds: 2
            failureThreshold: 3
          startupProbe: (5)
            httpGet:
              path: /q/health/started
              port: 8080
            initialDelaySeconds: 0
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 30
---
apiVersion: v1
kind: Service
metadata:
  name: quarkus-service
spec:
  selector:
    app: quarkus
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080
  type: NodePort
  1. Dies wird bei nicht-öffentlichen Images benötigt. Hierbei greifen wir auf das vorhin angelegt Secret zu.

  2. Image-Name mit deinem ersetzen.

  3. Konfiguration der Liveness-Probe. Als Endpunkt wird /q/health/live verwendet.

  4. Konfiguration der Readiness-Probe. Als Endpunkt wird /q/health/ready verwendet.

  5. Konfiguration der Startup-Probe. Als Endpunkt wird /q/health/started verwendet.

initialDelaySeconds: Gibt an, wie lange Kubernetes nach dem Start des Containers wartet, bevor es die erste Probe durchführt.
periodSeconds: Gibt an, wie oft (in Sekunden) Kubernetes die Probe wiederholt.
timeoutSeconds: Gibt an, wie lange Kubernetes auf eine Antwort wartet, bevor die Probe als fehlgeschlagen gilt.
failureThreshold: Gibt an, wie oft die Probe fehlschlagen darf, bis der Pod als gescheitert gilt.

3. Ingress für die Quarkus-Applikation

Details
k8s/quarkus-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: quarkus-ingress
  annotations:
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-methods: "PUT, GET, POST, OPTIONS, DELETE"
    #nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: if200156.cloud.htl-leonding.ac.at (1)
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: quarkus-service
                port:
                  number: 8080
  1. Mit deinem Namespace ersetzen

2.4.6. Resourcen erstellen

im Verzeichnis der Konfigurationsdateien (k8s)
kubectl apply -f postgres.yaml
kubectl apply -f quarkus-app.yaml
kubectl apply -f quarkus-ingress.yaml
Du kannst den Status der Deployments/Pods/Ingresses z.B. mit watch -n 2 kubectl get pods|deployments|ingress beobachten.

2.5. Was macht Kubernetes nun, wenn eine der Probes fehlschlägt?

Probe Prüft was? Verhalten bei Fehlschlag

Liveness

Läuft die App noch oder ist sie abgestürzt?

Container wird als "unhealthy" markiert. Nach failureThreshold Fehlschlägen startet Kubernetes den Container neu.

Readiness

Kann die App Anfragen verarbeiten?

Container bleibt am Leben, aber erhält keinen Traffic mehr. Sobald die Probe wieder erfolgreich ist, wird der Container wieder in den Load Balancer aufgenommen.

Startup

Ist die App noch im Startprozess?

Solange die Startup Probe fehlschlägt, werden die Readiness- & Liveness-Probes ignoriert. Erst wenn die Startup Probe erfolgreich ist, starten die anderen Probes.

2.6. Demonstration

  • Qute Template auf /health-view verfügbar

    • Buttons zum Trigger eines Readiness-/Livenessfehlschlages

  • Verhalten währenddessen im k8s Dashboard einsehbar

3. Begriffsklärung

  • Cluster: Ein Cluster ist eine Gruppe von Nodes, die gemeinsam von Kubernetes verwaltet werden. Es stellt eine skalierbare Umgebung für die Bereitstellung, Verwaltung und Orchestrierung von Containern bereit (z.B.: LeoCloud, Azure AKS, Amazon EKS, Google GKE).

  • Service: Ein Service stellt eine stabile Netzwerkadresse für eine Gruppe von Pods bereit. Er ermöglicht die Kommunikation zwischen Diensten innerhalb des Clusters und optional den externen Zugriff.

  • Deployment: Ein Deployment verwaltet die Bereitstellung und Skalierung von Pods. Es sorgt für Ausfallsicherheit, automatische Neustarts und ermöglicht Rollbacks sowie Updates.

  • Ingress: Ein Ingress ist ein HTTP(S)-Router, der Anfragen basierend auf definierten Regeln an verschiedene Services weiterleitet. Er ermöglicht den externen Zugriff auf Anwendungen innerhalb des Clusters.

  • Pod: Ein Pod ist die kleinste ausführbare Einheit in Kubernetes und enthält einen oder mehrere Container, die gemeinsam Ressourcen wie Netzwerk und Speicher nutzen.

  • Container: Ein Container ist eine isolierte Laufzeitumgebung, die eine Anwendung mit allen benötigten Abhängigkeiten enthält. Kubernetes verwaltet Container innerhalb von Pods.

  • Node: Ein Node ist ein physischer oder virtueller Rechner im Kubernetes-Cluster, auf dem Pods ausgeführt werden. Jeder Node enthält eine Kubelet-Instanz, die die Pods steuert und mit dem Cluster kommuniziert.

  • Load Balancer: Der Load Balancer ist eine Kubernetes-Komponente, die den eingehenden Traffic auf mehrere laufende Pods verteilt. Dadurch werden die Last und Anfragen optimal verteilt, um eine hohe Verfügbarkeit und Skalierbarkeit sicherzustellen.