Health Checks und Kubernetes
Health Checks in ASP.NET richtig nutzen, Teil 4
In containerisierten Umgebungen, speziell unter Kubernetes, werden Health Checks primär von Liveness- und Readiness-Probes konsumiert. Zur Erinnerung: Die Readiness-Probe beantwortet die Frage: „Kann dieser Pod aktuell sinnvoll Traffic verarbeiten?“ Schlägt die Prüfung fehl, wird der Pod aus dem Load-Balancing genommen, aber nicht neu gestartet. Die Liveness-Probe beantwortet die Frage: „Ist der Prozess in einem derart kaputten Zustand, dass ein Neustart sinnvoll ist?“ Schlägt diese Prüfung wiederholt fehl, startet Kubernetes den Container neu.
Best Practice ist, diese beiden Aspekte sauber zu trennen und im Code entsprechende Endpunkte bereitzustellen. ASP.NET Core unterstützt dafür Tagged Health Checks: Checks lassen sich mit Tags ausstatten, und so können Endpunkte definiert werden, die jeweils nur eine Teilmenge ausführen, wie Listing 1 zeigt.
Listing 1: Konfiguration von Readiness und Liveness
builder.Services.AddHealthChecks()
// Datenbank betrifft die Readiness
.AddSqlServer(
connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
name: "database",
tags: new[] { "ready" })
// Speicherprüfung betrifft die Liveness
.AddCheck("memory", () =>
{
var allocated = GC.GetTotalMemory(false);
var threshold = 1L * 1024L * 1024L * 1024L; // 1 GB
return allocated < threshold
? HealthCheckResult.Healthy($"Speicher: {allocated / 1024 / 1024} MB")
: HealthCheckResult.Unhealthy($"Speicherlimit überschritten: {allocated / 1024 / 1024} MB");
}, tags: new[] { "live" });
var app = builder.Build();
// Liveness-Endpunkt
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live")
});
// Readiness-Endpunkt
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
Die Liveness-Probe ruft dann nur schnelle Checks auf, im Beispiel self, während die Readiness-Probe die kompletten Abhängigkeiten prüft. Damit lässt sich verhindern, dass eine langsam reagierende Datenbank die Liveness-Probe triggert und Pods unnötig neu startet. In der Kubernetes-Deployment-Definition sehen die Probes typischerweise aus wie in Listing 2 gezeigt:
Listing 2: Beispiel für eine Kubernetes-Deployment-Definition
livenessProbe:
httpGet:
path: /health/live
port: 80
initialDelaySeconds: 10
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 80
initialDelaySeconds: 5
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 5
Die Parameter sollten zu Startzeit, typischer Latenz und Fehlermustern des konkreten Service passen. Zu aggressive Probes führen sonst dazu, dass Kubernetes Pods aus dem Verkehr zieht, obwohl sie sich gerade nur von einem kurzen Datenbank-Glitch erholen.
Management-Port und Sicherheit
In manchen Umgebungen werden Health-Endpunkte auf einen separaten Management-Port gelegt. Über RequireHost lässt sich der Endpunkt auf eine bestimmte Portkonfiguration beschränken. Dabei lässt sich der Management-Port beispielsweise per Umgebungsvariable setzen. Die beiden folgenden Code-Snippets zeigen die Konfiguration für eine konkrete Domain oder für alle Hosts, aber auf einem bestimmten Port:
app.MapHealthChecks("/health")
.RequireHost("www.example.net:5005");
app.MapHealthChecks("/health")
.RequireHost("*:5005");Health-Check-Bibliotheken verteilen
Größere Projekte entwickeln in der Regel eigene Health Checks in Form von Bibliotheken, beispielsweise für Warteschlangen, Message-Broker oder Cloud-Speicherdienste. Die Microsoft-Dokumentation empfiehlt, die IHealthCheck-Implementierung kapselbar zu halten und eine Extension-Methode zur Registrierung zu veröffentlichen. So lassen sich komplexe Prüfungen leicht in andere Services integrieren.
Für produktiven Code haben sich folgende Best Practices etabliert:
- Kurz angebundene Proben: Es ist sinnvoll, Time-outs für HTTP-Aufrufe, Datenbankverbindungen und Dateizugriffe zu vergeben, um blockierende Checks zu vermeiden.
- Degradierte Zustände: HealthStatus.Degraded bietet sich an, wenn der Dienst zwar verfügbar, aber nicht performant ist, zum Beispiel durch eine hohe Latenz bei einem API. Dies ermöglicht feineres Routing.
- Ausführliche Log-Meldungen: Bei Fehlern ist ein detailliertes Log mit Ausnahmen und Kontext zu schreiben, um die Ursachen schnell zu finden.
- CI-Integration: Health Checks bieten sich für Continuous-Integration-Pipelines an. Ein einfacher Curl-Aufruf gegen den /health-Endpunkt kann einen Build fehlschlagen lassen, wenn ein Service nicht läuft.
- Überwachungstools: Es ist sinnvoll, Health Checks mit Monitoring-Tools wie beispielsweise Prometheus, Grafana oder OneUptime zu kombinieren. Die JSON-Antwort lässt sich in Dashboards visualisieren, damit Teams frühzeitig Engpässe erkennen können.
Fazit
Health Checks sind ein zentrales Element für zuverlässige ASP.NET-Core-Anwendungen im Cloud-Umfeld. Durch die Kombination aus integrierten Checks (SQL, Redis), selbst erstellten Prüfungen (externe APIs, Speicherplatz) und Kubernetes-Probes lässt sich eine umfassende Sicht auf die Systemgesundheit erstellen. Eine saubere Strukturierung über Tags, klare Fehlerstatus und detaillierte JSON-Antworten ermöglichen es, Probleme schneller zu erkennen und zu beheben.