Wenn niedrige CPU-Auslastung trotzdem langsam ist
Ein Dashboard zeigt 40 Prozent CPU-Auslastung, trotzdem häufen sich Timeouts und Latenzspitzen. Der Widerspruch löst sich fast immer über einen Blick in cpu.stat auf: Der Container wird gedrosselt, lange bevor die durchschnittliche Auslastung irgendein Limit erreicht.
Inhaltsverzeichnis
- 1. Was CPU-Throttling technisch bedeutet
- 2. cpu.stat: Die Datei, die Throttling sichtbar macht
- 3. CPU-Requests und CPU-Limits sind zwei verschiedene Dinge
- 4. Typische Symptome im Betrieb
- 5. Throttling systematisch messen statt vermuten
- 6. CPU-Limits richtig dimensionieren
- 7. Alternative Strategie: Shares statt harter Limits
- 8. Häufige Fehler bei der Limit-Konfiguration
- 9. Praktischer Workflow von Symptom zu Lösung
- 10. Zusammenfassung
- 11. FAQ
1. Was CPU-Throttling technisch bedeutet
CPU-Throttling entsteht, wenn ein Prozess innerhalb einer Abrechnungsperiode mehr CPU-Zeit anfordert, als die gesetzte Quota erlaubt, und der Linux-Scheduler den Prozess daraufhin für den Rest dieser Periode komplett pausiert, unabhängig davon, ob auf dem Host gerade freie CPU-Kapazität verfügbar wäre. Das ist der entscheidende Unterschied zu einer normalen, überlasteten CPU: Selbst wenn acht von zwölf Host-Kernen komplett untätig sind, wird ein Container mit einem zu knappen Limit trotzdem gedrosselt, weil die Drosselung ausschließlich an der Quota der eigenen cgroup hängt, nicht an der tatsächlichen Host-Auslastung.
Der zugrunde liegende Mechanismus heißt Completely Fair Scheduler, kurz CFS, und arbeitet in festen Zeitfenstern, standardmäßig 100 Millisekunden lang. Innerhalb jedes Fensters darf ein Container so viel CPU-Zeit verbrauchen, wie seine Quota erlaubt. Ist die Quota innerhalb der ersten 20 Millisekunden eines 100-Millisekunden-Fensters bereits ausgeschöpft, etwa weil kurzzeitig mehrere Threads gleichzeitig aktiv waren, pausiert der Kernel den Container für die verbleibenden 80 Millisekunden, komplett unabhängig davon, wie dringend die Anfrage eigentlich wäre.
2. cpu.stat: Die Datei, die Throttling sichtbar macht
Die Kernel-Datei cpu.stat, die für jede cgroup unter /sys/fs/cgroup/.../cpu.stat existiert, enthält genau die Zähler, die Throttling nachweisen. Das Feld nr_periods zählt, wie viele CFS-Zeitfenster insgesamt beobachtet wurden, seit die cgroup existiert. nr_throttled zählt, in wie vielen dieser Fenster der Container tatsächlich gedrosselt wurde, weil die Quota vorzeitig aufgebraucht war. throttled_usec summiert die gesamte Zeit in Mikrosekunden, die der Container insgesamt in gedrosseltem Zustand verbracht hat, seit die cgroup angelegt wurde.
Das Verhältnis von nr_throttled zu nr_periods ist der aussagekräftigste Einzelwert für die Frage, ob ein CPU-Limit zu knapp bemessen ist: Liegt dieses Verhältnis dauerhaft über wenigen Prozent, drosselt der Container regelmäßig, selbst wenn die durchschnittliche CPU-Auslastung über docker stats harmlos aussieht, weil Durchschnittswerte über mehrere Sekunden hinweg kurze, harte Drosselungsspitzen glätten und dadurch praktisch unsichtbar machen.
# cpu.stat eines laufenden Containers auslesen
CID=$(docker inspect --format '{{.Id}}' mein-container)
cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/cpu.stat
# Beispielausgabe:
# usage_usec 48213942
# user_usec 41022103
# system_usec 7191839
# nr_periods 128841
# nr_throttled 9532
# throttled_usec 4821394
# Throttling-Quote grob berechnen: 9532 / 128841 = ~7.4 Prozent aller Perioden
3. CPU-Requests und CPU-Limits sind zwei verschiedene Dinge
Ein häufiges Missverständnis ist, CPU-Requests und CPU-Limits als dasselbe Konzept mit unterschiedlichem Namen zu behandeln. Ein Request, in Docker über --cpu-shares abgebildet, definiert nur eine relative Priorität bei Konkurrenz um CPU-Zeit, wenn mehrere Container gleichzeitig um dieselben Kerne konkurrieren, setzt aber keine absolute Obergrenze. Ein Container mit hohem Share-Wert bekommt bei Konkurrenz mehr CPU-Zeit zugeteilt als einer mit niedrigem Wert, kann aber, solange keine Konkurrenz besteht, durchaus die volle verfügbare Host-CPU nutzen.
Ein Limit dagegen, umgesetzt über die Quota in cpu.max und über das Docker-Flag --cpus, setzt eine harte, absolute Obergrenze, die unabhängig von der Auslastung des restlichen Hosts gilt. Genau hier entsteht der häufigste Fehler: Ein Team setzt ein Limit, um die Ressourcennutzung vorhersagbar zu machen, wählt dabei aber einen Wert, der unter der tatsächlichen Spitzenlast der Anwendung liegt, und produziert damit künstliches Throttling in genau den Momenten, in denen die Anwendung eigentlich am meisten Leistung bräuchte.
4. Typische Symptome im Betrieb
CPU-Throttling äußert sich selten als offensichtlicher Fehler, sondern als diffuses Performance-Problem: Latenzspitzen, die scheinbar zufällig auftreten, Requests, die gelegentlich deutlich länger dauern als der Median, ohne erkennbaren Zusammenhang mit der Gesamtlast, und Health-Checks, die sporadisch fehlschlagen, obwohl die Anwendung laut Logs normal weiterläuft. Besonders tückisch ist, dass klassische CPU-Auslastungs-Dashboards diese Spitzen oft komplett verstecken, weil sie über Sekunden oder Minuten gemittelt werden, während eine einzelne Drosselung nur wenige Millisekunden dauert.
In Umgebungen mit synchroner Request-Verarbeitung, etwa klassischen Web-Frameworks ohne asynchrones I/O, verstärkt sich das Problem zusätzlich: Ein gedrosselter Thread blockiert dann nicht nur sich selbst, sondern potenziell den gesamten Worker, wodurch eine kurze CFS-Drosselung von wenigen Millisekundenbruchteilen zu spürbaren Latenzspitzen im zweistelligen Millisekundenbereich anwächst, sobald mehrere Anfragen in derselben Periode um dieselbe knappe Quota konkurrieren.
5. Throttling systematisch messen statt vermuten
Statt bei vagen Latenzproblemen sofort Anwendungscode zu verdächtigen, lohnt sich ein systematischer erster Schritt: cpu.stat vor und nach einer Lastspitze auszulesen und die Differenz von nr_throttled und throttled_usec zu bilden. Steigen beide Werte während der Lastspitze deutlich an, ist CPU-Throttling ein sehr wahrscheinlicher Mitverursacher, unabhängig davon, was Anwendungsmetriken über CPU-Auslastung in Prozent aussagen.
Für dauerhaftes Monitoring eignet sich cAdvisor, das die Metrik container_cpu_cfs_throttled_periods_total direkt aus den cgroup-Statistiken extrahiert und über Prometheus als Zeitreihe verfügbar macht. Ein Grafana-Panel, das diese Metrik neben der klassischen CPU-Auslastung darstellt, deckt Throttling-Probleme zuverlässig auf, die in reinen Auslastungs-Dashboards komplett unsichtbar blieben, weil hohe Drosselungsraten durchaus bei niedriger durchschnittlicher Auslastung auftreten können.
# Throttling vor und nach einer Lastspitze vergleichen
CG=/sys/fs/cgroup/system.slice/docker-${CID}.scope
echo "Vorher:"; grep -E 'nr_throttled|throttled_usec' $CG/cpu.stat
# ... Lastspitze ausloesen, z.B. durch Lasttest ...
echo "Nachher:"; grep -E 'nr_throttled|throttled_usec' $CG/cpu.stat
# Prometheus-Query fuer dauerhaftes Monitoring
# rate(container_cpu_cfs_throttled_periods_total[5m])
# / rate(container_cpu_cfs_periods_total[5m])
6. CPU-Limits richtig dimensionieren
Die Grundregel für sinnvolle CPU-Limits ist dieselbe wie bei Memory-Limits: erst messen, dann limitieren, nicht umgekehrt. Ein Container sollte zunächst ohne oder mit einem sehr großzügigen Limit unter realistischer Spitzenlast beobachtet werden, idealerweise mit einem Lasttest, der kurzfristige Bursts simuliert, nicht nur eine konstante Dauerlast, weil gerade kurze, intensive Lastspitzen für Throttling besonders anfällig sind. Erst mit diesen Messwerten lässt sich ein Limit setzen, das die tatsächliche Spitzenlast mit ausreichendem Puffer abdeckt.
Ein konkretes Rechenbeispiel verdeutlicht die Quota-Mechanik: Ein Limit von --cpus=2 erzeugt eine Quota von 200000 Mikrosekunden bei einer Standardperiode von 100000 Mikrosekunden. Läuft die Anwendung mit vier parallelen Threads, die jeweils kurzzeitig auf verschiedenen Kernen aktiv werden, kann die Summe der CPU-Zeit aller vier Threads die Quota schon nach einem Bruchteil der Periode erreichen, selbst wenn kein einzelner Thread durchgehend rechnet. Multi-Thread-Anwendungen brauchen deshalb tendenziell großzügigere Limits als der reine Durchschnittsverbrauch vermuten lässt.
# Rechenbeispiel: --cpus=2 in cpu.max uebersetzt
docker run -d --name calc-test --cpus=2 nginx:alpine
CID=$(docker inspect --format '{{.Id}}' calc-test)
cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/cpu.max
# Ausgabe: 200000 100000 --> 2 volle CPU-Kerne pro 100ms-Fenster
# Periode selbst laesst sich theoretisch anpassen (selten sinnvoll),
# meist bleibt sie beim Standardwert von 100000 Mikrosekunden
7. Alternative Strategie: Shares statt harter Limits
Für latenzkritische Anwendungen, bei denen selbst seltenes Throttling inakzeptabel ist, ist eine Alternative, ganz auf harte CPU-Limits zu verzichten und ausschließlich mit CPU-Shares zu arbeiten. Ohne Limit kann ein Container jederzeit die volle verfügbare Host-CPU nutzen, während Shares nur bei tatsächlicher Konkurrenz um Ressourcen greifen und dann eine faire, proportionale Verteilung sicherstellen, statt eine harte, zeitfenstergebundene Bremse zu ziehen.
Diese Strategie funktioniert am besten auf dedizierten oder klar überprovisionierten Hosts, auf denen genug CPU-Kapazität für alle Container gemeinsam vorhanden ist, sodass ein einzelner Container im Fehlerfall zwar theoretisch mehr CPU beanspruchen könnte, dies in der Praxis aber selten zum Problem für andere Workloads wird. Auf stark ausgelasteten, dicht gepackten Multi-Tenant-Hosts ist dagegen ein hartes Limit meist unverzichtbar, um zu verhindern, dass ein einzelner fehlerhafter Container alle anderen Container verdrängt.
8. Häufige Fehler bei der Limit-Konfiguration
Ein besonders verbreiteter Fehler ist es, CPU-Limits pauschal von einer Kubernetes- oder Docker-Compose-Vorlage zu kopieren, ohne sie an die tatsächliche Anwendung anzupassen, etwa ein generisches cpus: 0.5 für jeden Microservice, unabhängig davon, ob dieser Service rechenintensive Aufgaben oder nur leichte I/O-Weiterleitung erledigt. Ein zweiter häufiger Fehler ist, Limits nur einmal beim initialen Deployment zu setzen und nie wieder anzupassen, obwohl sich der tatsächliche Ressourcenbedarf einer Anwendung über Monate durch neue Features und wachsende Datenmengen verändert.
Ein dritter, subtilerer Fehler betrifft Sprachen und Frameworks, die selbst die Anzahl verfügbarer CPU-Kerne für interne Thread-Pool-Größen abfragen: Wenn diese Abfrage die physische Kernanzahl des Hosts liefert statt der über cpu.max gesetzten effektiven Kernanzahl, dimensioniert die Anwendung ihre eigenen Thread-Pools zu groß, was die Wahrscheinlichkeit von Throttling zusätzlich erhöht, weil mehr Threads gleichzeitig um dieselbe, deutlich kleinere Quota konkurrieren als ursprünglich vorgesehen.
9. Praktischer Workflow von Symptom zu Lösung
Ein bewährter Workflow beginnt mit dem Symptom, meist unerklärliche Latenzspitzen trotz niedriger durchschnittlicher CPU-Auslastung, und führt über cpu.stat zur Bestätigung, ob Throttling tatsächlich auftritt. Bestätigt sich der Verdacht, folgt die Messung der realen Spitzenlast ohne Limit, danach die Neudimensionierung des Limits mit ausreichendem Puffer, und abschließend dauerhaftes Monitoring der Throttling-Rate, um zukünftige Regressionen frühzeitig zu erkennen, bevor sie sich erneut als diffuse Latenzprobleme äußern.
Die folgende Tabelle stellt die wichtigsten Kennzahlen aus cpu.stat mit ihrer jeweiligen Bedeutung gegenüber, als schnelle Referenz für die nächste Performance-Analyse eines Containers, dessen CPU-Auslastung im Dashboard harmlos aussieht, dessen Nutzer aber trotzdem über Verzögerungen klagen.
| Kennzahl | Bedeutung | Warnsignal | Nächster Schritt |
|---|---|---|---|
| nr_periods | Anzahl beobachteter CFS-Zeitfenster | Referenzwert, kein Alarm für sich | Basis für Verhältnisberechnung |
| nr_throttled | Anzahl gedrosselter Zeitfenster | Verhältnis zu nr_periods über wenigen Prozent | Limit erhöhen oder Last analysieren |
| throttled_usec | Gesamte Drosselungszeit in Mikrosekunden | Deutlicher Anstieg während Lastspitzen | Mit Latenzmetriken korrelieren |
| cpu.max Quota/Periode | Konfiguriertes Limit | Quota deutlich unter gemessener Spitzenlast | Limit anhand Peak-Messung neu setzen |
Mironsoft
Container-Infrastruktur, CI-Pipelines und Deployment-Automatisierung
Docker-Setups, die im Team und in Produktion tragfähig bleiben?
Wir prüfen bestehende Dockerfiles und Compose-Stacks auf Sicherheitslücken, aufgeblähte Images und fragile Build-Pipelines und bauen daraus eine Container-Infrastruktur, die schnell baut, sicher läuft und im Team nachvollziehbar bleibt.
Dockerfile-Review
Multi-Stage-Builds, Layer-Caching und Image-Größe systematisch optimieren.
Security-Audit
Container-Isolation, Secrets-Handling und Image-Scanning gegen echte Angriffsflächen absichern.
CI/CD-Integration
Build-Pipelines, Registries und Deployment-Strategien für reproduzierbare Releases aufbauen.
10. Zusammenfassung
CPU-Throttling erkennen: Das Wichtigste auf einen Blick
Kernursache
CFS-Quota wird innerhalb eines 100ms-Fensters vorzeitig aufgebraucht, unabhängig von freier Host-CPU.
Sichtbar in
cpu.stat mit den Feldern nr_periods, nr_throttled und throttled_usec.
Häufigster Fehler
Requests (Shares) und Limits (Quota) verwechseln, Limits ohne Lasttest raten.
Lösung
Reale Spitzenlast messen, Limit mit Puffer setzen, Throttling-Rate dauerhaft monitoren.