Prometheus Stale Metrics: Wenn alte Container-IDs tagelang falsche Alerts auslösen
Das Problem
Seit mehreren Tagen trudelte täglich dieselbe Alerting-Mail ein: hohe CPU-Auslastung eines bestimmten Containers – auch nachdem das Problem längst behoben schien. Ein Blick in das Monitoring bestätigte: der Container lief völlig normal, trotzdem feuerte der Alert weiter.
Ursache: Stale Metrics in Prometheus
Das Monitoring basiert auf einem selbstgeschriebenen Docker-Exporter in Python, der die prometheus_client-Bibliothek verwendet. Diese Bibliothek speichert Metriken als Gauge-Objekte mit Label-Sets – zum Beispiel container_name und container_id.
Das Problem: Wird ein Container neugestartet, bekommt er eine neue Container-ID. Der Exporter legt dann ein neues Label-Set für die neue ID an – das alte Label-Set mit der alten ID bleibt aber im Speicher erhalten und wird weiterhin an Prometheus ausgeliefert, solange der Exporter läuft.
# Alte ID (Container nicht mehr aktiv) → letzter Messwert bleibt
docker_container_cpu_usage_percent{container_id="a4f77ae527f0"} 93.4
# Neue ID (aktuell laufend) → korrekt
docker_container_cpu_usage_percent{container_id="3ba80a06d518"} 0.6
Prometheus wertete beide Serien aus – und weil die alte Serie über 80 % lag, feuerte der ContainerHighCPU-Alert alle 4 Stunden neu (gemäß repeat_interval). Das über mehrere Tage, obwohl der Container real längst stabil lief.
Fix 1: Stale Metrics aktiv entfernen
Die Lösung: Der Exporter trackt jetzt, welche (container_name, container_id)-Paare bekannt sind. Am Ende jedes Scrape-Zyklus werden alle Label-Sets entfernt, die nicht mehr im Docker-API auftauchen:
_known_basic = set() # (container_name, container_id)
_known_state = set() # (container_name, container_id, state, status)
def collect_metrics():
global _known_basic, _known_state
current_basic = set()
for container in client.containers.list(all=True):
name = container.name
cid = container.short_id
current_basic.add((name, cid))
# ... Metriken setzen ...
# Veraltete Label-Sets löschen
for (name, cid) in _known_basic - current_basic:
container_cpu_usage.remove(name, cid)
container_memory_usage.remove(name, cid)
# ...
print(f"Removed stale metrics for {name} ({cid})")
_known_basic = current_basic
Der Gauge.remove()-Aufruf aus prometheus_client entfernt das Label-Set vollständig aus der internen Registry – Prometheus sieht es beim nächsten Scrape nicht mehr und markiert die Serie als stale.
Fix 2: Alert-Regel um State-Filter ergänzen
Als zusätzliche Absicherung wurde die Alert-Regel selbst defensiver gestaltet. Statt nur den CPU-Wert zu prüfen, wird jetzt per AND ON(container_id)-Join sichergestellt, dass die Container-ID auch tatsächlich als running bekannt ist:
# Vorher
expr: docker_container_cpu_usage_percent > 80
# Nachher – feuert nur für aktuell laufende Container
expr: >
docker_container_cpu_usage_percent > 80
AND ON(container_id) docker_container_state{state="running"}
So kann eine stale Serie aus einem gestoppten Container nie mehr einen False-Positive auslösen.
Fix 3: TooManyContainersStopped verfeinert
Ein zweiter Alert zählte alle Container, die sich nicht im running-State befanden. Das Problem: Viele Dienste nutzen One-Shot-Container, die nach der Initialisierung im created-State verbleiben und nie wirklich stoppen. Diese wurden fälschlicherweise als Problemfälle gewertet.
# Vorher: zählt auch "created"-Container
expr: count(docker_container_state{state!="running"}) > 10
# Nachher: nur wirklich beendete Container, bekannte Ausnahmen exkludiert
expr: count(docker_container_state{state="exited", container_name!~"known-helper-.*"}) > 3
Takeaway
Die prometheus_client-Bibliothek akkumuliert Label-Sets über die gesamte Laufzeit eines Exporters. Wer Metriken mit dynamischen Labels (wie Container-IDs) erfasst, muss veraltete Einträge aktiv mit .remove() bereinigen – sonst werden alte Werte endlos weitergeliefert.
Zusätzlich lohnt es sich, Alert-Regeln defensiv zu formulieren: eine CPU-Alert, die per Join prüft ob der Container überhaupt läuft, ist robuster als ein reiner Schwellenwert-Vergleich.