CPU Contention: kiedy proces chce CPU, ale go nie dostaje
- data
- kategoria
- Capacity & Performance
- także w
- Computer Science
- czytanie
- 3 min / 691 słów
CPU throttling ma jednoznaczną przyczynę:
workload wykorzystał swój CPU bandwidth limit
CPU contention jest inne.
Tutaj proces może nie mieć żadnego limitu, a mimo to nie dostawać tyle CPU, ile potrzebuje.
Powód jest prosty:
więcej tasków chce się wykonywać,
niż CPU jest w stanie wykonać jednocześnie
To właśnie CPU contention.
Najprostszy przypadek
Mamy jeden logiczny CPU:
CPU 0
i jeden CPU-bound task:
A
Task cały czas ma pracę.
Scheduler może więc praktycznie cały czas wykonywać:
CPU 0 -> A
Mamy:
demand = 1 CPU
capacity = 1 CPU
Nie ma contention.
Dodajmy drugi identyczny task:
A
B
Oba chciałyby wykonywać się przez cały czas.
Ich łączny demand wynosi:
2 CPU
ale dostępna capacity nadal wynosi:
1 CPU
W jednej chwili CPU może wykonywać tylko jeden z nich:
CPU 0 -> A
B -> runnable, czeka
Po chwili scheduler może przełączyć:
CPU 0 -> B
A -> runnable, czeka
I tak dalej:
A -> B -> A -> B -> A -> B
To jest podstawowa forma CPU contention.
Contention zaczyna się przy demand > capacity
Najprostszy model:
CPU demand <= CPU capacity
|
v
brak konieczności czekania na CPU
CPU demand > CPU capacity
|
v
część runnable tasków musi czekać
Na maszynie z czterema logical CPUs:
capacity = 4 CPU
Jeżeli mamy cztery stale runnable single-threaded taski:
A
B
C
D
wszystkie mogą wykonywać się równolegle:
CPU 0 -> A
CPU 1 -> B
CPU 2 -> C
CPU 3 -> D
Dodajmy:
E
Teraz mamy:
5 runnable tasks
4 CPU
Przynajmniej jeden task musi w każdej chwili czekać.
Nie dlatego, że jest throttlowany.
Nie dlatego, że śpi.
Jest gotowy do wykonania.
Po prostu nie ma dla niego CPU.
Runnable task może niczego nie wykonywać
To najważniejsza rzecz w contention.
Task w stanie runnable mówi schedulerowi:
jestem gotowy,
daj mi CPU
Ale runnable nie oznacza:
aktualnie wykonuję instrukcje
Przykład:
time -------------------------------------->
task A:
RUNNING | RUNNING | RUNNING | RUNNING
task B:
WAIT | WAIT | RUNNING | WAIT
W momentach WAIT task B nadal może być runnable.
Nie czeka na:
dysk
sieć
timer
lock aplikacyjny
Czeka wyłącznie na scheduler.
To właśnie czas tracony przez CPU contention.
Runqueue
Runnable taski muszą być gdzieś reprezentowane przez scheduler.
W uproszczeniu można myśleć o runqueue:
CPU 0
running:
A
runnable:
B
C
D
Im więcej runnable pracy przypada na dostępną capacity, tym więcej czasu taski mogą spędzać na oczekiwaniu.
Nie chodzi przy tym wyłącznie o liczbę procesów.
Jeden proces może posiadać:
32 runnable threads
i sam wygenerować duży CPU demand.
Z punktu widzenia schedulera interesują nas taski gotowe do wykonania.
CPU usage nie pokazuje całego demand
Załóżmy aplikację, która przy braku ograniczeń mogłaby wykorzystać:
2 CPU
Node jest jednak przeciążony i scheduler jest w stanie dać jej średnio:
1 CPU
Monitoring pokaże mniej więcej:
CPU usage = 1 CPU
Ale to nie znaczy, że aplikacja potrzebowała tylko jednego CPU.
Realna sytuacja mogła być:
CPU demand = 2 CPU
CPU received = 1 CPU
CPU waiting = 1 CPU worth of work
Oczywiście „waiting = 1 CPU” jest tutaj modelem mentalnym, nie bezpośrednim licznikiem kernela.
Najważniejsze jest rozróżnienie:
usage = CPU time, który workload dostał
demand = CPU time, który workload chciał dostać
Jeżeli scheduler nie uruchomił taska, niewykonana praca nie pojawi się jako CPU usage.
Przykład z node Kubernetes
Załóżmy node:
8 CPU
Uruchomione workloady generują łącznie:
4 CPU demand
Mamy dużo wolnej capacity:
demand = 4
capacity = 8
Nie ma globalnego contention.
Teraz przychodzi peak:
Workload A wants 3 CPU
Workload B wants 3 CPU
Workload C wants 4 CPU
Workload D wants 2 CPU
Łącznie:
demand = 12 CPU
na:
capacity = 8 CPU
Brakuje:
4 CPU
Nie oznacza to, że Linux nagle „tworzy” dodatkowe CPU.
Oznacza:
część runnable pracy będzie czekać
Scheduler będzie rozdzielał osiem dostępnych CPU pomiędzy konkurujące taski zgodnie ze swoimi regułami i wagami cgroups.
Contention nie wymaga limitu
To bardzo ważne po poprzednim artykule.
Załóżmy:
resources:
requests:
cpu: "1"
Brak:
limits:
cpu:
Czyli workload nie posiada CPU bandwidth limitu.
W cgroup v2:
cpu.max = max ...
Nie może więc zostać throttlowany z powodu wyczerpania własnej quota.
Ale nadal może czekać na CPU.
Jeżeli node ma:
8 CPU
a runnable workloady chcą łącznie:
16 CPU
to fizycznie nie da się wykonać całej pracy jednocześnie.
Brak limitu nie oznacza:
zawsze dostanę tyle CPU, ile chcę
Oznacza tylko:
nie mam własnego twardego bandwidth ceiling
Throttling i contention wyglądają podobnie z punktu widzenia aplikacji
W obu przypadkach task może mieć pracę, ale nie wykonywać instrukcji.
Throttling
task runnable
|
v
CPU może być dostępne
|
v
quota wyczerpana
|
v
task nie może się wykonywać
Contention
task runnable
|
v
quota jest dostępna / brak limitu
|
v
CPU wykonuje inne taski
|
v
task czeka
Efekt aplikacyjny może być podobny:
większe latency
mniejszy throughput
Ale przyczyna jest zupełnie inna.
Więcej threadów nie tworzy więcej CPU
Załóżmy aplikację na node z:
4 CPU
Aplikacja tworzy:
4 worker threads
Jeżeli nic innego nie konkuruje, wszystkie mogą wykonywać się równolegle:
T1 -> CPU 0
T2 -> CPU 1
T3 -> CPU 2
T4 -> CPU 3
Dodanie kolejnych:
T5
T6
T7
T8
nie podwaja fizycznej capacity.
Teraz osiem threadów konkuruje o cztery CPU.
W każdej chwili:
4 -> running
4 -> runnable i czekają
oczywiście w uproszczeniu, bo scheduler ciągle przełącza taski.
Większa concurrency może więc zwiększyć:
runnable pressure
bez zwiększenia dostępnej mocy obliczeniowej.
Scheduler latency
Załóżmy task, który staje się runnable w:
t = 0 ms
ale scheduler uruchamia go dopiero:
t = 8 ms
Mamy:
0 ms 8 ms
|--------------------|
runnable
waiting
Dopiero później:
8 ms 10 ms
|-----------|
running
Task potrzebował:
2 ms CPU time
ale wykonanie operacji trwało co najmniej:
10 ms wall-clock
z czego:
8 ms
to było oczekiwanie na możliwość wykonania.
Ten czas nazywamy w praktyce scheduler latency albo runqueue latency.
I jest on jednym z najbardziej bezpośrednich skutków CPU contention.
Dlaczego contention szczególnie boli latency-sensitive workloady
Batch workload może dostać mniej CPU i po prostu skończyć pracę później.
Dla request-response service sytuacja jest gorsza.
Załóżmy request HTTP wymagający:
3 ms CPU time
Bez contention:
~3 ms execution
Przy przeciążonym node:
5 ms waiting
3 ms running
4 ms waiting
otrzymujemy:
12 ms
mimo że aplikacja nadal zużyła tylko:
3 ms CPU time
CPU work się nie zmienił.
Zmienił się czas, kiedy proces mógł go wykonać.
Dlatego contention bardzo szybko zaczyna być widoczne w latency.
Model mentalny
CPU contention można sprowadzić do jednego warunku:
runnable CPU demand
>
available CPU capacity
Wtedy:
task ma pracę
|
v
runnable
|
v
CPU zajęte przez konkurencję
|
v
task czeka
|
v
scheduler w końcu wybiera task
|
v
running
Najważniejsze rozróżnienie względem poprzedniego artykułu:
THROTTLING:
nie możesz używać CPU,
bo wyczerpałeś quota
CONTENTION:
możesz używać CPU,
ale inni też chcą je teraz dostać
W obu przypadkach aplikacja może przestać wykonywać pracę.
Tylko powód jest inny.