Przejdź do treści
Kubernetes CPU throttling: dlaczego pody są throttlowane przy 40% CPU (CFS)Aleksander Roszig 7 czerwca 2026 | 8 min czytania

Kubernetes CPU throttling: dlaczego pody są throttlowane przy 40% CPU (CFS)

To trzecia część naszej serii o zarządzaniu zasobami w Kubernetes. W pierwszej części omówiłem request i limit CPU w Kubernetes, a w drugiej wyjaśniłem jak requesty i limity zachowują się w przypadku pamięci. Ten artykuł dotyczy zagadnienia, które dotyczy wielu projektów, ale jest często niezauważane przez brak znajomości mechanizmu działania limitów. CPU throttling - to powód, dla którego pod może wyglądać na całkowicie poprawnie działający na każdym dashboardzie, a mimo to nie działać optymalnie.

Objaw: pod wygląda dobrze, ale aplikacja nie działa tak, jak powinna

Jednym z najtrudniejszych do zdebugowania incydentów jest ten, w którym każdy wykres mówi, że wszystko jest w porządku.

Typowy przykład z data pipeline: usługa pobiera wiadomości i zapisuje je do bazy danych.

Pierwsze, co robi każdy, to otwiera wykres CPU. A wykres CPU pokazuje, że pod wykorzystuje 40% CPU. Patrzymy więc we wszystkie inne strony — sieć, baza danych, garbage collection — podczas gdy jedyna metryka, która naprawdę ma znaczenie, pozostaje bez nadzoru.

To throttling CFS: mechanizm jądra Linuksa, który Kubernetes używa do egzekwowania limitów CPU. Prawie żaden domyślny dashboard go nie pokazuje.

Czym jest CPU throttling w Kubernetes?

CPU throttling to sytuacja gdy kontener wyczerpie kwotę swojego limitu CPU w obrębie pojedynczego okna planowania schedulera. Scheduler Linuksa rozdziela czas CPU w stałych okresach — domyślnie 100 ms w CFS (cpu.cfs_period_us w cgroups v1; pole okresu w cpu.max w cgroups v2). W każdym okresie cgroupa może zużyć tylko swój limit czasu CPU. Gdy zużycie grupy osiągnie ten udział w danym okresie, wszystkie zadania w ramach tej cgroupy zostają throttlowane — wstrzymane i niedopuszczone do wykonywania aż do początku kolejnego okresu.

W cgroups działają dwa mechanizmy kontrolera CPU:

  • Model wagowy (ang. weight model) stoi za requestem CPU. Decyduje o proporcjonalnym udziale CPU, jaki dostaje kontener w sytuacji rywalizacji o zasoby.
  • Model przepustowości (ang. bandwidth model) stoi za limitem CPU. To absolutny pułap egzekwowany w każdym okresie i to właśnie on powoduje throttling.

Oba mechanizmy szczegółowo opisuję we wpisie o request i limit.

Requesty nigdy nie powodują throttlingu. Limity — tak.

Jak naprawdę działa throttling CFS

O tym, czy Twój kontener zostanie throttlowany, decydują trzy liczby:

  1. Limit CPU — załóżmy 2000m.
  2. Okres planowania CFS — domyślnie 100 ms w CFS (cpu.cfs_period_us w cgroups v1; drugie pole cpu.max w cgroups v2).
  3. Liczba rdzeni CPU na hoście — tutaj 4.

Kubernetes przelicza limit na kwotę CFS:

quota_µs = (milliCPU * period_µs) / milliCPUToCPU(1000)

dla limitu 2000m i okresu 100 ms (100000 µs):
quota_µs = 2000 * 100000 / 1000 = 200000 µs   // 200 ms czasu CPU na okres

Przy limicie 2000m i okresie 100 ms kontener dostaje 200 ms czasu CPU na okres. Kontener może wydać te 200 ms na każdym rdzeniu noda jednocześnie.

Wyobraź sobie teraz usługę typu HTTP na węźle 4-rdzeniowym. Pojedyncze, zasobożerne żądanie rozkłada się na wszystkie cztery rdzenie. W zaledwie 50 ms czasu rzeczywistego, pracując równolegle na 4 rdzeniach, zużywa 4 * 50 ms = 200 ms czasu CPU. Cały budżet okresu znika w 50 ms czasu rzeczywistego — w połowie okresu CFS.

Przez pozostałe 50 ms tego okresu kontener jest zamrożony. Każde żądanie, które trafi w to okno, czeka — nie dlatego, że CPU jest zajęte, ale dlatego, że jądro wstrzymało kontener aż do początku kolejnego okresu.

Jeśli Twoje obciążenie wygląda jak burst, idle, idle, idle, burst — czyli dokładnie tak, jak wyglądają obciążenia sterowane żądaniami i zdarzeniami — to skoki są throttlowane, podczas gdy wykres CPU pokazuje spokojne 40%.

I właśnie taka sytuacja powoduje że request z krótkim deadlinem kończy się błędem context deadline exceeded. Praca nie zwolniła. Została wstrzymana przez jądro, a deadline wygasł w trakcie pauzy którą wprowadził scheduler.

cgroups v1 vs v2: gdzie to odczytać

Okres 100 ms i zachowanie throttlingu są identyczne w cgroups v1 i v2 — zmienia się to, gdzie to odczytasz.

Pojęciecgroups v1cgroups v2
Limit CPU (kwota)cpu.cfs_quota_us + cpu.cfs_period_uscpu.max (<kwota> <okres>)
Request CPU (waga)cpu.shares (domyślnie 1024)cpu.weight (domyślnie 100)
Liczniki throttlingucpu.statcpu.stat

Większość nowych dystrybucji — Amazon Linux 2023, Ubuntu 22.04+, RHEL 9+ — domyślnie używa cgroups v2, więc pojedynczy plik cpu.max zawiera zarówno quotę, jak i okres. Plik cpu.stat istnieje w obu wersjach i raportuje te same liczniki throttlingu.

Jak sprawdzić CPU throttling w Kubernetes

Jądro zapisuje to dla każdego kontenera, który ma limit CPU, w /sys/fs/cgroup/cpu.stat:

kubectl exec <pod> -- cat /sys/fs/cgroup/cpu.stat
usage_usec 49823715
user_usec 41205893
system_usec 8617822
nr_periods 300
nr_throttled 30
throttled_usec 6142188
nr_bursts 0
burst_usec 0

Dwie linie, które mają znaczenie:

  • nr_throttled — ile okresów planowania zakończyło się throttlingiem kontenera.
  • throttled_usec — łączna liczba mikrosekund, jaką kontener spędził zamrożony.

Jeśli te wartości rosną w czasie, masz odpowiedź — niezależnie od tego, co twierdzi wykres średniego CPU. W powyższym przykładzie 30 z 300 okresów (10%) zostało throttlowanych. Dla usługi wrażliwej na opóźnienia to już wystarczy, by zrujnować p99.

Jeśli korzystasz z Prometheusa i cAdvisora, ten sam sygnał jest dostępny jako metryka container_cpu_cfs_throttled_periods_total oraz container_cpu_cfs_throttled_seconds_total (podziel liczbę throttlowanych okresów przez container_cpu_cfs_periods_total, by uzyskać współczynnik throttlingu, na którym możesz oprzeć alert).

Co obserwować zamiast średniego CPU

cpu.stat to bezpośrednie sprawdzenie. Dla pełnego obrazu obserwuj cztery sygnały razem:

SygnałGdzieWykrywa
Throttling CFScpu.statnr_throttled, throttled_usecKontener trafił na własny budżet CPU cgroup
Kernel PSIcpu.pressure (cgroup)Nasycenie z rywalizacji z innymi obciążeniami, nawet pod limitem
Steal time%st w topHypervisor oddał slot Twojego vCPU innej VM
Opóźnienia schedulera appGo: /sched/latencies:secondsGoroutines czekające na wykonanie — throttling na poziomie języka Go

Kilka praktycznych uwag:

  • PSI (cpu.pressure) - raportuje procent czasu rzeczywistego, w którym zadanie było gotowe do wykonania, ale nie wykonywane — pokazuje więc nasycenie powodowane przez głośnych sąsiadów (ang. noisy neighbors) na hoście, a nie tylko wyczerpanie Twoich własnych limitów.
  • Steal time jest niewidoczny dla cgroups. - na maszynie wirtualnej rywalizacja na poziomie hypervisora nigdy nie pojawia się w cpu.stat. Widzisz ją tylko jako %st w top: Twój kod jest gotowy, ale fizyczne CPU jest zajęte przez inną VM.
  • Wykrywanie throttling po stronie aplikacji - aplikacja okresowo pyta, czy milisekunda nadal trwa milisekundę; jeśli czas rzeczywisty rozciąga się względem czasu CPU, coś ponad nią — throttling CFS, steal time, proces wykorzystujący wspólny budżet — ją throttluje. Od Go 1.25 GOMAXPROCS domyślnie uwzględnia cgroup, co ogranicza (choć nie eliminuje) problem — nie pomoże, gdy inny proces w tym samym kontenerze wykorzystuje wspólny budżet czasu.

Co z tym zrobić

Nie ma jednej odpowiedzi, ale drzewo decyzyjne jest krótkie:

  1. Potwierdź, że to throttling, a nie nasycenie. Rosnący cpu.stat + niskie średnie CPU = throttling. Wysokie średnie CPU + wysokie PSI = prawdziwe nasycenie, które jest problemem skalowania, a nie limitu.
  2. Podnieś lub przemyśl limit CPU. Jeśli skok faktycznie potrzebuje więcej czasu wykonywania, niż pozwala budżet, limit jest zbyt ciasny. Wiele zespołów działa z ustawionymi requestami CPU i usuniętymi limitami CPU (limity pamięci zostają), pozwalając obciążeniom skokowym korzystać z wolnej pojemności noda, podczas gdy requesty wciąż gwarantują bazowy poziom. To kompromis requesty vs limity w praktyce.
  3. Zachowaj limity tam, gdzie wymaga tego zgodność z frameworkami — i monitoruj throttling, byś znał jego koszt.

Czy w ogóle ustawiać limity CPU?

Jak zwykle — to zależy, a uczciwa odpowiedź jest taka sama, jak ta, którą daliśmy dla requestów w części pierwszej. Zawsze ustawiaj requesty. Nie kosztują nic w kwestii throttlingu, a dają schedulerowi to, czego potrzebuje, by rozmieścić poda i zagwarantować sprawiedliwy udział w warunkach rywalizacji.

Limity CPU to część dyskusyjna. Limit ogranicza Twoją zdolność do szybkiego przetwarzania nawet wtedy, gdy node ma wolne cykle, których nikt inny nie używa — co jest przeciwieństwem tego, co miały zapewnić chmura, autoskalowanie i wydajna infrastruktura. Dla usługi wrażliwej na opóźnienia w klastrze, który kontrolujesz i monitorujesz, limit często przynosi więcej szkody niż pożytku.

Są jednak realne scenariusze, w których limit to właściwy wybór:

  • Klastry multi-tenant — kilka zespołów lub klientów współdzieli jeden klaster i potrzebuje izolacji.
  • Niezaufane obciążenia — nie kontrolujesz w pełni kodu działającego w kontenerach.
  • Ochrona przed nielimitowanym obciążeniem — zabezpieczenie przed nieskończonymi pętlami lub koparkami kryptowalut.
  • Kontrola kosztów — twardy budżet do celów planowania, rozliczeń lub księgowości.
  • Platformy zarządzane — usługi takie jak AWS Fargate wymagają ustawienia CPU (i pamięci) na poziomie zadania.

W tych przypadkach limit zapewnia izolację i przewidywalność, a to jest warte kosztu throttlingu.

Podsumowanie

Gdy ustawiasz limit CPU na kontenerze, nie ograniczasz liczby rdzeni, których może dotknąć. Dajesz mu budżet czasowy na każdy okres 100 ms. Pojedynczy skok użycia może wyczerpać okres i zamrozić kontener na resztę tego okresu — podczas gdy uśredniony wykres CPU pozostaje spokojny, a opóźnienie p99 rośnie. To pułapka CPU throttlingu w Kubernetesie: dashboard wygląda zdrowo dokładnie do momentu, w którym deadline zostaje przekroczony.

Co obserwować zamiast średniego CPU:

  • throttling cgroup: nr_throttled i throttled_usec w cpu.stat (lub container_cpu_cfs_throttled_* w Prometheusie)
  • kernel PSI: cpu.pressure dla nasycenia pod limitem
  • steal time hypervisora przy użyciu VM: %st w top
  • sygnały throttlingu aplikacji (w Go /sched/latencies:seconds)

Jeśli uruchamiasz w Kubernetes obciążenia wrażliwe na opóźnienia — zwłaszcza data pipelines lub dane szeregów czasowych (ang. time series) — i chcesz observability, które ujawnia throttling to w RoszigIT projektujemy i wdrażamy stack Kubernetes + Grafana. Skontaktuj się, jeśli potrzebujesz drugiej opinii lub praktycznej pomocy przy architekturze.