Kubernetes CPU-throttling: waarom pods stilvallen bij laag verbruik

Een pod toont 12% gemiddeld CPU-verbruik in Grafana, maar wordt 60% van de tijd gethrottled. De oorzaak is niet een overbelaste node. Het is de Linux CFS-scheduler die een tijdsbudget per 100 ms handhaaft dat monitoringdashboards onzichtbaar maken door middeling. Dit artikel legt het mechanisme uit, toont hoe je het meet, en zet de opties voor herstel naast elkaar met hun afwegingen.

Hoe CFS-bandbreedtecontrole werkt

De Linux Completely Fair Scheduler (CFS) handhaaft CPU-limits via twee cgroup-parameters: een period en een quota. Kubernetes zet de period standaard op 100.000 microseconden (100 ms). De quota wordt afgeleid van de CPU-limit in de pod-spec.

Een CPU-limit van 500m vertaalt zich naar een quota van 50 ms per 100 ms period. De container mag maximaal 50 ms CPU-tijd verbruiken, opgeteld over al zijn threads, binnen elk period-venster. Zodra de quota op is, stopt de kernel alle threads tot de volgende period-grens. Ongebruikte quota rolt niet over. Er wordt niet vooruitgeleend.

Op cgroups v1 staan deze waarden in cpu.cfs_quota_us en cpu.cfs_period_us. Op cgroups v2 zijn ze gecombineerd in cpu.max (bijv. 50000 100000). Het enforcementmodel is identiek in beide versies.

Waarom laag gemiddeld CPU-verbruik niet betekent dat er geen throttling is

Dit is het kernmisverstand. Drie mechanismen verklaren het gat tussen dashboardgemiddelden en daadwerkelijke throttling.

Burstiness binnen het 100 ms-venster. Monitoringtools (Grafana, kubectl top, cloudconsoles) tonen doorgaans gemiddelden over 1 minuut of langer. Een workload die een CPU-burst van 60 ms concentreert in een enkele 100 ms period, put een quota van 50 ms uit en wordt gethrottled voor de resterende 40 ms. Over de minuut lijkt het gemiddelde CPU-verbruik op 5%. De throttle-ratio zegt 60%.

Multithreaded workloads versterken quota-uitputting. Quota wordt gedeeld over alle threads. Tien threads die elk 50 ms nodig hebben op aparte cores, met een 2 CPU-limit (200 ms quota), verbruiken die quota collectief in 20 ms wall time. Ze worden dan 80 ms bevroren. Gerapporteerd gemiddeld CPU-verbruik: precies 2.0. Doorlooptijd: meer dan verdrievoudigd.

Het scrape-interval vlakt pieken af tot niets. Een standaard Prometheus rate() over [5m] aggregeert 300 seconden aan data. Een workload die elke 500 ms 80 ms gethrottled wordt, toont een laag throttle-percentage, maar de p99-latency wordt gedomineerd door die pauzes. Gemiddelden liegen; percentile-latency vertelt de waarheid.

Welke workloads het hardst geraakt worden

Elke workload met korte, intense CPU-bursts is een kandidaat. De meest voorkomende:

  • JVM-applicaties. Garbage collection, vooral G1 GC, veroorzaakt CPU-pieken van 10 tot 50 ms. Bij een krappe CPU-limit worden de GC-threads zelf gethrottled, wat stop-the-world-pauzes verlengt, Old Gen-druk opbouwt en doorwerkt in service-timeouts.
  • Go-applicaties. De Go-runtime berekent GC-parallelisme als 25% van GOMAXPROCS. Onder throttling klopt die aanname niet meer, wat GC-druk veroorzaakt die tail-latency opblaast.
  • HTTP-API's bij request-pieken. Elke burst van inkomende requests concentreert CPU-verbruik in smalle vensters. De 100 ms-enforcementperiod creëert kunstmatige queuing.
  • JIT-compiled runtimes (Node.js V8, JVM JIT). Startup- en warmup-fases zijn CPU-intensieve bursts die er heel anders uitzien dan steady-state gemiddelden.

Hoe je throttling detecteert

Kubernetes toont throttling niet in kubectl describe pod of kubectl top. De stilte is het probleem: veel teams draaien wekenlang gethrottlede pods en diagnosticeren de symptomen als traag netwerk of database-contention.

Detectie vereist Prometheus (zie de Prometheus-monitoringtutorial). De belangrijkste metrics, aangeboden door cAdvisor:

Metric Wat het meet
container_cpu_cfs_throttled_periods_total Aantal periods waarin de container gethrottled werd
container_cpu_cfs_periods_total Totaal aantal verstreken enforcementperiods
container_cpu_cfs_throttled_seconds_total Cumulatieve CPU-tijd verloren aan throttling

De throttled-period-ratio is de standaard detectiequery:

100 * (
  sum by (namespace, pod, container) (
    rate(container_cpu_cfs_throttled_periods_total{container!="", container!="POD"}[5m])
  )
  /
  sum by (namespace, pod, container) (
    rate(container_cpu_cfs_periods_total{container!="", container!="POD"}[5m])
  )
)

Het kubernetes-mixin CPUThrottlingHigh-alert vuurt bij 25% gethrottlede periods. In de praktijk: alles boven 25% gedurende 15 minuten is het onderzoeken waard. Boven 50% heeft het vrijwel zeker impact op latency.

Om te correleren: vergelijk de throttle-ratio met de p99-latency van dezelfde service. Als beide tegelijk pieken, is throttling de oorzaak.

Cgroups v2: wat er veranderd is en wat niet

Kubernetes 1.25 maakte cgroups v2 GA (augustus 2022). De meeste moderne distributies (Ubuntu 21.10+, Debian 11+, RHEL 9+, Fedora 31+) gebruiken standaard v2. De vereisten: Linux-kernel 5.8+, containerd 1.4+ of CRI-O 1.20+, en de systemd cgroup-driver.

Wat er veranderd is: de filesystempaden zijn verschoven van /sys/fs/cgroup/cpu,cpuacct/kubepods/ naar /sys/fs/cgroup/kubepods/. Quota en period zijn samengevoegd in cpu.max. Het cpu.stat-bestand is overgegaan van nanoseconden naar microseconden. Pressure Stall Information (PSI) is beschikbaar geworden.

Wat niet veranderd is: de standaard period van 100 ms, het fundamentele throttling-gedrag, de Prometheus-metricnamen (cAdvisor abstraheert het v1/v2-verschil), en de CPU-limitsyntax in pod-specs. Als je tooling rechtstreeks cgroupbestanden leest, moet je die aanpassen. Als je op Prometheus vertrouwt, verandert er niks.

Herstelopties en afwegingen

Er is geen universeel juist antwoord. Elke optie ruilt iets anders in.

CPU-limits verhogen. De eenvoudigste fix. Als het throttle-percentage consistent boven 25% zit, verhoog dan de limit naar 2 tot 4x de request. Monitor de throttle-ratio na de wijziging; die zou sterk moeten dalen. Het risico: in multitenant-clusters concurreren hogere limits met andere pods om CPU-ruimte op de node. Maar limits beïnvloeden scheduling niet, alleen enforcement. Zie resource requests en limits voor de volledige mechanica.

CPU-limits helemaal verwijderen. De meest agressieve optie. De pod draait met Burstable QoS en kan alle beschikbare CPU op de node gebruiken. Voor latency-gevoelige services op dedicated of licht gedeelde nodes is dit vaak de juiste keuze. Op gedeelde multitenant-clusters kan een op hol geslagen pod buren uithongeren. Het breekt ook HPA die geconfigureerd is op CPU-gebruik als percentage van de limit, en verlaagt de pod van Guaranteed naar Burstable QoS, wat de eviction-prioriteit verandert.

De CFS-period aanpassen. De kubelet-instelling cpuCFSQuotaPeriod (standaard 100ms) kan verhoogd worden naar 200 ms of 500 ms. Een breder venster laat bursty workloads meer CPU-tijd opbouwen voor ze het plafond raken. De afweging: het is een node-brede instelling (niet per pod), het vermindert scheduling-fairness tussen concurrerende processen, en sommige managed Kubernetes-services bieden het niet aan.

CPU burst (experimenteel). Linux 5.14 introduceerde cpu.cfs_burst_us (v1) / cpu.max.burst (v2), een kredietsysteem dat ongebruikte quota van rustige periods opbouwt. Kubernetes biedt dit nog niet aan als native veld in de pod-spec (tracking issue). Het kan gezet worden via een DaemonSet die cgroupbestanden patcht, maar dat is nog niet geschikt voor generiek productiegebruik zonder maatwerk aan je infrastructuur.

VPA voor right-sizing. De Vertical Pod Autoscaler analyseert historisch CPU-verbruik en past requests automatisch aan. In Off-modus geeft hij aanbevelingen zonder ze door te voeren. VPA wijzigt standaard alleen requests; limits vereisen de expliciete RequestsAndLimits-modus. Het is een goed startpunt voor workloads waar je nog geen historische data hebt.

Wat CPU-throttling NIET is

Geen signaal van node-overbelasting. Een pod kan gethrottled worden op een node met 90% idle CPU. Throttling is per container, afgedwongen door de kernel tegen de cgroup-limit van die specifieke container.

Niet zichtbaar in standaard Kubernetes-tooling. kubectl describe pod toont geen throttling-events. kubectl top toont verbruik, niet de throttle-ratio. Zonder Prometheus is throttling onzichtbaar.

Niet hetzelfde als CPU-contention. Bij contention (meerdere pods die strijden om CPU) verdeelt de CFS tijd proportioneel aan requests. Dat is sharing, geen throttling. Throttling ontstaat wanneer een individuele container zijn eigen quota overschrijdt, ongeacht wat andere pods doen.

Niet op te lossen door meer nodes toe te voegen. Horizontaal schalen helpt niet als de individuele CPU-limit van de pod te laag is voor zijn burst-patroon. De limit is per container, niet per node.

Terugkerende server- of deploymentproblemen?

Ik help teams productie betrouwbaar maken met CI/CD, Kubernetes en cloud—zodat fixes blijven en deploys geen stress meer zijn.

Bekijk DevOps consultancy

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.