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:
| Metric | Series |
|---|---|
container_network_advance_tcp_stats_total | 52.173 |
container_network_receive_bytes_total | 3.599 |
container_network_transmit_bytes_total | 3.599 |
| weitere container_network_* | je ~3.599 |
| Gesamt im Head-Block | 115.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
advancedtcpwar in cAdvisors--disable_metricsnicht 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.
| Zeitpunkt | advance_tcp Series | Prometheus RSS | Swap belegt |
|---|---|---|---|
| Vor Fix | 52.173 | ~940 MB | 3,7 GiB |
| +3 min | 12.672 | ~670 MB | 3,4 GiB |
| Nach Head-Compaction | ~0 | deutlich weniger | normalisiert |
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
| Komponente | Status |
|---|---|
| CPU-Last (Prometheus) | ✅ Normalisiert nach Head-Compaction |
| Swap-Auslastung | ✅ Rückläufig |
| cAdvisor advancedtcp | ✅ Deaktiviert |
| Prometheus Drop Rule | ✅ Aktiv |
| Series-Count | ✅ ~50 % reduziert |