Konrad Kowalski (rootsher)Principal Platform & Reliability Architect110000000100001011011101010101001101011000111011

CPU Limits i throttling: co naprawdę robi limit CPU

data
kategoria
Containers
także w
Capacity & Performance
czytanie
4 min / 768 słów

requests.cpu mówi Kubernetesowi, ile CPU workload deklaruje jako potrzebne.

limits.cpu odpowiada na zupełnie inne pytanie:

text
ile CPU workload może maksymalnie wykorzystać

Na Linuxie CPU limit jest egzekwowany przez cgroups i CPU throttling. Kubernetes nie „spowalnia” procesu sam. Kubelet i runtime konfigurują cgroup, a kernel pilnuje, żeby workload nie przekroczył przydzielonego CPU bandwidth.

Najważniejsze jest jednak to, jak kernel rozumie limit.


limits.cpu: 1 nie oznacza jednego rdzenia

Załóżmy:

yaml
resources:
  limits:
    cpu: "1"

Naturalna interpretacja:

text
kontener dostaje jeden CPU

To nie jest dokładnie to, co się dzieje.

Kernel nie musi przypisać kontenera do:

text
CPU 4

i trzymać go tam na stałe.

Limit definiuje maksymalną ilość CPU time, którą cgroup może skonsumować w określonym przedziale czasu.

Czyli bardziej:

text
możesz zużyć odpowiednik jednego CPU

niż:

text
możesz wykonywać się tylko na jednym konkretnym CPU

To istotna różnica.


Jak limits.cpu zamienia się w quota

Kubernetesowy limit:

yaml
resources:
  limits:
    cpu: 500m

nie trafia do kernela jako wartość 500m.

Kernel operuje czasem CPU.

W cgroup v2 mechanizm limitowania CPU znajduje się w:

text
cpu.max

Format wygląda tak:

text
MAX PERIOD

Obie wartości, jeśli MAX jest liczbą, są wyrażone w mikrosekundach.

Relację można uprościć do:

text
quota = CPU limit × period

Jeżeli period wynosi:

text
100 ms

to dla:

text
limit = 0.5 CPU

dostajemy:

text
quota = 0.5 × 100 ms
      = 50 ms CPU time

Czyli:

text
limit  = 500m
period = 100 ms
quota  = 50 ms

W cpu.max będzie to reprezentowane jako:

text
50000 100000

czyli:

text
quota  = 50 000 µs
period = 100 000 µs

Ogólna zależność jest więc bardzo prosta:

text
CPU limit = quota / period

oraz odwrotnie:

text
quota = CPU limit × period

Przy periodzie 100 ms:

text
limit      quota

250m   ->   25 ms
500m   ->   50 ms
1 CPU  ->  100 ms
2 CPU  ->  200 ms
4 CPU  ->  400 ms

Na pierwszy rzut oka:

text
quota  = 200 ms
period = 100 ms

może wyglądać dziwnie.

Jak można zużyć 200 ms czasu w ciągu 100 ms?

Przez równoległość.

Dwa thready wykonywane jednocześnie przez 100 ms:

text
CPU 0: 100 ms
CPU 1: 100 ms

zużywają łącznie:

text
200 ms CPU time

mimo że upłynęło tylko:

text
100 ms wall-clock time

Dlatego limit 2 CPU może być reprezentowany jako:

text
quota  = 200 ms
period = 100 ms

limit jest więc abstrakcją Kubernetes API.

quota i period są mechanizmem, za pomocą którego kernel tę wartość egzekwuje.


A co, jeśli CPU limitu nie ma?

Quota nie musi być skończona.

W cgroup v2 cpu.max może wyglądać tak:

text
max 100000

max jest specjalną wartością oznaczającą:

text
brak maksymalnej quota

Czyli CPU bandwidth controller nie narzuca tej cgroup twardego limitu CPU time.

Porównajmy:

text
50000 100000

oznacza:

text
quota  = 50 ms
period = 100 ms
limit  = 0.5 CPU

natomiast:

text
max 100000

oznacza:

text
brak CPU bandwidth limitu

W tym drugim przypadku workload nie zostanie throttlowany z powodu wyczerpania quota w cpu.max.

Może oczywiście nie dostać CPU z innych powodów - na przykład dlatego, że inne runnable taski również chcą się wykonywać - ale nie wynika to z CPU limitu.


CPU time może być zużywane równolegle

To najważniejszy detal całego mechanizmu.

Załóżmy limit:

text
1 CPU

i okres:

text
100 ms

Mamy więc:

text
quota = 100 ms CPU time

Kontener posiada jeden CPU-bound thread.

Może wyglądać to tak:

text
0 ms ----------------------------- 100 ms

CPU:
[========== 100 ms ===============]

Quota wystarcza na cały period.

Ale teraz aplikacja ma cztery runnable threads.

Jeżeli scheduler wykona je równolegle na czterech CPU:

text
CPU 0: [ T1 ]
CPU 1: [ T2 ]
CPU 2: [ T3 ]
CPU 3: [ T4 ]

każda milisekunda wall-clock time może kosztować około:

text
4 ms CPU time

Czyli quota:

text
100 ms

może zostać zużyta po około:

text
25 ms wall-clock time

Wtedy:

text
0 ms       25 ms                         100 ms
|-----------|-------------------------------|
   running             throttled

Cztery thready:

text
4 × 25 ms = 100 ms CPU time

zużyły cały budżet odpowiadający limitowi 1 CPU.

To właśnie powód, dla którego:

text
limit = 1 CPU

nie oznacza:

text
jeden thread działa sobie spokojnie przez cały czas

Wielowątkowy workload może wykorzystać cały budżet bardzo szybko.


Co dzieje się po wykorzystaniu quota

Jeżeli cgroup zużyje dostępny bandwidth przed końcem periodu, kernel ją throttluje.

To znaczy, że runnable taski należące do tej cgroup nie mogą dalej wykonywać się na CPU, mimo że mają pracę.

Czekają do momentu, kiedy bandwidth ponownie stanie się dostępny.

Przykład:

text
period = 100 ms
quota  = 50 ms

Workload wykorzystuje całe:

text
50 ms CPU time

przed końcem okresu.

W uproszczeniu:

text
|---------- period: 100 ms ----------|

| running 50 ms | throttled 50 ms   |

Na początku kolejnego okresu workload ponownie otrzymuje CPU bandwidth.

Cykl może więc wyglądać tak:

text
RUN -> THROTTLE -> RUN -> THROTTLE

CPU limit jest twardym ograniczeniem bandwidth, egzekwowanym przez kernel po wykorzystaniu dostępnego budżetu.


Wolne CPU na node niczego nie zmienia

To często zaskakuje.

Załóżmy node:

text
32 CPU

Aktualne wykorzystanie:

text
3 CPU

Czyli ogromna część maszyny jest wolna.

Kontener ma jednak:

yaml
limits:
  cpu: "1"

Jeżeli wykorzysta swój CPU bandwidth, kernel może go throttlować mimo tego, że obok znajduje się:

text
29 wolnych CPU

Dlaczego?

Bo limit nie opisuje aktualnego braku zasobu.

Opisuje maksymalny dozwolony bandwidth tej cgroup.

To fundamentalna różnica między:

text
CPU contention

a:

text
CPU throttling

Przy throttlingu CPU może fizycznie być dostępne.

Proces nie może go wykorzystać, ponieważ wyczerpał swój budżet wynikający z limitu.


Limit 500m

Załóżmy:

yaml
resources:
  limits:
    cpu: 500m

Przy periodzie 100 ms:

text
quota  = 50 ms
period = 100 ms

Workload może więc średnio konsumować:

text
50 ms CPU time / 100 ms wall time
=
0.5 CPU

Nie oznacza to jednak koniecznie:

text
RUN 50 ms
WAIT 50 ms

Kernel rozlicza CPU time całej grupy.

Może więc być:

text
1 thread × 50 ms

albo:

text
2 threads × 25 ms

albo:

text
4 threads × 12.5 ms

Sumarycznie w każdym przypadku:

text
50 ms CPU time

Sposób zużycia quota zależy od tego, kiedy taski są runnable i ile z nich wykonuje się równolegle.


Limit jest liczony dla cgroup, nie pojedynczego threada

Załóżmy kontener z:

text
limit = 1 CPU

i ośmioma workerami:

text
T1
T2
T3
T4
T5
T6
T7
T8

Nie dostają one po:

text
1 CPU każdy

Współdzielą budżet swojej cgroup.

Jeżeli wiele z nich wykonuje się równolegle, razem zużywają quota.

To ważne dla aplikacji wielowątkowych: limit dotyczy całego workloadu objętego daną cgroup, a nie każdego threada osobno.


Jak zobaczyć throttling

cgroup udostępnia statystyki w:

text
cpu.stat

W cgroup v2 możemy zobaczyć między innymi:

text
nr_periods
nr_throttled
throttled_usec

nr_periods mówi, ile okresów enforcementu upłynęło.

nr_throttled mówi, w ilu okresach grupa została throttlowana.

throttled_usec pokazuje skumulowany czas throttlingu.

Przykładowo:

text
usage_usec     245231843
user_usec      220012391
system_usec     25219452

nr_periods          10000
nr_throttled         3200
throttled_usec   48231000

Sam fakt, że:

text
nr_throttled > 0

nie oznacza jeszcze problemu.

Informuje jednak, że workload rzeczywiście dochodził do skonfigurowanego CPU bandwidth limitu.


Request i limit robią dwie różne rzeczy

Po poprzednim artykule możemy już zestawić te mechanizmy bez mieszania ich znaczenia.

Request:

text
requests.cpu
     |
     v
deklarowane zapotrzebowanie
+
względna waga CPU

Limit:

text
limits.cpu
     |
     v
maksymalny CPU bandwidth
     |
     v
quota / period
     |
     v
throttling po wykorzystaniu budżetu

Najważniejsze rozróżnienie:

text
request:
jak traktować workload przy planowaniu
i konkurencji o CPU

limit:
ile CPU time maksymalnie wolno
workloadowi skonsumować

Model mentalny

Dla:

yaml
resources:
  limits:
    cpu: "1"

nie myśl:

text
kontener ma jeden rdzeń

Myśl:

text
kontener posiada CPU-time budget,
rozliczany w kolejnych okresach

Jeżeli budżet zostanie wykorzystany:

text
task nadal ma pracę
        |
        v
task jest runnable
        |
        v
quota wyczerpana
        |
        v
THROTTLED

A jeśli:

text
cpu.max = max 100000

to nie istnieje skończona quota, którą workload może wyczerpać.

I to jest istota CPU limitu:

text
CPU limit nie określa miejsca wykonania.

CPU limit określa maksymalny CPU bandwidth.