Konrad Kowalski (rootsher)Principal Platform & Reliability Architect010000111000111000101010100100010111001101101000

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:

text
requests.cpu: 500m

czyli:

text
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:

text
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:

yaml
resources:
  requests:
    cpu: 500m

oznacza więc:

text
deklaruję zapotrzebowanie na 0.5 CPU

Nie oznacza:

text
ogranicz mnie do 0.5 CPU

To rozróżnienie jest kluczowe.


Pierwsze zastosowanie requestu: kube-scheduler

Załóżmy node:

text
Allocatable CPU: 8

Są na nim już zaplanowane Pody:

text
Pod A: request 2 CPU
Pod B: request 3 CPU
Pod C: request 2 CPU

SUM = 7 CPU

Przychodzi kolejny:

yaml
resources:
  requests:
    cpu: "2"

Dla kube-schedulera rachunek wygląda tak:

text
7 + 2 = 9 CPU

Node posiada tylko:

text
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ć:

text
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ć:

text
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:

yaml
resources:
  requests:
    cpu: 500m

Po uruchomieniu kontenera kernel nie tworzy czegoś takiego:

text
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:

text
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:

text
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:

text
cpu.shares = milliCPU * 1024 / 1000

czyli mniej więcej:

text
250m -> 256 shares
750m -> 768 shares

Daje to proporcję:

text
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:

text
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:

text
A request = 250m
B request = 750m

Ponieważ oba cały czas chcą procesora, mamy contention.

W uproszczeniu ich udział może wyglądać tak:

text
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:

text
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:

text
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:

text
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ć:

text
request =
deklarowane zapotrzebowanie

a nie:

text
request =
twardo zagwarantowana porcja procesora

Co jeśli request jest zbyt niski

Załóżmy node:

text
16 CPU

Uruchamiamy 16 Podów.

Każdy deklaruje:

text
request = 500m

Scheduler widzi:

text
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ć:

text
2 CPU

Realny demand wynosi:

text
16 × 2 CPU = 32 CPU

na node posiadającym:

text
16 CPU

Scheduler nie zrobił niczego niepoprawnego.

Dostał deklarację:

text
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:

text
200m

ale deklarujemy:

text
request = 2 CPU

Dla schedulera każda replika zajmuje wtedy:

text
2 CPU capacity

nawet jeśli prawie nigdy jej nie wykorzystuje.

Efekt może być prosty:

text
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

text
requests.cpu
    |
    v
resource accounting
    |
    v
decyzja, czy Pod zmieści się na node

Linux

text
requests.cpu
    |
    v
CPU shares / weight
    |
    v
względna pozycja workloadu podczas contention

To prowadzi do prostego modelu:

text
requests.cpu: 500m

nie znaczy:

text
masz pół rdzenia

Znaczy raczej:

text
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:

text
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ć.