CPU-Throttling ist das erzwungene Pausieren eines Prozesses oder Containers durch den Linux-Scheduler, sobald er seine CPU-Quota für die aktuelle Scheduling-Periode aufgebraucht hat. In Kubernetes entsteht es durch CPU-Limits, die per CFS Bandwidth Control durchgesetzt werden, und erhöht oft die Tail-Latenz, obwohl die durchschnittliche CPU-Auslastung niedrig wirkt.
Was ist CPU-Throttling?
CPU-Throttling bedeutet im Container-Kontext, dass der Linux-Kernel eine Gruppe von Prozessen nicht mehr einplant, weil sie ihre erlaubte CPU-Zeit für die aktuelle Periode verbraucht hat. Die Threads wären lauffähig, müssen aber bis zum Beginn der nächsten Periode warten. Der Begriff wird auch für Hardware verwendet, die wegen Hitze oder Leistungsgrenzen ihren Takt senkt. Dieser Artikel behandelt die Bedeutung für Container und cgroups, die für Cloud-native Services am relevantesten ist.
Throttling ist eine klassische Quelle verwirrender Latenz: Dashboards zeigen einen Container bei moderater durchschnittlicher CPU-Last, trotzdem stocken Requests immer wieder für Dutzende Millisekunden.
So funktionieren CFS-Quotas
CPU-Limits in Kubernetes werden über die Bandwidth Control des Completely Fair Scheduler durchgesetzt. Jede cgroup erhält eine Quota an CPU-Zeit pro Periode, hinterlegt in cpu.max bei cgroup v2 (bzw. cpu.cfs_quota_us und cpu.cfs_period_us bei v1). Die Standardperiode beträgt 100 ms. Ein Limit von 500m (eine halbe CPU) wird zu 50 ms CPU-Zeit pro 100 ms Periode.
Die Quota teilen sich alle Threads des Containers. Ein Service mit acht beschäftigten Threads kann die vollen 50 ms in etwa 6 ms Echtzeit verbrauchen und wird dann für die restlichen 94 ms der Periode angehalten. Die durchschnittliche Auslastung über eine Minute kann trotzdem bei 30 % liegen, weil das Throttling in kurzen Schüben auftritt, die Durchschnittswerte verdecken.
CPU-Requests funktionieren anders: Sie legen ein relatives Gewicht bei CPU-Konkurrenz fest und beeinflussen das Scheduling, begrenzen den Verbrauch aber nicht. Nur Limits verursachen Throttling.
CPU-Throttling messen
Der Kernel zählt Throttling in cpu.stat: nr_periods, nr_throttled und throttled_usec. cAdvisor stellt diese Werte als container_cpu_cfs_periods_total, container_cpu_cfs_throttled_periods_total und container_cpu_cfs_throttled_seconds_total bereit. Eine illustrative PromQL-Abfrage für den Anteil gedrosselter Perioden:
sum by (pod) (rate(container_cpu_cfs_throttled_periods_total{container="api"}[5m]))
/
sum by (pod) (rate(container_cpu_cfs_periods_total{container="api"}[5m]))
Ein Wert von 0,25 hieße, dass ein Viertel aller Scheduling-Perioden mit einem pausierten Container endete. Stellen Sie den Wert neben die Perzentile der Request-Latenz, denn Throttling zeigt sich im p99 lange bevor es den Median bewegt.
Warum Throttling die Tail-Latenz erhöht
Ein gedrosselter Container wird nicht gleichmäßig langsamer. Er läuft mit voller Geschwindigkeit, bis die Quota aufgebraucht ist, und hält dann vollständig an, bis die nächste Periode beginnt. Ein Request, der kurz vor diesem Moment eintrifft oder etwas mehr CPU braucht als üblich, wartet bis zum Rest der Periode, oft Dutzende Millisekunden, bevor er weiterläuft. Erstreckt sich ein Request über mehrere Perioden, summieren sich die Wartezeiten. Nachgelagerte Services sehen das als sporadisch langsame Aufrufe, Retries und Timeouts, die die Last weiter erhöhen und noch mehr Throttling auslösen können.
Weil sich der Effekt auf kurze Schübe konzentriert, zeigt der übliche CPU-Graph mit Minutenauflösung ihn nicht. Die Garbage Collection verstärkt ihn: Ein Collector, der viele parallele Threads auf einmal startet, kann einen Großteil der Quota in einem Zug verbrauchen und die nachfolgenden Anwendungsthreads pausieren lassen. Deshalb sollten Sie Throttling immer zusammen mit Latenzverteilungen betrachten und nicht nur mit der durchschnittlichen CPU-Auslastung.
Beispiel: Throttling trotz niedriger Auslastung
Ein illustratives Szenario: Eine Go-API mit einem CPU-Limit von einem Kern läuft auf einem Node mit 16 Kernen. Vor Go 1.25 setzte die Runtime GOMAXPROCS auf die Zahl der Host-Kerne statt auf das Container-Limit. 16 Threads konkurrieren also um 100 ms CPU pro Periode und verbrauchen sie bei Lastschüben in wenigen Millisekunden. Der Service zeigt etwa 40 % durchschnittliche Auslastung, die p99-Latenz liegt aber beim Mehrfachen des Medians. Wird GOMAXPROCS an das Limit angepasst oder eine Go-Version genutzt, die das cgroup-Limit ausliest, sinkt der Anteil gedrosselter Perioden und das Tail-Verhalten glättet sich. Ähnliches gilt für ältere JVMs, die Thread-Pools und GC-Threads nach den Host-Kernen dimensionierten.
Best Practices
- Setzen Sie CPU-Requests auf Basis des beobachteten Verbrauchs, damit der Scheduler Pods sinnvoll platziert.
- Entscheiden Sie bewusst, ob latenzkritische Services überhaupt CPU-Limits brauchen. Viele Teams setzen stattdessen auf Requests und Isolation auf Node-Ebene, andere behalten Limits für Fairness und Kostenkontrolle.
- Wenn Sie Limits behalten, dimensionieren Sie sie für Lastspitzen statt für Durchschnittswerte und beobachten Sie den Anteil gedrosselter Perioden.
- Stimmen Sie die Parallelität der Runtime (Thread-Pools,
GOMAXPROCS, GC-Threads) auf das CPU-Limit ab. - Unterscheiden Sie Throttling von Konkurrenz: Ein Noisy Neighbor bremst Sie über geteilte CPU, Throttling entsteht durch Ihre eigene Quota.
Wie Obseria hilft
Das Kubernetes-Monitoring von Obseria zeigt CPU-Verbrauch im Verhältnis zu Requests und Limits zusammen mit gedrosselten Perioden pro Container und korreliert sie mit der Trace-Latenz derselben Pods. Continuous Profiling zeigt, welcher Code die CPU-Quota verbraucht. Throttling und Speicherdruck treten bei unterdimensionierten Workloads oft gemeinsam auf, siehe OOM Kill.