Ein CPU-Alarm, der in Wahrheit ein Firewall-Problem war
Ein CPU-Alarm, der in Wahrheit ein Firewall-Problem war
Inhaltsverzeichnis
Das Symptom: 280 % CPU
Eine Warnung meldete einen Container mit dauerhaft hoher CPU-Last – rund 280 Prozent, also fast drei ausgelastete Kerne. Betroffen war ausgerechnet die Zeitreihen-Datenbank des Monitorings selbst. Der naheliegende Verdacht: teure Abfragen, zu viele Kennzahlen, ein Speicherleck.
Ein Blick auf die Auslastung über die Zeit zeigte ein Sägezahnmuster: Die CPU stieg über etwa eine Stunde langsam an, kollabierte schlagartig und begann von vorn. Solche Muster deuten selten auf die App selbst hin, sondern auf etwas, das im Takt mit ihr wächst und periodisch aufgeräumt wird. Die Abfragen waren tatsächlich vernachlässigbar günstig. Der wahre Treiber lag woanders – und zwar überraschend tief.
Die Ursachenkette rückwärts
Aufgerollt vom Symptom zur Wurzel ergab sich eine Kette über vier Ebenen. Es lohnt sich, sie einmal ganz zu sehen, weil jedes Glied für sich harmlos aussah:
| Ebene | Beobachtung |
|---|---|
| 1. Symptom | Monitoring-Datenbank bei 280 % CPU, im Stundentakt sägezahnförmig |
| 2. Speicherdruck | Der Arbeitsspeicher wuchs im selben Takt gegen ein weiches Limit |
| 3. Datenflut | Explosionsartig viele neue Zeitreihen – mehr pro Stunde als der gesamte Bestand |
| 4. Wurzel | Ein Container startete sich alle zwölf Sekunden selbst neu |
Der Zusammenhang zwischen Ebene 3 und 4: Der Metrik-Sammler identifiziert Container über ihre eindeutige ID. Bei jedem Neustart bekommt ein Container eine neue ID – und damit legt der Sammler jedes Mal einen kompletten neuen Satz Zeitreihen an. Ein Container, der sich zehntausendfach neu startet, erzeugt so eine Flut immer neuer, kurzlebiger Kennzahlen.
Diese Flut trieb den Speicher der Zeitreihen-Datenbank über eine Schwelle, ab der die automatische Speicherbereinigung ihrer Laufzeitumgebung dauerhaft lief – über alle verfügbaren Kerne. Das waren die 280 Prozent. Alle ein bis zwei Stunden räumte die Datenbank ihren Zwischenspeicher auf, der Druck fiel, die CPU kollabierte – bis die nächste Runde begann. Das Sägezahnmuster.
Der Crashloop und seine Folgen
Der eigentliche Übeltäter war der Netzwerk-Filter-Container einer Mailserver-Suite. Er überwacht periodisch, ob seine eigene Firewall-Kette an der richtigen Stelle steht, und startet sich neu, wenn das nicht der Fall ist – ein Selbstheilungsmechanismus. Nur heilte hier nichts: Er startete sich alle zwölf Sekunden neu, seit Tagen, zehntausende Male.
Die Fehlermeldung wirkte harmlos – ein Hinweis, dass die Kette „an Position X“ stehe. In Wahrheit war es ein Dauer-Absturz. Und er hatte zwei Nebenwirkungen, die schwerer wogen als die CPU-Last selbst. Dazu später mehr; zunächst zur Wurzel.
Die eigentliche Wurzel: Regel-Reihenfolge
Die Mailserver-Suite besteht darauf, dass ihre Firewall-Kette ganz oben in der Eingangs- und Weiterleitungskette steht. Steht etwas darüber, wertet sie das als Manipulation und startet den Container neu. Genau das passierte: Die Kette war nach unten gerutscht.
Der Grund war ein selbstgeschriebenes Skript zur automatischen Sperrung von Angreifer-IPs. Es fügte jede neue Sperre ganz oben in die Firewall ein – in der Annahme, die geschützte Kette der Mailserver-Suite stünde ohnehin an erster Stelle. Das war ein Trugschluss: An erster Stelle stand die Kette des VPN-Dienstes. Jede neue Sperre schob die geschützte Kette also eine Position weiter nach unten. Nach ein paar Dutzend Sperren war die Toleranz überschritten, und der Selbstheilungs-Neustart begann – endlos.
Die dauerhafte Lösung: ipset statt Einzelregeln
Die schnelle Reparatur wäre gewesen, das Skript ein paar Positionen tiefer einfügen zu lassen. Doch das kuriert nur das Symptom. Zwei tiefere Probleme blieben: Erstens stapelten sich mit der Zeit tausende einzelne Sperrregeln, die jedes Paket linear durchlaufen muss. Zweitens waren viele dieser Regeln längst abgelaufene Sperren, die nie entfernt worden waren – sie leckten.
Die saubere Lösung ist ein ipset: eine kernelseitige Menge von IP-Adressen, auf die eine einzige Firewall-Regel verweist. Statt tausend Regeln gibt es eine Regel und einen Satz von Einträgen.
- Ein Eintrag, kein Regel-Wildwuchs: Neue Sperren sind Einträge in der Menge, keine neuen Firewall-Regeln. Die Kette bleibt kurz und stabil.
- Kernel-Zeitlimit: Jeder Eintrag bekommt eine Ablaufzeit. Der Kernel entfernt ihn selbst. Verwaiste Dauer-Sperren sind damit strukturell unmöglich – das Leck-Problem verschwindet.
- Konstante Nachschlagezeit: Die Prüfung, ob eine IP gesperrt ist, kostet unabhängig von der Anzahl der Einträge gleich viel – statt linear durch tausende Regeln zu laufen.
- Feste Position: Die eine Regel liegt genau unterhalb der geschützten Kette und bewegt sich nie wieder.
Wichtig ist dabei, dass das Sperr-Skript die Zielposition dynamisch ermittelt – also bei jeder Sperre nachschaut, wo die geschützte Kette gerade steht, und sich direkt darunter einordnet. So heilt sich die Anordnung nach einem Neustart oder einem Umbau der Ketten von selbst.
Zwei stille Nebenwirkungen
Der CPU-Alarm war letztlich das harmloseste Symptom. Zwei Nebenwirkungen des Crashloops wogen schwerer:
Die zweite Nebenwirkung waren die bereits erwähnten tausenden verwaisten Sperrregeln, die der Reboot dank Persistenz-Mechanismus sogar überdauerten. Beide Probleme verschwanden mit dem ipset-Umbau: Die Sperren leben jetzt in einer Menge mit Zeitlimit, und der Container startet nicht mehr neu, weil seine geschützte Kette stabil oben bleibt.
Verifiziert wurde das nicht durch Zusehen, sondern durch einen kontrollierten Test: eine harmlose Test-IP sperren, prüfen, dass der Eintrag mit Zeitlimit in der Menge landet, und wieder entsperren. Danach lief der Container stabil ohne einen einzigen weiteren Neustart – nach zuvor zehntausenden.
Lessons Learned
- Ein CPU-Alarm ist ein Symptom, keine Ursache. Ein Sägezahnmuster deutet auf etwas hin, das im Takt wächst und periodisch aufgeräumt wird – nicht auf die App selbst.
- Häufige Container-Neustarts sind teurer, als sie aussehen: Jede neue Container-ID erzeugt einen frischen Satz Kennzahlen und kann die Monitoring-Datenbank fluten.
- Automatisch eingefügte Firewall-Regeln müssen die Reihenfolge respektieren. „Ganz oben“ kollidiert mit jedem System, das selbst eine feste Position braucht.
- Für dynamische Sperrlisten ist ipset mit Kernel-Zeitlimit die robustere Wahl: eine Regel statt tausend, kein Leck, konstante Nachschlagezeit.
- Ein Crashloop, der bei jedem Neustart Sperren löscht, legt still den Brute-Force-Schutz lahm. Das Symptom (CPU) verrät davon nichts.
- Ziele auf die Ursache, nicht auf das Symptom. Das Limit hochzusetzen oder Kennzahlen zu filtern hätte die CPU beruhigt – und den eigentlichen Ausfall weiterlaufen lassen.
Am Ende steht eine unscheinbare Firewall-Regel gegen einen tagelangen, unsichtbaren Ausfall. Genau deshalb lohnt es sich, einem Alarm bis zur Wurzel zu folgen, statt ihn ruhigzustellen: Das lauteste Symptom ist selten das eigentliche Problem.