Zurück zum Blog

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.