Unser Kubernetes-Cluster skalierte perfekt. Zu perfekt.

Der Incident:

  • Traffic-Spitze um 10 Uhr
  • HPA skalierte Pods: 5 → 50
  • Jeder Pod: 20 Datenbankverbindungen
  • Verbindungen gesamt: 100 → 1.000
  • PostgreSQL max_connections: 200
  • „FATAL: too many connections“ auf jedem neuen Pod

Die Kaskade:

Neue Pods bekamen keine Verbindung, fielen bei ihren Health Checks durch und wurden neu gestartet — und jeder Neustart löste eine frische Welle von Verbindungsversuchen gegen eine Datenbank aus, die bereits am Limit war. Innerhalb von Minuten war der Cluster im Thrashing: Pods im Dauerzyklus, bei jedem Durchlauf ein Verbindungssturm, und die ursprüngliche Traffic-Spitze blieb weiterhin unbedient.

Der Fix:

PgBouncer vor PostgreSQL, im Modus Transaction Pooling: 1.000 Verbindungen auf Anwendungsseite, gemultiplext auf rund 100 echte Serververbindungen. Die Wahl des Modus ist entscheidend. Session Pooling bindet eine Serververbindung für die gesamte Lebensdauer an einen Client und hätte unsere Zahl also überhaupt nicht reduziert. Transaction Pooling gibt die Serververbindung nach jeder Transaktion frei — daher kommt das 10:1-Multiplexing —, bricht aber alles, was Session-State voraussetzt: Advisory Locks auf Session-Ebene, LISTEN/NOTIFY, SET-Variablen auf Session-Ebene und Prepared Statements bei älteren Treibern. Darauf haben wir zuerst geprüft; der eine Service, der Advisory Locks nutzte, bekam seinen eigenen kleinen Session-Mode-Pool.

Zweitens haben wir den HPA davon abgehalten, loszustürmen. Das Hochskalieren ist jetzt ratenlimitiert, damit nachgelagerte Systeme Zeit haben, jeden Schritt zu verkraften, und die maximale Replica-Zahl ist auf das gedeckelt, was die Datenschicht tatsächlich tragen kann:

behavior:
  scaleUp:
    stabilizationWindowSeconds: 0
    policies:
    - type: Pods
      value: 4
      periodSeconds: 60
maxReplicas: 30

Auf AWS ist RDS Proxy die Managed-Alternative: dieselbe Multiplexing-Idee, IAM-Auth, nichts zu betreiben — zu Preisen pro vCPU und mit dem Vorbehalt, dass Session-State Verbindungen „pinnt“ und das Multiplexing, für das Sie bezahlen, stillschweigend abschaltet.

Was wir bewusst nicht getan haben:

Readiness Probes an eine Datenbankverbindung koppeln. Das klingt vernünftig, aber Readiness auf einer geteilten Abhängigkeit bedeutet: Wenn die Datenbank kurz wackelt, gehen alle Pods im selben Moment auf unready, der Service verliert sämtliche Endpoints, und aus einer partiellen Degradierung wird ein Totalausfall — genau die Kaskade, die wir stoppen wollten. Probes sollten den Pod prüfen, nicht über geteilte Infrastruktur abstimmen; diesen Fehlermodus behandeln wir in Health Checks, die logen.

Lektion: Autoscaling weiß nichts von nachgelagerten Limits. Sie müssen sie durchsetzen — im Pooler, in der HPA-Policy und in der Kapazitätsplanung für die eine Komponente, die nicht autoskaliert. Diese Kette Ende-zu-Ende zu dimensionieren ist Kernarbeit im DevOps Engineering, kein nachträgliches Tuning.


← Zurück zu Lessons Learned

Incidents wie dieser sind vermeidbar. Unser Team für AWS Managed Services überwacht Kundenumgebungen 24/7 mit KI-gestütztem Monitoring und Senior Engineers am Pager — damit Geschichten wie diese im Archiv bleiben.