CPU-Throttling in Containern: Erkennen und CPU-Limits richtig setzen
AI generated
FROM
RUN
Docker · Performance · Linux
CPU-Throttling in Containern
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.

16 Min. Lesezeit cpu.stat nr_throttled CFS Quota Requests vs Limits

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.

11. FAQ: CPU-Throttling erkennen: Das Wichtigste auf einen Blick

1Was ist CPU-Throttling technisch genau?
Der Linux-CFS-Scheduler pausiert einen Container für den Rest eines Zeitfensters, sobald dieser innerhalb dieses Fensters seine gesetzte CPU-Quota vollständig verbraucht hat, unabhängig davon, ob auf dem Host gerade freie CPU-Kapazität verfügbar wäre.
2Warum zeigt eine niedrige durchschnittliche CPU-Auslastung trotzdem Throttling?
Durchschnittswerte über mehrere Sekunden glätten kurze, harte Drosselungsspitzen von wenigen Millisekunden fast vollständig, wodurch sie in klassischen Auslastungs-Dashboards praktisch unsichtbar bleiben, obwohl sie spürbare Latenz verursachen.
3Was bedeutet das Feld nr_throttled in cpu.stat?
Es zählt, in wie vielen der beobachteten CFS-Zeitfenster der Container tatsächlich gedrosselt wurde, weil die Quota vorzeitig aufgebraucht war. Das Verhältnis zu nr_periods zeigt, wie häufig Throttling relativ zur Gesamtlaufzeit auftritt.
4Was ist der Unterschied zwischen CPU-Requests und CPU-Limits?
Requests, umgesetzt über CPU-Shares, definieren nur eine relative Priorität bei Konkurrenz um Ressourcen, ohne absolute Obergrenze. Limits, umgesetzt über die Quota in cpu.max, setzen eine harte Obergrenze, unabhängig von der Host-Auslastung.
5Warum sind Multi-Thread-Anwendungen besonders anfällig für Throttling?
Wenn mehrere Threads kurzzeitig gleichzeitig auf verschiedenen Kernen aktiv werden, addiert sich ihre CPU-Zeit innerhalb desselben Zeitfensters, wodurch die Quota deutlich schneller aufgebraucht sein kann, als es der reine Durchschnittsverbrauch eines einzelnen Threads vermuten lässt.
6Wie messe ich, ob ein Container aktuell gedrosselt wird?
Am direktesten über die Datei cpu.stat der Container-cgroup, durch Beobachtung der Felder nr_throttled und throttled_usec vor und nach einer Lastspitze. Für dauerhaftes Monitoring eignet sich die Prometheus-Metrik container_cpu_cfs_throttled_periods_total.
7Sollte ich CPU-Limits ganz weglassen?
Auf dedizierten oder klar überprovisionierten Hosts kann das sinnvoll sein, kombiniert mit CPU-Shares für faire Verteilung bei Konkurrenz. Auf dicht gepackten Multi-Tenant-Hosts ist ein hartes Limit dagegen meist nötig, um andere Container zu schützen.
8Wie hängt die CFS-Periode mit Throttling zusammen?
Die Standardperiode beträgt 100 Millisekunden. Innerhalb jedes solchen Fensters darf der Container maximal die konfigurierte Quota verbrauchen. Ist die Quota früh im Fenster aufgebraucht, pausiert der Container für den Rest des Fensters, unabhängig von der Dringlichkeit der Anfrage.
9Können auch Sprachlaufzeiten selbst zu Throttling beitragen?
Ja, wenn eine Laufzeitumgebung die physische Kernanzahl des Hosts statt der über cpu.max effektiv verfügbaren Kernanzahl für ihre internen Thread-Pools verwendet, dimensioniert sie diese zu groß, was die Wahrscheinlichkeit von Throttling zusätzlich erhöht.
10Wie groß sollte der Sicherheitspuffer bei CPU-Limits sein?
Eine verbreitete Praxis ist, das Limit auf Basis der gemessenen realen Spitzenlast mit einem Puffer von etwa 20 bis 30 Prozent zu setzen, ähnlich der Vorgehensweise bei Memory-Limits, wobei Multi-Thread-Anwendungen tendenziell einen größeren Puffer benötigen.