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:
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:
resources:
limits:
cpu: "1"
Naturalna interpretacja:
kontener dostaje jeden CPU
To nie jest dokładnie to, co się dzieje.
Kernel nie musi przypisać kontenera do:
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:
możesz zużyć odpowiednik jednego CPU
niż:
możesz wykonywać się tylko na jednym konkretnym CPU
To istotna różnica.
Jak limits.cpu zamienia się w quota
Kubernetesowy limit:
resources:
limits:
cpu: 500m
nie trafia do kernela jako wartość 500m.
Kernel operuje czasem CPU.
W cgroup v2 mechanizm limitowania CPU znajduje się w:
cpu.max
Format wygląda tak:
MAX PERIOD
Obie wartości, jeśli MAX jest liczbą, są wyrażone w mikrosekundach.
Relację można uprościć do:
quota = CPU limit × period
Jeżeli period wynosi:
100 ms
to dla:
limit = 0.5 CPU
dostajemy:
quota = 0.5 × 100 ms
= 50 ms CPU time
Czyli:
limit = 500m
period = 100 ms
quota = 50 ms
W cpu.max będzie to reprezentowane jako:
50000 100000
czyli:
quota = 50 000 µs
period = 100 000 µs
Ogólna zależność jest więc bardzo prosta:
CPU limit = quota / period
oraz odwrotnie:
quota = CPU limit × period
Przy periodzie 100 ms:
limit quota
250m -> 25 ms
500m -> 50 ms
1 CPU -> 100 ms
2 CPU -> 200 ms
4 CPU -> 400 ms
Na pierwszy rzut oka:
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:
CPU 0: 100 ms
CPU 1: 100 ms
zużywają łącznie:
200 ms CPU time
mimo że upłynęło tylko:
100 ms wall-clock time
Dlatego limit 2 CPU może być reprezentowany jako:
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:
max 100000
max jest specjalną wartością oznaczającą:
brak maksymalnej quota
Czyli CPU bandwidth controller nie narzuca tej cgroup twardego limitu CPU time.
Porównajmy:
50000 100000
oznacza:
quota = 50 ms
period = 100 ms
limit = 0.5 CPU
natomiast:
max 100000
oznacza:
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:
1 CPU
i okres:
100 ms
Mamy więc:
quota = 100 ms CPU time
Kontener posiada jeden CPU-bound thread.
Może wyglądać to tak:
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:
CPU 0: [ T1 ]
CPU 1: [ T2 ]
CPU 2: [ T3 ]
CPU 3: [ T4 ]
każda milisekunda wall-clock time może kosztować około:
4 ms CPU time
Czyli quota:
100 ms
może zostać zużyta po około:
25 ms wall-clock time
Wtedy:
0 ms 25 ms 100 ms
|-----------|-------------------------------|
running throttled
Cztery thready:
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:
limit = 1 CPU
nie oznacza:
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:
period = 100 ms
quota = 50 ms
Workload wykorzystuje całe:
50 ms CPU time
przed końcem okresu.
W uproszczeniu:
|---------- 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:
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:
32 CPU
Aktualne wykorzystanie:
3 CPU
Czyli ogromna część maszyny jest wolna.
Kontener ma jednak:
limits:
cpu: "1"
Jeżeli wykorzysta swój CPU bandwidth, kernel może go throttlować mimo tego, że obok znajduje się:
29 wolnych CPU
Dlaczego?
Bo limit nie opisuje aktualnego braku zasobu.
Opisuje maksymalny dozwolony bandwidth tej cgroup.
To fundamentalna różnica między:
CPU contention
a:
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:
resources:
limits:
cpu: 500m
Przy periodzie 100 ms:
quota = 50 ms
period = 100 ms
Workload może więc średnio konsumować:
50 ms CPU time / 100 ms wall time
=
0.5 CPU
Nie oznacza to jednak koniecznie:
RUN 50 ms
WAIT 50 ms
Kernel rozlicza CPU time całej grupy.
Może więc być:
1 thread × 50 ms
albo:
2 threads × 25 ms
albo:
4 threads × 12.5 ms
Sumarycznie w każdym przypadku:
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:
limit = 1 CPU
i ośmioma workerami:
T1
T2
T3
T4
T5
T6
T7
T8
Nie dostają one po:
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:
cpu.stat
W cgroup v2 możemy zobaczyć między innymi:
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:
usage_usec 245231843
user_usec 220012391
system_usec 25219452
nr_periods 10000
nr_throttled 3200
throttled_usec 48231000
Sam fakt, że:
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:
requests.cpu
|
v
deklarowane zapotrzebowanie
+
względna waga CPU
Limit:
limits.cpu
|
v
maksymalny CPU bandwidth
|
v
quota / period
|
v
throttling po wykorzystaniu budżetu
Najważniejsze rozróżnienie:
request:
jak traktować workload przy planowaniu
i konkurencji o CPU
limit:
ile CPU time maksymalnie wolno
workloadowi skonsumować
Model mentalny
Dla:
resources:
limits:
cpu: "1"
nie myśl:
kontener ma jeden rdzeń
Myśl:
kontener posiada CPU-time budget,
rozliczany w kolejnych okresach
Jeżeli budżet zostanie wykorzystany:
task nadal ma pracę
|
v
task jest runnable
|
v
quota wyczerpana
|
v
THROTTLED
A jeśli:
cpu.max = max 100000
to nie istnieje skończona quota, którą workload może wyczerpać.
I to jest istota CPU limitu:
CPU limit nie określa miejsca wykonania.
CPU limit określa maksymalny CPU bandwidth.