Dlaczego 1 CPU nie oznacza jednego rdzenia
- data
- kategoria
- Containers
- także w
- Computer Science
- czytanie
- 4 min / 721 słów
W Kubernetes bardzo łatwo zbudować sobie taki model:
resources:
requests:
cpu: "1"
limits:
cpu: "1"
czyli:
ten kontener działa na jednym rdzeniu
To nie jest poprawne.
1 CPU w Kubernetes opisuje ilość CPU capacity, a nie konkretny procesor.
Jeżeli workload ma limit 1 CPU, kernel ogranicza jego łączny CPU time do odpowiedniego bandwidthu. Nie oznacza to jednak, że wszystkie jego taski muszą wykonywać się na jednym konkretnym logical CPU.
To są dwa różne problemy:
ile CPU time mogę zużyć?
oraz:
na których CPU mogę się wykonywać?
CPU time i CPU placement to osobne mechanizmy
Załóżmy node:
CPU 0
CPU 1
CPU 2
CPU 3
CPU 4
CPU 5
CPU 6
CPU 7
Kontener ma:
limit = 1 CPU
Nie oznacza to:
container -> CPU 3
Może wyglądać tak:
t0: thread A -> CPU 2
t1: thread A -> CPU 5
t2: thread A -> CPU 1
t3: thread A -> CPU 7
Scheduler Linuksa może migrować task pomiędzy logical CPUs.
Limit nadal pozostaje:
1 CPU
bo dotyczy sumarycznego CPU time, a nie identyfikatora procesora.
Wielowątkowa aplikacja jeszcze lepiej pokazuje różnicę
Załóżmy:
limit = 1 CPU
ale aplikacja ma cztery runnable thready:
T1
T2
T3
T4
Kernel może przez pewien czas wykonywać je równolegle:
CPU 0 -> T1
CPU 1 -> T2
CPU 2 -> T3
CPU 3 -> T4
Jeżeli wszystkie cztery wykonują się przez:
25 ms
to workload zużył:
4 × 25 ms
=
100 ms CPU time
Czyli przy quota odpowiadającej 1 CPU i periodzie 100 ms mógł właśnie wykorzystać cały swój budżet.
To pokazuje fundamentalną rzecz:
limit = 1 CPU
nie znaczy:
maksymalnie jeden thread może być running
Znaczy:
łączny CPU bandwidth odpowiada jednemu CPU
Logical CPU
Kiedy Kubernetes mówi:
1 CPU
operuje na jednostce odpowiadającej CPU widocznemu dla systemu operacyjnego.
Na typowej maszynie x86 z SMT system może widzieć na przykład:
16 physical cores
32 logical CPUs
Linux ponumeruje je mniej więcej:
CPU 0
CPU 1
...
CPU 31
Scheduler operuje właśnie na tych logical CPUs.
Dlatego:
1 CPU
w Kubernetes nie oznacza automatycznie:
jeden fizyczny core
To rozróżnienie stanie się szczególnie istotne przy SMT, ale na tym etapie wystarczy:
Kubernetes CPU
≈ logical CPU capacity widoczna dla systemu
Kto więc decyduje, gdzie task się wykonuje?
Linux scheduler.
Jeżeli task może wykonywać się na wielu CPU, scheduler wybiera konkretny procesor.
Może również później przenieść task:
CPU 2 -> CPU 6
czyli wykonać CPU migration.
Powodów może być wiele, na przykład próba zbalansowania runnable work pomiędzy CPU.
Z punktu widzenia limitu nic szczególnego się nie wydarzyło.
Task nadal zużywa ten sam wspólny CPU-time budget swojej cgroup.
Zmieniło się tylko:
gdzie był wykonywany
CPU affinity
Jeżeli chcemy ograniczyć task do określonych CPU, potrzebujemy osobnego mechanizmu:
CPU affinity
Możemy na przykład powiedzieć:
task może wykonywać się tylko na CPU 2 i CPU 3
Conceptually:
allowed CPUs = {2,3}
Scheduler nadal decyduje:
CPU 2 czy CPU 3?
ale nie może przenieść taska na:
CPU 0
CPU 1
CPU 4
...
Affinity odpowiada więc na pytanie:
gdzie task może być wykonywany?
Nie:
ile CPU time może zużyć?
To kolejny niezależny wymiar.
cpuset
Dla grup procesów Linux posiada controller:
cpuset
W cgroup v2 możemy spotkać:
cpuset.cpus
Na przykład:
2-3
oznacza, że taski należące do tej cgroup mają być wykonywane na CPU należących do tego zestawu.
Kernel dokumentuje cpuset.cpus jako listę CPU, które mogą być używane przez taski z danej cgroup; efektywny zestaw jest dodatkowo ograniczany przez hierarchię nadrzędnych cgroups.
Przykład:
cpuset.cpus = 4-5
daje:
T1 -> CPU 4
T2 -> CPU 5
ale nie:
T1 -> CPU 8
To jest faktyczne ograniczenie placementu.
Limit i cpuset mogą działać jednocześnie
Załóżmy cgroup:
cpuset.cpus = 4-5
oraz:
CPU limit = 1
Mamy więc dwa niezależne ograniczenia.
Placement
workload może wykonywać się tylko na:
CPU 4
CPU 5
Bandwidth
workload może średnio zużywać
odpowiednik 1 CPU
Czyli dwa thready mogą wykonywać się równolegle:
CPU 4 -> T1
CPU 5 -> T2
ale wspólnie nadal konsumują jeden CPU-time budget.
Na przykład:
T1 = 50 ms
T2 = 50 ms
daje:
100 ms CPU time
Request też nie definiuje placementu
Analogicznie:
resources:
requests:
cpu: "1"
nie oznacza:
zarezerwuj dla mnie CPU #7
Jak wiemy z poprzednich artykułów, request odpowiada za:
resource accounting kube-schedulera
+
względną wagę CPU przy contention
Nie za lokalizację taska na konkretnym procesorze.
Dlatego zwykły Pod z:
requests:
cpu: "1"
może wykonywać swoje taski na różnych CPU node'a.
Kiedy Kubernetes rzeczywiście przydziela konkretne CPU?
Do tego służy CPU Manager kubeleta.
Domyślna polityka nie daje zwykłym Podom dedykowanych CPU.
Natomiast polityka:
static
umożliwia przydzielanie wybranym kontenerom exclusive CPUs.
W uproszczeniu, do takiego przydziału kwalifikują się kontenery należące do Podów Guaranteed, które posiadają całkowitoliczbowe CPU requests. Workloady BestEffort, Burstable oraz Guaranteed z fractional CPU requests pozostają we współdzielonej puli CPU.
Przykład:
resources:
requests:
cpu: "2"
limits:
cpu: "2"
przy odpowiedniej konfiguracji CPU Managera może skończyć się przypisaniem:
CPU 6
CPU 7
dla tego kontenera.
Wtedy faktycznie wchodzimy w świat:
CPU placement
a nie tylko:
CPU bandwidth
„Exclusive” też wymaga precyzji
Nawet określenie:
exclusive CPU
nie znaczy bezwarunkowo:
nikt poza moim procesem nigdy nie wykona tam instrukcji
Dokumentacja Kubernetes zaznacza, że ekskluzywność CPU Managera dotyczy innych Podów. Procesy systemowe, takie jak kubelet czy container runtime, nadal mogą wykonywać pracę na takich CPU.
Czyli:
exclusive dla workload placement
niekoniecznie oznacza:
pełna izolacja CPU od całego systemu operacyjnego
Pełniejsza izolacja procesora jest osobnym problemem.
Fractional CPU i integer CPU
To rozróżnienie jest istotne.
Request:
500m
jest fractional.
Nie istnieje coś takiego jak:
pół logical CPU
które można fizycznie wydzielić przez cpuset.
Dlatego fractional CPU naturalnie działa jako:
time sharing
Przykład:
500m
może oznaczać wykonanie taska:
na CPU 1
potem CPU 5
potem CPU 2
przy odpowiednim udziale CPU time.
Natomiast:
2 CPU
może - przy odpowiedniej polityce CPU Managera - zostać odwzorowane na dwa konkretne logical CPUs.
Trzy osobne pytania
Najlepiej rozdzielić wszystkie poznane mechanizmy na trzy pytania.
Ile deklaruję?
requests.cpu
Przykład:
500m
Ile maksymalnie mogę zużyć?
limits.cpu
Przykład:
1 CPU
Gdzie mogę się wykonywać?
affinity / cpuset
Przykład:
CPU 4-5
To trzy różne konfiguracje.
Możemy więc mieć workload:
request = 500m
limit = 1 CPU
cpuset = CPU 4-5
co oznacza w uproszczeniu:
Kubernetes accounting:
0.5 CPU
CPU bandwidth:
maksymalnie 1 CPU
CPU placement:
tylko CPU 4 i 5
Te wartości nie są ze sobą sprzeczne.
Opisują inne właściwości wykonania.
Model mentalny
Nie myśl:
1 CPU
=
jeden konkretny rdzeń
Myśl:
CPU request / limit
=
ilość CPU capacity / CPU time
Natomiast:
CPU affinity / cpuset
=
zbiór CPU, na których task może się wykonywać
Dlatego:
limits:
cpu: "1"
nie przypina procesu do jednego CPU.
A:
cpuset.cpus = 4
nie oznacza automatycznie limitu:
1 CPU bandwidth
Jedno kontroluje czas wykonania.
Drugie kontroluje miejsce wykonania.
Dopiero gdy świadomie połączymy te mechanizmy, możemy mówić o rzeczywistym przypisaniu workloadu do konkretnych CPU.