CPU Requests w Kubernetes: czym naprawdę są
- data
- kategoria
- Containers
- także w
- Capacity & Performance
- czytanie
- 3 min / 662 słów
W poprzednich częściach ustaliliśmy dwie rzeczy.
Po pierwsze, proces nie „posiada CPU” - scheduler Linuksa przydziela mu czas wykonania.
Po drugie, cgroups pozwalają grupować procesy i nadawać tym grupom względne wagi przy dostępie do procesora.
Dopiero teraz requests.cpu zaczyna mieć sens.
Najczęstszy błędny model wygląda tak:
requests.cpu: 500m
czyli:
kontener dostaje pół CPU
Nie.
CPU request jest przede wszystkim deklaracją zapotrzebowania. Kubernetes wykorzystuje tę deklarację do podjęcia decyzji, gdzie Pod może zostać uruchomiony. Później ta sama informacja wpływa również na względną wagę CPU workloadu.
To nadal nie jest limit.
Co oznacza 500m
CPU w Kubernetes jest wyrażane w jednostkach CPU:
1 CPU = 1000m
500m = 0.5 CPU
250m = 0.25 CPU
Na maszynie fizycznej 1 CPU odpowiada jednemu logicznemu CPU widzianemu przez system. Na VM będzie to odpowiednio vCPU. Kubernetes traktuje tę wartość jako absolutną ilość zasobu przy resource accounting.
Przykład:
resources:
requests:
cpu: 500m
oznacza więc:
deklaruję zapotrzebowanie na 0.5 CPU
Nie oznacza:
ogranicz mnie do 0.5 CPU
To rozróżnienie jest kluczowe.
Pierwsze zastosowanie requestu: kube-scheduler
Załóżmy node:
Allocatable CPU: 8
Są na nim już zaplanowane Pody:
Pod A: request 2 CPU
Pod B: request 3 CPU
Pod C: request 2 CPU
SUM = 7 CPU
Przychodzi kolejny:
resources:
requests:
cpu: "2"
Dla kube-schedulera rachunek wygląda tak:
7 + 2 = 9 CPU
Node posiada tylko:
8 CPU Allocatable
więc Pod nie może zostać tam zaplanowany.
Najważniejszy szczegół: scheduler nie musi patrzeć na aktualne zużycie CPU.
Node może w tej chwili zużywać:
0.8 CPU
i nadal zostać odrzucony.
Scheduler porównuje requesty, a nie chwilowe CPU usage. Kubernetes robi to celowo - request ma reprezentować capacity, którą trzeba uwzględnić na wypadek, gdy workload rzeczywiście będzie jej potrzebował.
Możemy więc powiedzieć:
request = rezerwacja w modelu kube-schedulera
Ale trzeba uważać ze słowem „rezerwacja”.
Scheduler rezerwuje capacity, kernel nie odkłada czasu CPU
Załóżmy:
resources:
requests:
cpu: 500m
Po uruchomieniu kontenera kernel nie tworzy czegoś takiego:
każda sekunda:
500 ms -> ten kontener
500 ms -> reszta systemu
Nie istnieje statycznie odłożona pula 500m, która czeka tylko na ten workload.
Jeżeli workload nic nie robi, CPU może zostać wykorzystany przez kogoś innego.
Jeżeli natomiast workload chce więcej CPU i procesor jest wolny, może wykorzystać więcej niż wynosi jego request.
Przykład:
request = 500m
usage = 1800m
jest całkowicie normalny.
Request nie jest sufitem.
Drugie zastosowanie requestu: względny udział w CPU
Tutaj wykorzystujemy mechanizm z poprzedniego artykułu.
Załóżmy dwa workloady:
A request = 250m
B request = 750m
Po stronie resource management wartości requestów są wykorzystywane do nadania odpowiednich CPU weights.
Historycznie, w cgroup v1, Kubernetes przeliczał request na cpu.shares:
cpu.shares = milliCPU * 1024 / 1000
czyli mniej więcej:
250m -> 256 shares
750m -> 768 shares
Daje to proporcję:
1 : 3
W świecie cgroup v2 odpowiednim mechanizmem jest cpu.weight; dokładna konwersja zależy dziś również od warstwy OCI runtime.
Najważniejsza jest jednak nie konkretna liczba, tylko mechanizm:
większy request
|
v
większa względna waga CPU
Co to daje w praktyce
Mamy jeden dostępny CPU.
Oba workloady są stale runnable:
A request = 250m
B request = 750m
Ponieważ oba cały czas chcą procesora, mamy contention.
W uproszczeniu ich udział może wyglądać tak:
A ≈ 25%
B ≈ 75%
Teraz B przestaje wykonywać pracę.
Zostaje tylko A.
Czy A nadal może używać maksymalnie 25% CPU?
Nie.
Może wykorzystać praktycznie:
A ≈ 100%
Request 250m nie ogranicza go do 25%.
To właśnie konsekwencja mechanizmu wagowego.
Request wpływa na podział wtedy, gdy istnieje konkurencja.
Dlaczego request nie jest „minimum CPU”
Często można spotkać uproszczenie:
request = minimum
limit = maximum
W przypadku CPU słowo „minimum” jest niebezpieczne.
requests.cpu: 500m nie oznacza, że proces w każdej sekundzie bezwarunkowo otrzyma dokładnie co najmniej:
500 ms CPU time
Request wpływa na resource accounting i priorytet podczas konkurencji, ale nadal mówimy o współdzielonym schedulerze.
Znacznie lepiej myśleć:
request =
deklarowane zapotrzebowanie
a nie:
request =
twardo zagwarantowana porcja procesora
Co jeśli request jest zbyt niski
Załóżmy node:
16 CPU
Uruchamiamy 16 Podów.
Każdy deklaruje:
request = 500m
Scheduler widzi:
16 × 0.5 CPU = 8 CPU
Czyli z jego perspektywy node posiada jeszcze dużo wolnej capacity.
Ale podczas ruchu produkcyjnego każdy Pod chce wykorzystać:
2 CPU
Realny demand wynosi:
16 × 2 CPU = 32 CPU
na node posiadającym:
16 CPU
Scheduler nie zrobił niczego niepoprawnego.
Dostał deklarację:
500m
i na jej podstawie wykonał placement.
To workload zużywa znacznie więcej, niż zadeklarowaliśmy.
Request jest więc nie tylko ustawieniem technicznym.
Jest wejściem do modelu capacity całego klastra.
Co jeśli request jest zbyt wysoki
Możliwy jest również odwrotny przypadek.
Aplikacja realnie zużywa:
200m
ale deklarujemy:
request = 2 CPU
Dla schedulera każda replika zajmuje wtedy:
2 CPU capacity
nawet jeśli prawie nigdy jej nie wykorzystuje.
Efekt może być prosty:
mniej Podów na node
więcej node'ów
gorszy bin packing
Czyli request jest kompromisem.
Zbyt niski powoduje, że scheduler przecenia dostępną capacity.
Zbyt wysoki powoduje, że jej niepotrzebnie nie wykorzystujemy.
Najważniejsze rozróżnienie
CPU request istnieje jednocześnie w dwóch światach.
Kubernetes
requests.cpu
|
v
resource accounting
|
v
decyzja, czy Pod zmieści się na node
Linux
requests.cpu
|
v
CPU shares / weight
|
v
względna pozycja workloadu podczas contention
To prowadzi do prostego modelu:
requests.cpu: 500m
nie znaczy:
masz pół rdzenia
Znaczy raczej:
Kubernetes:
policz mnie jako workload potrzebujący 0.5 CPU
Linux:
przy konkurencji uwzględnij wagę wynikającą
z mojego zadeklarowanego requestu
I najważniejsze:
request != limit
Workload z requestem 500m może zużywać znacznie więcej niż 500m, jeśli CPU jest dostępne.
Request opisuje deklarowane zapotrzebowanie i względny udział w konkurencji.
Nie definiuje maksymalnej ilości CPU, którą proces może wykorzystać.