Zurück zum Blog

Prometheus frisst CPU: Kardinalitätsexplosion durch cAdvisor

Heute Morgen: Server-Load dauerhaft über 100 %, Swap fast vollständig belegt (3,7 von 4 GiB). SSH war noch erreichbar, alle Services liefen – aber das System arbeitete am Limit.

load average: 5.16, 4.86, 4.49
Swap: 4.0Gi  3.7Gi used  304Mi free

Ein Blick auf die Prozessliste zeigte den Schuldigen sofort:

nobody  101292  285%  /bin/prometheus ...
root      3441   57%  /usr/bin/cadvisor ...

Prometheus bei 285 % CPU, cAdvisor bei 57 % – seit dem letzten Start vor 16 Stunden. In dieser Zeit hatte Prometheus knapp 2.740 CPU-Minuten akkumuliert, also durchgehend fast drei Kerne belastet.

Ursachenanalyse

Die Prometheus TSDB-Status-API liefert eine aufschlüssreiche Übersicht der aktuell geladenen Time Series. Das Top-Ergebnis war eindeutig:

MetricSeries
container_network_advance_tcp_stats_total52.173
container_network_receive_bytes_total3.599
container_network_transmit_bytes_total3.599
weitere container_network_*je ~3.599
Gesamt im Head-Block115.025

container_network_advance_tcp_stats_total allein hatte 45 % aller Time Series im Prometheus-Head-Block belegt. Das ist eine klassische Kardinalitätsexplosion.

Woher kommen 52.000 Series für eine einzige Metric?

cAdvisor sammelt für jedes Netzwerk-Interface jedes Containers TCP-Statistiken auf Verbindungsebene – aufgeschlüsselt nach State, Interface und weiteren Labels. Mit vielen laufenden Containern und Docker-internen Bridge-Netzwerken multipliziert sich das schnell:

  • Jeder Container hat mehrere Netzwerk-Interfaces (je verbundenem Docker-Netzwerk)
  • Pro Interface entstehen viele TCP-Statuskombinationen
  • Die Kategorie advancedtcp war in cAdvisors --disable_metrics nicht aufgeführt

Für operative Zwecke braucht niemand diese TCP-Verbindungsdetails pro Container. Der Informationswert rechtfertigte die Last nicht.

Fix

1. cAdvisor: advancedtcp deaktivieren

In docker-compose.yml wurde advancedtcp zur --disable_metrics-Liste hinzugefügt:

command:
  - '--housekeeping_interval=30s'
  - '--store_container_labels=true'
  - '--disable_metrics=percpu,sched,tcp,udp,diskIO,disk,advancedtcp'

2. Prometheus: Drop Rule

metric_relabel_configs greift vor dem Schreiben in den TSDB – die Metric wird nie gespeichert und belastet damit weder Speicher noch Indizierung:

- job_name: 'cadvisor'
  static_configs:
    - targets: ['cadvisor:8080']
      labels:
        service: 'cadvisor'
  metric_relabel_configs:
    - source_labels: [__name__]
      regex: 'container_network_advance_tcp_stats_total'
      action: drop

Anwenden

cd /opt/docker/monitoring && docker compose up -d --force-recreate cadvisor
curl -X POST http://localhost:9090/-/reload

Verlauf nach dem Fix

Der Abbau der vorhandenen Series war nicht sofort sichtbar – die 52.000 Series steckten noch im aktuellen Head-Block (2h-Fenster im RAM). Prometheus räumt den Head-Block erst bei der nächsten automatischen Compaction auf.

Zeitpunktadvance_tcp SeriesPrometheus RSSSwap belegt
Vor Fix52.173~940 MB3,7 GiB
+3 min12.672~670 MB3,4 GiB
Nach Head-Compaction~0deutlich wenigernormalisiert

Lernpunkte

Wenn eine einzelne Metric mehr als 10–20 % aller Series hält, ist sie ein Kandidat für eine Drop-Rule oder eine Deaktivierung an der Quelle. Cardinality-Check:

curl -s http://localhost:9090/api/v1/status/tsdb | python3 -c "
import sys, json
d = json.load(sys.stdin)['data']
print('Total series:', d['headStats']['numSeries'])
for x in d['seriesCountByMetricName'][:10]:
    print(f'  {x[chr(34)+'name'+chr(34)]}: {x[chr(34)+'value'+chr(34)]}')
"

Drop Rules sind kein Workaround – sie sind Design: In größeren Setups ist es normal, dass eine Scrape-Quelle mehr Metriken liefert als man tatsächlich braucht. metric_relabel_configs mit action: drop ist der saubere Weg, die Ingestion zu begrenzen.

Ergebnis

KomponenteStatus
CPU-Last (Prometheus)✅ Normalisiert nach Head-Compaction
Swap-Auslastung✅ Rückläufig
cAdvisor advancedtcp✅ Deaktiviert
Prometheus Drop Rule✅ Aktiv
Series-Count✅ ~50 % reduziert