Konrad Kowalski (rootsher)Principal Platform & Reliability Architect101001101111011011010111011101001111100111000111

Requests, Limits i QoS: jak Kubernetes klasyfikuje Pody

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

Mamy już trzy elementy układanki:

text
requests.cpu -> deklarowane zapotrzebowanie + względna waga
limits.cpu   -> maksymalny CPU bandwidth
contention   -> konkurencja o dostępne CPU

Kubernetes dokłada do tego jeszcze jedną warstwę:

text
QoS class

Każdy Pod otrzymuje jedną z trzech klas:

text
Guaranteed
Burstable
BestEffort

Nazwy brzmią tak, jakby bezpośrednio określały „jakość CPU”, ale to zbyt duże uproszczenie.

QoS class jest przede wszystkim klasyfikacją wynikającą z konfiguracji CPU i memory requests/limits. Kubernetes wykorzystuje ją między innymi przy podejmowaniu decyzji pod resource pressure.

W tym tekście interesuje nas wyłącznie to, jak klasy powstają i co oznaczają w kontekście CPU.


BestEffort

Najprostszy przypadek:

yaml
containers:
- name: app
  image: example/app

Brak:

text
requests.cpu
limits.cpu
requests.memory
limits.memory

Pod otrzymuje klasę:

text
BestEffort

Żeby Pod był BestEffort, żaden jego kontener nie może posiadać requestu ani limitu CPU lub memory.

To nie oznacza jednak:

text
Pod nie może używać CPU

Wręcz przeciwnie.

Jeżeli node ma wolną moc obliczeniową, BestEffort może ją wykorzystywać.

Na przykład:

text
node = 16 CPU

inne workloady używają = 4 CPU

BestEffort workload chce = 8 CPU

może realnie dostać te 8 CPU, jeśli nic innego mu w tym nie przeszkadza.

BestEffort nie oznacza więc:

text
mało CPU

Oznacza raczej:

text
brak zadeklarowanego CPU request
brak CPU limit

Burstable

Teraz ustawmy:

yaml
resources:
  requests:
    cpu: 500m

Nie ma limitu.

Pod nie spełnia kryteriów Guaranteed, ale posiada konfigurację zasobów.

Trafia więc do:

text
Burstable

Do klasy Burstable trafia Pod, który:

text
nie jest Guaranteed

oraz posiada przynajmniej jeden request lub limit CPU albo memory.

Przykład:

yaml
resources:
  requests:
    cpu: 500m
  limits:
    cpu: "2"

również będzie typowym przypadkiem Burstable, zakładając że pozostała konfiguracja Poda nie spełnia warunków Guaranteed.

W kontekście CPU oznacza to, że możemy mieć:

text
request = 500m
limit   = 2 CPU

czyli:

text
scheduler accounting = 0.5 CPU

relative CPU weight = wynikający z 500m

maksymalny bandwidth = 2 CPU

QoS nie zastępuje requestów i limitów.

Jest klasyfikacją zbudowaną na ich podstawie.


Guaranteed

Teraz konfiguracja:

yaml
resources:
  requests:
    cpu: "1"
    memory: 1Gi
  limits:
    cpu: "1"
    memory: 1Gi

Jeżeli każdy kontener w Podzie ma dodatnie requesty i limity CPU oraz memory, a dla każdego zasobu:

text
request == limit

Pod otrzymuje klasę:

text
Guaranteed

Czyli dla CPU:

text
requests.cpu = limits.cpu

ale samo to nie wystarcza.

Memory również musi spełniać odpowiednie warunki.

To ważny detal.

Konfiguracja:

yaml
resources:
  requests:
    cpu: "1"
  limits:
    cpu: "1"

nie oznacza automatycznie:

text
Guaranteed

jeżeli nie spełniliśmy również wymagań dotyczących memory.

QoS jest klasyfikacją całego Poda, nie wyłącznie CPU.


Guaranteed nie oznacza dedicated CPU

Nazwa może sugerować:

text
Guaranteed
=
mam zagwarantowany fizyczny core

Nie.

Pod:

yaml
resources:
  requests:
    cpu: "1"
    memory: 1Gi
  limits:
    cpu: "1"
    memory: 1Gi

może być Guaranteed, ale nadal działać na współdzielonych CPU.

Z punktu widzenia mechanizmów, które już znamy:

text
request = 1 CPU
limit   = 1 CPU

oznacza:

text
scheduler uwzględnia 1 CPU capacity

workload posiada CPU weight wynikający z requestu

workload posiada bandwidth limit odpowiadający 1 CPU

Nie oznacza:

text
CPU #7 należy tylko do tego Poda

Ekskluzywne CPU wymagają dodatkowego mechanizmu CPU Managera i odpowiedniej konfiguracji node'a. Kubernetes dokumentuje, że Pody Guaranteed mogą kwalifikować się do exclusive CPUs przy użyciu polityki static, ale sama klasa Guaranteed nie tworzy takiego przypisania.


Trzy Pody na jednym CPU

Załóżmy jeden CPU i trzy workloady.

Pod A - BestEffort

yaml
resources: {}

Pod B - Burstable

yaml
resources:
  requests:
    cpu: 500m

Pod C - Guaranteed

yaml
resources:
  requests:
    cpu: "1"
    memory: 1Gi
  limits:
    cpu: "1"
    memory: 1Gi

Nie należy myśleć:

text
Guaranteed zawsze pierwszy
Burstable potem
BestEffort na końcu

Linux scheduler nie interpretuje nazw Kubernetes QoS w ten sposób.

Runtime działa na mechanizmach cgroups.

Pod B ma CPU weight wynikający z requestu:

text
500m

Pod C ma weight wynikający z:

text
1 CPU

i dodatkowo posiada CPU bandwidth limit:

text
1 CPU

Pod A nie posiada zadeklarowanego CPU requestu, więc jego pozycja przy contention będzie odpowiednio słabsza zgodnie z konfiguracją cgroups wygenerowaną przez kubelet.

To nie jest klasyczny scheduler priority:

text
Guaranteed > Burstable > BestEffort

QoS class sama w sobie nie jest odpowiednikiem nice, scheduler priority ani kolejki priorytetowej CPU.


Najważniejsze: QoS to wynik konfiguracji

Dobry model mentalny:

text
requests + limits
       |
       v
Kubernetes wylicza QoS class
       |
       v
Guaranteed / Burstable / BestEffort

Nie odwrotnie.

Nie konfigurujemy bezpośrednio:

yaml
qosClass: Guaranteed

Kubernetes wylicza klasę na podstawie konfiguracji zasobów.

Można ją zobaczyć na przykład w:

bash
kubectl get pod <pod> -o jsonpath='{.status.qosClass}'

i otrzymać:

text
Guaranteed

albo:

text
Burstable

czy:

text
BestEffort

Klasa jest przypisywana przy utworzeniu Poda i pozostaje jego klasą przez lifetime Poda; obecne mechanizmy in-place resize nie pozwalają zmianą zasobów przejść do innej klasy QoS.


Jedna pułapka: limit bez jawnego requestu

Załóżmy:

yaml
resources:
  limits:
    cpu: "1"

bez:

yaml
requests:
  cpu:

Nie należy automatycznie zakładać:

text
CPU request = 0

Jeżeli request nie został podany, Kubernetes może ustawić request na wartość limitu. W efekcie kontener może skończyć z:

text
request = 1 CPU
limit   = 1 CPU

To ma znaczenie zarówno dla schedulera, jak i dla ustalania QoS.

Dlatego analizując działający Pod, warto patrzeć na finalną konfigurację zasobów, a nie wyłącznie na fragment manifestu, który pamiętamy z repozytorium.


QoS nie zmienia znaczenia requestu ani limitu

To ważne, żeby nie tworzyć kolejnej abstrakcji tam, gdzie jej nie ma.

Dla Burstable:

yaml
requests:
  cpu: 500m
limits:
  cpu: "2"

nadal:

text
500m

jest requestem, a:

text
2 CPU

jest bandwidth limitem.

Dla Guaranteed:

yaml
requests:
  cpu: "1"
limits:
  cpu: "1"

nadal są to te same dwa mechanizmy.

Po prostu ich wartości są równe.

QoS nie tworzy nowego sposobu schedulowania CPU.


Model mentalny

Najprościej:

text
BestEffort

request = brak
limit   = brak
text
Burstable

jakakolwiek konfiguracja CPU/memory,
która nie spełnia Guaranteed
text
Guaranteed

dla każdego kontenera:
CPU request = CPU limit > 0
Memory request = Memory limit > 0

W kontekście CPU najważniejsze jest jednak:

text
QoS class
!=
CPU priority class

oraz:

text
Guaranteed
!=
dedicated CPU

Request nadal odpowiada za deklarowaną capacity i względną wagę.

Limit nadal odpowiada za maksymalny CPU bandwidth.

QoS jest warstwą klasyfikacji Kubernetes zbudowaną na tych ustawieniach - nie trzecim, niezależnym mechanizmem przydzielania CPU.