Am 26. September kam die Nachricht zum wiederholten Mal: Load average is too high (per CPU load over 1.5 for 5m). Diesmal für einen Container, auf dem eine Handvoll Automationsdienste laufen. Der Container war idle. Die Last, die der Alarm meldete, gehörte jemand anderem.
Das Problem
Der Alarm war nicht neu. Am 15. September 14 Events, am 18. September 24, am 26. September 7× PROBLEM und 7× RESOLVED. Ein Trigger, der im selben Takt auf- und wieder zugeht, ist kein Lastproblem. Das ist eine Schwelle, die auf den falschen Messwert zeigt.
Betroffen war srv-rebellion, ein LXC auf pve-dagobah. Auf so einem Container laufen Dinge, die selten Rechenzeit brauchen: ein paar Hintergrunddienste, eine API, etwas Automatisierung. Wenn dort die Last „zu hoch" ist, erwartet man einen hängenden Prozess. Also erst der übliche Griff: rein in den Container, top, ps, die üblichen Verdächtigen. Nichts. Die CPU langweilte sich.
Die Diagnose
Der Messwert selbst war der Hinweis. Über /proc/loadavg sah der Container:
7.30 4.60 4.80
Bei vier Kernen sind das 1,83 pro Kern in der 1-Minuten-Last — deutlich über der Template-Schwelle von 1,5. Nur: Diese Last entstand nicht im Container. Ein LXC hat keinen eigenen Kernel und damit auch keine eigene Load-Average. Der Wert kommt von pve-dagobah, dem physischen Host, auf dem der Container läuft. Der Container erbte die Last seiner Nachbarn.
Damit war klar, in welche Richtung der Fix geht: nicht am Trigger drehen, sondern nachsehen, welche Schwelle dieser Host überhaupt haben soll. Die Doku war eindeutig. {$LOAD_AVG_PER_CPU.MAX.WARN} stand dort mit dem Wert 100, gesetzt am 6. Juli.
In der Datenbank stand sie nicht mehr.
Der Abruf über die API ist ein einziger Aufruf und braucht keine Weboberfläche:
curl -s -H "Authorization: $AUTH_HEADER" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"usermacro.get","params":{"host":"srv-rebellion","output":"extend"},"id":1}' \
"$ZABBIX/api_jsonrpc.php"
Das Ergebnis war leer, was das Load-Makro anging. Zabbix fiel damit auf den Template-Default zurück: 1,5. Ein Wert, der für einen echten Host vernünftig ist und für einen Container, der die Host-Last erbt, gar nichts aussagt.
Beim Nachziehen fiel ein zweiter Fall auf: Auf srv-coruscant fehlte {$CPU.UTIL.CRIT} — obwohl die Doku sie als gesetzt beschrieb. Dieselbe Klasse von Lücke, ein anderer Host, ein anderes Makro.
Die Ursache
Zwei Ursachen, die zusammenkommen.
Die erste ist konstruktionsbedingt: Ein LXC teilt den Kernel mit dem Host. /proc/loadavg im Container ist deshalb keine Container-Kennzahl, sondern die des Hosts. Das Linux-Template von Zabbix ist für einen Host mit eigenem Kernel gebaut. Auf einem Container misst der Load-Trigger also die Arbeit der Nachbarn. Deshalb steht auf diesen Hosts seit Juli ein Makro mit dem Wert 100 — keine Lastschwelle, sondern das kontrollierte Abschalten einer Messung, die dort nichts bedeutet.
Die zweite Ursache ist unangenehmer. Die Macro war weg. Warum, lässt sich nicht rekonstruieren. Zabbix führt für Host-Macros kein Audit-Log: Es gibt keinen Eintrag, der zeigt, wer wann usermacro.update oder usermacro.delete aufgerufen hat. Damit bleiben nur Möglichkeiten ohne Beleg — manuelles Löschen, ein erneutes Verknüpfen des Templates, ein Import, ein API-Batch. Ich kann keine davon behaupten, also behaupte ich keine.
Was sich sagen lässt: Das Template war die ganze Zeit in Ordnung. Sein Default von 1,5 ist korrekt für einen Host. Geschützt hat den Container nur der Override — und der ist im Template nicht sichtbar. Ein makellos grünes Template ist kein Beweis dafür, dass die Ausnahmen darunter noch existieren.
Der Fix
Die Load-Macro auf srv-rebellion neu anlegen, mit dem Wert 100. Dieselbe Macro-Klasse auf srv-coruscant für {$CPU.UTIL.CRIT} ergänzen. Beide werden über die API erzeugt, nicht über die Oberfläche — bei einem einzelnen Host ist das egal, bei einer Liste von Hosts ist es der Unterschied zwischen fünf Minuten und einem Nachmittag.
Danach die Gegenprobe, und zwar alle drei Punkte:
usermacro.getliefert beide Makros mit dem erwarteten Wert zurück.- Für den Host stehen keine offenen Probleme mehr an.
- Der Template-Trigger bleibt aktiviert und recovered sauber, statt als Leiche im Dashboard zu bleiben.
Der letzte Punkt ist der wichtige. Ein deaktivierter Trigger wäre die schnellere Lösung gewesen, aber er nimmt die Überwachung dauerhaft mit. Das Makro lässt den Trigger scharf und stellt nur die Schwelle dorthin, wo sie für diesen Hosttypus hingehört.
Was du mitnimmst
Zabbix auditiert Host-Macros nicht. Es gibt keine Historie, keine Änderungsliste, keinen Eintrag im Action-Log. Ein Override, der verschwindet, verschwindet spurlos — und der Trigger fällt still auf den Template-Default zurück, ohne dass irgendwo etwas rot wird.
Bei einem erneuten LXC-Fehlalarm gehört der erste Blick deshalb in usermacro.get, nicht in die Doku. Die Doku beschreibt den Zustand, den jemand einmal hergestellt hat. Die Datenbank beschreibt den Zustand, der gerade gilt. Genau diese Lücke war der Grund, warum der Alarm überhaupt wieder auftauchen konnte.
Und die unangenehmste Lehre: Ein grünes Template ist keine Garantie. Die Ausnahmen stehen nicht darin. Wer sie nicht prüft, prüft nur die Hälfte.
Wer den Zabbix-Server nicht auf einem bestehenden Host mitlaufen lassen will, kommt mit einem gebrauchten Mini-PC hin: Lenovo ThinkCentre M720q Tiny (i5-8400T, 16 GB, generalüberholt). Für mehr Reserve und mehrere Proxy-Instanzen reicht ein Mini-PC N100 Beelink S12 Pro; wer Zabbix als zentrale Instanz für ein größeres Netz betreibt, ist mit einem Minisforum MS-01 (i5-12600H, Barebone ohne RAM/SSD) in der oberen Klasse besser aufgestellt.