Selbstheilendes Kubernetes in Aktion: Drei Ausfälle, SigNoz-Signale und genehmigte Lösungen
Selbstheilendes Kubernetes mit OpenTelemetry und SigNoz: Ein Absturzzyklus, ein OOM-Kill und ein Anstieg der Tokens auf Kind – was jedes Signal anzeigt – sowie genehmigte KI-Lösungen.
Quellen: Self-Healing Kubernetes: Wiring OpenTelemetry, SigNoz, and a Groq-Powered Remediation Agent (PART 7)
Die Demo umfasst mehrere bewegliche Komponenten. Ein lokaler Kind-Cluster hostet SigNoz für Spuren und Protokolle sowie eine Python-App, deren drei Endpunkte absichtlich fehlerhaft sind. Ein Behebungsagent prüft auf Anomalien und bittet Groq um eine Diagnose, und ein React-Dashboard ermöglicht es dem Operator, die vorgeschlagene Korrektur vor dem Ausführen zu genehmigen oder abzulehnen. Sobald all das erstellt ist, besteht der interessante Teil darin, das Verhalten zu beobachten. Diese Durchführung löst jeden Fehler mit dem vollständigen Stack in Echtzeit aus, prüft, was SigNoz für jeden Vorfall aufzeichnet, und listet auf, was geändert werden muss, bevor man der Einrichtung außerhalb eines Laptops vertrauen kann.
Bestätigen, dass der Stack läuft
Vor dem Auslösen eines Fehlers sollten fünf Komponenten laufen. Die erste Überprüfung besteht darin, sicherzustellen, dass jeder Pod in beiden Namespaces funktionsfähig ist.
# All pods should show Running
kubectl get pods -n observability
kubectl get pods -n apps
Die Port-Forward-Einstellungen für SigNoz, den Zusammenfassungsdienst und das Dashboard benötigen jeweils eigene Terminal-Tabs, und der letzte Befehl im Block fügt die Agenten-Logs hinzu.
# Port-forwards (run each in its own tab if not already open)
# Tab A: kubectl port-forward -n observability svc/signoz 8080:8080
# Tab B: kubectl port-forward -n apps svc/summarizer 8000:80
# Tab C: kubectl port-forward -n apps svc/approval-dashboard 9000:9000
# Tab D: kubectl port-forward -n apps svc/approval-frontend-svc 3000:3000# Confirm the agent is polling
kubectl logs -n apps deployment/remediation-agent --tail=5
Die aktuellen „all-clear“-Anzeigen in diesem Ausgabeinhalt deuten darauf hin, dass der Agent gesund ist. Ein Pod, der nicht im Laufzustand ist, sollte zunächst mit kubectl describe überprüft und repariert werden. Halten Sie zwei Browser-Tabs sichtbar: SigNoz auf Port 8080 und das Dashboard auf Port 3000.
Szenario eins: CrashLoopBackOff
Durch das Setzen der Umgebungsvariable KILL_ME bei der Deployment des Summarizers und anschließendes Aufrufen seines Absturz-Endpunkts stirbt der Prozess mitten in der Anfrage ab.
kubectl set env deployment/summarizer -n apps KILL_ME=true
curl -s http://localhost:8000/crash
Der curl-Aufruf kann nichts zurückgeben oder hängen bleiben, da der Prozess während seiner Ausführung beendet wird. Eine Überwachung der Pods zeigt, dass die Neustartanzahl steigt und der Status zwischen „Running“ und „CrashLoopBackOff“ wechselt; bereits zwei oder drei solcher Zyklen reichen aus.
kubectl get pods -n apps -w
In SigNoz. Die Übersicht des Zusammenfassungsdienstes zeigt, wie die Fehlerrate sofort auf 100 % steigt, sobald der Pod zu abstürzen beginnt, während die Anfragenrate auf null fällt, wenn der Pod zwischen den Neustarts nicht verfügbar ist. Verkehr, dann nichts, dann wieder Verkehr – das ist das visuelle Zeichen eines Absturzloops. Ein Deployment-Rollout sieht anders aus: Es gibt einen reibungslosen Übergang ohne Anstieg der Fehlerrate, und auch eine Netzwerkpartition zeigt sich anders, da die Fehlerrate ansteigt, während der Pod weiterhin läuft.
Die Registerkarte „Logs“, gefiltert auf den Zusammenfassungsdienst, enthält die entscheidende Zeile, die die Anwendung unmittelbar vor ihrem Absturz ausgesendet hat.
CRITICAL summarizer — KILL_ME is true, exiting process
Dieses Protokoll enthält die Trace-ID der Anfrage, die es ausgelöst hat, sodass mit einem Klick von der Protokollzeile zur Trace und zur genauen HTTP-Anfrage übergegangen werden kann, die den Vorgang beendet hat.
Der Agent reagiert. Innerhalb von 30 Sekunden nach dem ersten Absturz wird der Abfrachtschleifenzyklus ausgelöst. Er erkennt eine Fehlerrate von 1,0 gegenüber einem Schwellenwert von 0,10, packt das Signal zusammen und ruft Groq auf. Die Protokolle des Agents zeigen diese Abfolge an.
kubectl logs -n apps deployment/remediation-agent --tail=20 -f
WARNING agent — crash loop signal: error_rate=1.00
INFO agent — anomaly detected: crash_loop — calling Groq for diagnosis
INFO agent — diagnosis: type=crash_loop severity=critical confidence=0.91
fix=kubectl set env deployment/summarizer -n apps KILL_ME-
INFO incident_poster — incident posted: id=<uuid> type=crash_loop
Genehmigung. Das Dashboard zeigt die Karte innerhalb von etwa zehn Sekunden an, und da die vorgeschlagene Korrektur die richtige ist, klickt der Operator auf „Genehmigen“. Der Backend-Server überprüft, ob der Befehl mit „kubectl set env“ beginnt und keine blockierten Begriffe enthält, und führt ihn anschließend aus. Die Karte wird aktualisiert, um die Genehmigung sowie die Änderung der Umgebung zu bestätigen.
✓ Approved by operator
deployment.apps/summarizer env updated
In der Pod-Überwachung gelingt der nächste Start, der Absturzzyklus endet und die Fehlerrate von SigNoz fällt auf null zurück. Vor dem nächsten Szenario bestätigt eine schnelle Überprüfung, dass die Variable nicht mehr vorhanden ist.
# The approve action already removed KILL_ME, but confirm:
kubectl get deployment summarizer -n apps -o jsonpath='{.spec.template.spec.containers[0].env}' | jq
KILL_ME sollte nicht in der Ausgabe erscheinen.
Szenario zwei: OOMKill
Eine Schleife von POST-Anfragen an den Leak-Endpunkt füllt einen Puffer im Speicher nach und nach etwas mehr.
for i in {1..30}; do
curl -s -X POST http://localhost:8000/leak | jq -c '{leaked_mb,chunks}'
sleep 0.5
done
Der Speicherverbrauch im Pod-Watch steigt an, und gegen den 25. Aufruf wird der Pod mitten im Lauf beendet, woraufhin curl anfängt, Verbindungsfehler zurückzugeben.
In SigNoz. Dieses Mal steigt die Fehlerrate nicht kontinuierlich an. Ein OOM-Kill erfolgt plötzlich, und die Anwendung gibt niemals eine Fehlerantwort zurück. Stattdessen fällt die Anfragenrate rapide ab: Verkehr, ein abrupter Stopp, eine Lücke, anschließend wieder Verkehr, sobald der Pod neu gestartet wird.
Die Logs enthalten die eigentliche Erklärung. Wenn man sie auf den Zusammenfassungsmodus filtert, geht der Zeitraum mit der Lücke von Warnungen voraus, bei denen die Größe des Puffers zunimmt.
WARNING summarizer — leak endpoint hit, bucket now holds 10 MB
WARNING summarizer — leak endpoint hit, bucket now holds 20 MB
...
WARNING summarizer — leak endpoint hit, bucket now holds 240 MB
After the last warning comes silence, with no exception, no stack trace and no graceful shutdown message, because the kernel's OOM killer gives the process no chance to log. A run of climbing warnings followed by nothing is the pattern worth recognizing.
The cluster confirms the cause from its side.
kubectl describe pod -n apps -l app=summarizer | grep -A 5 "Last State"
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Der Abbruchcode 137 ist das Signaturwert des Kernels – SIGKILL (Signal 9) plus 128. Er unterscheidet einen Abbruch aufgrund von zu wenig Speicher von einem Anwendungsfehler, der mit Code 1 beendet wird, sowie von einem Misserfolg einer Lebensfähigkeitsprüfung, die ebenfalls 137 anzeigt, aber einen anderen Grundtext im Beschreibungsausgabe-Teil enthält.
Der Agent reagiert. Seine Überprüfung auf zu wenig Speicher durchsucht den SigNoz-Log-Index nach dem String „OOMKilled“ in den letzten zehn Minuten. Bei einem Treffer packt er Logbeispiele sowie das Ereignis des Pod-Neustarts in eine Meldung zusammen, und die Diagnose wird zurückgegeben.
type=oom_kill severity=critical confidence=0.88
fix=kubectl set resources deployment/summarizer -n apps \
--containers=summarizer --limits=memory=512Mi
Die vorgeschlagene Lösung verdoppelt die Speichergrenze. Als kurzfristige Maßnahme ist das vernünftig, da sie Zeit schafft, um die Anwendungsbehebung bereitzustellen. Nach der Freigabe setzt der Backend eine Grenze von 512 MiB fest, Kubernetes führt einen schrittweisen Neustart durch und der Pod arbeitet wieder mit ausreichend Speicher.
Das Leck selbst bleibt im Code bestehen. Die Änderung erhöht lediglich die Obergrenze; bei einem echten Vorfall wäre eine Codebehebung notwendig, die das Leck periodisch beseitigt. Die Aufgabe des Agents besteht darin, das Leck zu stoppen, während die Ursache weiterhin eine menschliche Bearbeitung erfordert.
Szenario drei: Latenz und Anstieg der Token
Eine kurze Schleife von Aufrufen an den teuren Endpunkt führt zum dritten Ausfall.
for i in {1..5}; do
curl -s -X POST http://localhost:8000/expensive \
| jq '{output_tokens, length_chars}'
sleep 2
done
Nichts stürzt ab und nichts wird beendet, wodurch der Pod weiterhin läuft. Die Antworten dauern jedoch jeweils 8 bis 15 Sekunden, und die Token-Zahl nähert sich der Obergrenze von 2.048 max_tokens.
In SigNoz. Gerade bei solchen leisen Fehlern zeigt sich, warum der Observability-Stack wichtig ist. Die p95-Latenzlinie bleibt bei normalen Zusammenfassungsaufgaben nahe bei 1,5 Sekunden, steigt jedoch an, wenn teure Aufrufe stattfinden. Dabei handelt es sich um einen plötzlichen Sprung und nicht um einen langsamen Anstieg, da jeder Aufruf die Obergrenze erreicht. Wenn man die Spuren nach Dauer sortiert, kommen diese teuren Aufrufe an die Spitze – jeweils mit einer Dauer von 8 bis 15 Sekunden.
Die Anzeige einer langsamen Trace im Flame Graph zeigt fast die gesamte Zeit innerhalb eines einzelnen Child-Span für die ausgehende HTTP-Anfrage an api.groq.com. Der Span für den Endpunkt-Handler selbst ist sehr kurz, da der FastAPI-Handler fast nichts ausführt, und die Kommunikation mit dem LLM dominiert die Zeitverbräuche. Die Länge des Antwortinhalts dieses Spans ist deutlich größer als bei einem normalen Summarisierungsaufruf – das sind die Ausgabetoken, die als Bytes übertragen werden. Gerade Linie, Anstieg und wieder Gerade Linie, wenn die Schleife endet: Das ist das Profil eines falsch konfigurierten Endpunkts, nicht eines Infrastrukturfehlers.
Der Agent reagiert. Die Latenzprüfung vergleicht den p95-Wert pro Route. Die teure Route weist einen Wert von etwa 11.000 ms auf, während der Schwellenwert bei 5.000 ms liegt, und der Agent fasst die detaillierten Werte zusammen.
{
"check": "latency_spike",
"spikes": { "/expensive": 11240.0 },
"p95_ms_by_route": {
"/summarize": 1820.0,
"/expensive": 11240.0,
"/healthz": 4.0
}
}
Groq liest es und erstellt eine präzise Diagnose.
type=token_spike severity=high confidence=0.85
root_cause: The /expensive endpoint has no reasonable max_tokens budget
and a prompt that invites long outputs, causing p95 latency
to spike to 11s versus 1.8s on the normal /summarize path.
fix=kubectl set env deployment/summarizer -n apps MAX_TOKENS_EXPENSIVE=256
Die vorgeschlagene Lösung legt eine Umgebungsvariable fest, die die App lesen kann, um das Token-Budget an diesem Endpunkt zu begrenzen. Nach der Freigabe wird diese Variable gesetzt. Bei einer tatsächlichen Bereitstellung würde die App außerdem so angepasst, dass sie MAX_TOKENS_EXPENSIVE aus der Umgebung abruft. Der Agent schlägt eine Konfigurationsanpassung statt einer Codeänderung vor, was im Rahmen einer kubectl-basierten Lösung angemessen ist.
Drei Ausfälle in derselben Zeitspanne
Wenn alle drei Szenarien nacheinander ausgeführt werden, zeigt die Übersicht über einen Zeitraum von zwei Stunden drei unterschiedliche Muster.
- Der Absturzzyklus äußert sich als wellenförmiger Anstieg der Fehlerrate auf 100 %, anschließendem Abfall auf Null und erneutem Anstieg, begleitet von entsprechenden Einbrüchen in der Durchsatzleistung.
- Der Absturz aufgrund von Speicherüberschreitung zeigt sich als plötzlicher Einbruch in der Anfragenrate ohne Anstieg der Fehlerrate, da der Prozess nie die Gelegenheit hatte, einen Fehler zurückzugeben.
Drei Fehlermuster mit drei erkennbaren Mustern werden auf einer einzigen Anzeigetafel dargestellt, ohne dass spezielle Alarmregeln oder manuelles Auswerten von Protokollen erforderlich sind. Das ist der Vorteil von korrelierten Spuren, Metriken und Protokollen.
Änderungen für die Produktion
Die Demo läuft auf einem Laptop und verzichtet auf Aspekte, die an anderen Stellen wichtig sind.
Ersetzen Sie den im Speicher gespeicherten Incident-Store. Die Liste der Incidents ist ein Python-Dictionary innerhalb des Backend-Pods, wodurch ein Neustart alle Informationen löscht. SQLite über SQLAlchemy erfordert nur eine einzige Änderung und benötigt keinen zusätzlichen Dienst, und Redis eignet sich, wenn die Anzeigetafel über mehrere Backend-Replicas skaliert werden muss. Die API-Struktur bleibt unverändert, nur der dahinterliegende Speicher wechselt.
Fügen Sie Slack-Benachrichtigungen hinzu. Derzeit muss jemand den Dashboard beobachten, um zu erkennen, dass eine Karte erschienen ist. Ein Slack-Webhook-Aufruf im Incident-Receiver des Backends löst dieses Problem.
import httpx
SLACK_WEBHOOK = os.environ.get("SLACK_WEBHOOK_URL")def notify_slack(card: dict):
if not SLACK_WEBHOOK:
return
d = card["diagnosis"]
httpx.post(SLACK_WEBHOOK, json={
"text": (
f"*{d['severity'].upper()} — {d['incident_type']}* on `{card['service']}`\n"
f"Root cause: {d['root_cause']}\n"
f"Proposed fix: `{d['proposed_fix']}`\n"
f"Confidence: {int(d['confidence']*100)}%\n"
f"<http://your-dashboard-url|Review on dashboard>"
)
})
Die Aufrufung des Benachrichtigers am Ende des Empfängers bedeutet, dass jedes neue Incident den Bereitschaftskanal anpingt, und der Genehmiger muss nicht länger eine Browserleiste im Auge behalten.
Behandeln Sie die Cluster-Zugangsdaten sorgfältig. Der Backend ruft kubectl auf, was in Ordnung ist, wenn die Node-Umgebung dies konfiguriert. In einem echten Cluster benötigt der Backend-Pod ein ServiceAccount mit geeigneten RBAC-Rechten sowie einen für Cluster-Zugangsdaten konfigurierten Kubernetes-Python-Client.
from kubernetes import client, config
config.load_incluster_config()
apps_v1 = client.AppsV1Api()
core_v1 = client.CoreV1Api()
Jeder genehmigte Befehlstyp wird anschließend zu einem direkten SDK-Aufruf und nicht zu einem Shell-String. Das ist umständlicher, bietet aber eine strukturierte Fehlerbehandlung, Audit-Logging am API-Server sowie kein Risiko von Shell-Injection.
Verengen Sie die Allowlist. Die Demo akzeptiert jeden kubectl set env-Befehl. In der Produktion sollte dies auf benannte Deployments und Namespaces beschränkt werden.
ALLOWED_TARGETS = {
"deployment/summarizer -n apps",
"deployment/worker -n apps",
}
def validate_command(cmd: str) -> bool:
if not any(cmd.startswith(p) for p in ALLOWED_PREFIXES):
return False
if any(term in cmd for term in BLOCKED_TERMS):
return False
if not any(target in cmd for target in ALLOWED_TARGETS):
return False
return True
Ein Modell darf nicht in der Lage sein, Korrekturen für einen beliebigen Namespace oder eine beliebige Bereitstellung außerhalb der ihm mitgegebenen Liste vorzunehmen.
Projekte, die man sich als Nächstes ansehen sollte
Die Demo wurde von Grund auf entwickelt, um zu zeigen, wie die einzelnen Komponenten miteinander verbunden sind, während eine produktive Nutzung auf bestehenden Projekten aufbauen würde.
- KubeStellar hilft, wenn Arbeitslasten mehrere Clustern umfassen. Es übernimmt die Aufgaben des Control-Planes, die die Behebung über Clustergrenzen hinweg erschweren – beispielsweise die Identifizierung des Clusters, in dem sich ein Incident befindet, bevor eine Korrektur vorgeschlagen wird.
- OpenTelemetry geht weit über das hinaus, was die Demo benötigte. Der Collector verfügt über Prozessoren und Exporteure, die Kubernetes-Metadaten automatisch zu den Spans hinzufügen können, sodass Ressourcenattribute nicht manuell in den App-Manifesten eingefügt werden müssen.
Was die gesamte Implementierung enthält
Das fertige System ist ein dreiknotiger Kind-Cluster mit einer realistischen Mehrknoten-Scheduling-Topologie, wobei SigNoz über Helm bereitgestellt wird und Spuren, Metriken sowie Protokolle über den OpenTelemetry Collector aufnimmt. Ein Python-FastAPI-Dienst wird beim Start automatisch instrumentiert, sodass jede HTTP-Anfrage und jeder LLM-Aufruf zu einer Spur wird und jede Protokollzeile ohne zusätzlichen Instrumentierungscode in der Anwendung korreliert werden kann.
Drei fehlerhafte Endpunkte verursachen die drei häufigsten Kubernetes-Ausfälle: einen Prozessabsturz, einen Speichermangel sowie eine Verschlechterung hinsichtlich Kosten und Latenz. Ein Korrekturagent prüft alle 30 Sekunden SigNoz, führt Schwellenwertprüfungen durch, bündelt die Signale in eine strukturierte Anfrage, bittet ein LLM um eine Diagnose und erstellt eine Incidents-Karte. Ein FastAPI-Backend erhält diese Karten, überprüft die vorgeschlagenen Lösungen anhand einer Allowlist und führt die genehmigten Befehle über kubectl aus. Das React-Dashboard bietet Kontrollen für Genehmigung und Ablehnung, ein Abzeichen zur Anzeige des Modellvertrauens, ein Banner für unsichere Diagnosen sowie ein Protokoll aller durchgeführten Aktionen.
Alles läuft lokal über APIs der kostenlosen Ebene und kostet nichts. Der Code ist ausreichend klein – etwa 600 Zeilen Python zusammen mit einer HTML-Seite und einigen YAML-Manifesten – sodass jeder Bestandteil von Anfang bis Ende verstanden werden kann.
Verwandte Literatur
- Selbst-heilendes Multi-Agenten-Team in LangGraph: Architekt, Tester, Debugger — Erstellen Sie eine LangGraph-Zustandsmaschine in Python, in der ein Architekt, Entwickler, pytest-Sandbox, Debugger und Reviewer zusammenarbeiten und solange wiederholen, bis die Tests bestehen.
- Erstellen eines ReAct-Agenten in LangChain: Executor, Scratchpads und ihre Grenzen — Erstellen Sie einen ReAct-Agenten, der Tools verwendet, sehen Sie, wie Agent und AgentExecutor die Arbeit aufteilen, und erfahren Sie, warum LangGraph den Kontrollfluss, Schleifen und den Zustand besser handhabt.