cgroups i CPU: jak Linux dzieli procesor między grupy procesów
- data
- kategoria
- Infrastructure
- także w
- Computer Science
- czytanie
- 4 min / 760 słów
W poprzednim tekście sprowadziliśmy scheduling CPU do prostego modelu:
task ma pracę
|
v
runnable
|
v
scheduler wybiera task
|
v
running
To działa dobrze, dopóki patrzymy na pojedyncze procesy.
Problem zaczyna się wtedy, gdy chcemy powiedzieć:
te 3 procesy należą do aplikacji A,
a te 20 procesów należą do aplikacji B
i chcemy, żeby scheduler traktował te dwie aplikacje jako grupy, a nie jako 23 niezależne taski.
Do tego służą cgroups.
Po co w ogóle grupować procesy
Załóżmy jeden CPU i dwie aplikacje.
Aplikacja A:
A1
Aplikacja B:
B1
B2
B3
B4
B5
B6
B7
B8
B9
Jeżeli scheduler patrzyłby wyłącznie na pojedyncze taski i każdy z nich miałby taki sam udział, mielibyśmy łącznie 10 konkurentów.
W bardzo uproszczonym modelu:
A1 ≈ 10% CPU
B1..B9 ≈ 90% CPU
Czyli aplikacja B dostaje dziewięć razy więcej CPU tylko dlatego, że stworzyła dziewięć tasków.
Czasami dokładnie tego chcemy.
Ale często nie.
Możemy chcieć powiedzieć:
aplikacja A = jedna grupa
aplikacja B = druga grupa
i najpierw podzielić CPU pomiędzy grupy:
A ≈ 50%
B ≈ 50%
a dopiero później rozdzielić udział grupy B pomiędzy jej dziewięć tasków.
To właśnie jeden z problemów, które rozwiązują cgroups.
Linuxowy scheduler posiada wsparcie dla group schedulingu: taski można pogrupować, a CPU time rozdzielać pomiędzy grupy zamiast traktować wszystkie procesy wyłącznie jako płaską listę równorzędnych konkurentów.
Czym jest cgroup
cgroup oznacza control group.
Najprościej:
cgroup to mechanizm kernela pozwalający umieścić zestaw procesów w hierarchicznej grupie i zastosować do tej grupy określone reguły zarządzania zasobami.
Kernel opisuje cgroups jako mechanizm organizowania procesów hierarchicznie i kontrolowanego rozdzielania zasobów wzdłuż tej hierarchii.
Wyobraźmy sobie:
root
├── application-a
│ ├── process A1
│ └── process A2
│
└── application-b
├── process B1
├── process B2
├── process B3
└── process B4
Procesy nadal są normalnymi taskami Linuksa.
cgroup nie tworzy nowego typu procesu.
Daje natomiast kernelowi dodatkową informację:
A1 i A2 należą do jednej grupy
B1..B4 należą do drugiej
Na tej podstawie resource controller może zastosować reguły do całej grupy.
cgroup to nie tylko CPU
Sam mechanizm cgroups odpowiada przede wszystkim za:
organizację procesów
+
hierarchię
Dopiero controllers implementują zachowanie dotyczące konkretnych zasobów.
Możemy więc mieć kontrolę dotyczącą między innymi:
CPU
memory
I/O
pids
W tym artykule interesuje nas wyłącznie:
cpu controller
Jego zadaniem jest wpływanie na sposób, w jaki grupy tasków dzielą procesor.
Bez cgroup: scheduler widzi taski
Wyobraźmy sobie cztery CPU-bound taski:
A1
A2
B1
B2
Przy identycznych warunkach możemy uprościć sytuację do:
A1 ≈ 25%
A2 ≈ 25%
B1 ≈ 25%
B2 ≈ 25%
Sumarycznie:
A ≈ 50%
B ≈ 50%
Teraz aplikacja B tworzy kolejne sześć workerów:
A1
A2
B1
B2
B3
B4
B5
B6
B7
B8
Jeżeli wszystkie taski konkurowałyby na tych samych zasadach, aplikacja B uzyskałaby znacznie większą część CPU.
To pokazuje, dlaczego liczba threadów nie zawsze powinna automatycznie definiować udział aplikacji w maszynie.
Z cgroup możemy najpierw podzielić CPU między aplikacje
Tworzymy:
root
├── group-A
│ ├── A1
│ └── A2
│
└── group-B
├── B1
├── B2
├── B3
├── B4
├── B5
├── B6
├── B7
└── B8
I ustawiamy obie grupy jako równorzędne.
Wtedy możemy otrzymać podział:
group-A ≈ 50%
group-B ≈ 50%
Dopiero wewnątrz grupy A:
A1
A2
dzielą między siebie jej część CPU.
Analogicznie osiem tasków B dzieli między siebie udział group-B.
Czyli scheduler może rozumować hierarchicznie:
CPU
|
+-- 50% -> group-A
| |
| +-- A1
| +-- A2
|
+-- 50% -> group-B
|
+-- B1
+-- B2
+-- ...
To jest istota group scheduling.
CPU weight
Współczesne cgroup v2 udostępnia dla CPU parametr:
cpu.weight
Domyślna wartość wynosi:
100
a dostępny zakres:
1 .. 10000
cpu.weight określa względną wagę grupy w stosunku do jej aktywnych siblingów.
Załóżmy:
group-A:
cpu.weight = 100
group-B:
cpu.weight = 100
Jeżeli obie grupy cały czas potrzebują CPU, oczekujemy w przybliżeniu:
A : B
1 : 1
Czyli:
A ≈ 50%
B ≈ 50%
Zmieńmy konfigurację:
A = 100
B = 200
Teraz proporcja wynosi:
1 : 2
czyli przy pełnej konkurencji mniej więcej:
A ≈ 33%
B ≈ 67%
Weight jest więc proporcją.
Nie ilością CPU.
Weight nie mówi „ile CPU dostaniesz”
To ważne.
cpu.weight = 100
nie oznacza:
100% CPU
Ani:
1 CPU
Ani:
100 ms CPU
Sam weight praktycznie niczego nie mówi bez znajomości konkurencji.
Jeżeli:
A = 100
B = 100
to mamy około:
50% : 50%
Jeżeli dodamy:
C = 100
otrzymujemy około:
33% : 33% : 33%
Nie zmieniliśmy konfiguracji A.
A mimo to jego udział się zmienił.
Dlatego weight należy zawsze czytać jako:
moja waga
względem wag innych aktywnych grup
Aktywnych jest tutaj bardzo ważne
Załóżmy ponownie:
A weight = 100
B weight = 100
Ale workload B aktualnie niczego nie wykonuje.
Wtedy A nie zostaje sztucznie ograniczone do 50%.
Może wykorzystać dostępny CPU.
Kernel określa model weight-based distribution jako work-conserving: w podziale uczestniczą grupy, które w danym momencie rzeczywiście mogą wykorzystać zasób.
Czyli:
A wants CPU
B idle
może dać:
A ≈ 100%
B ≈ 0%
Mimo identycznego weight.
Weights zaczynają definiować proporcję wtedy, gdy faktycznie pojawia się konkurencja.
Hierarchia ma znaczenie
cgroups tworzą drzewo.
To oznacza, że podział CPU nie musi odbywać się globalnie pomiędzy wszystkimi grupami naraz.
Przykład:
root
├── A weight=100
│
└── B weight=100
├── B1 weight=100
└── B2 weight=100
Najpierw konkurują:
A
B
Jeżeli obie są aktywne:
A ≈ 50%
B ≈ 50%
Dopiero udział przypadający B jest dzielony pomiędzy:
B1
B2
czyli mniej więcej:
B1 ≈ 25% całego CPU
B2 ≈ 25% całego CPU
Finalnie:
A ≈ 50%
B1 ≈ 25%
B2 ≈ 25%
I właśnie dlatego nie można spojrzeć na dwa cpu.weight znajdujące się w różnych miejscach drzewa i bezpośrednio porównać ich wartości.
Weight ma znaczenie przede wszystkim pomiędzy sibling cgroups, czyli grupami konkurującymi na tym samym poziomie hierarchii.
Historycznie: cpu.shares
Jeżeli pracowałeś ze starszymi systemami albo cgroup v1, zamiast:
cpu.weight
mogłeś spotkać:
cpu.shares
Idea była podobna.
Przykład z dokumentacji kernela:
multimedia = 2048 shares
browser = 1024 shares
oznacza względną proporcję:
2 : 1
pomiędzy tymi grupami podczas konkurencji o CPU.
Nie warto jeszcze wchodzić głębiej w różnice między v1 i v2.
Na tym etapie istotne jest tylko:
cgroup v1 -> cpu.shares
cgroup v2 -> cpu.weight
Oba pojęcia opisują względny udział grupy w CPU.
Model mentalny
Bez cgroups możemy myśleć:
scheduler
|
+-- task A
+-- task B
+-- task C
+-- task D
Z CPU cgroups:
scheduler
|
+-- group A
| |
| +-- task A1
| +-- task A2
|
+-- group B
|
+-- task B1
+-- task B2
+-- task B3
Scheduler nadal wykonuje konkretne taski.
cgroups pozwalają jednak powiedzieć kernelowi:
zanim zaczniesz patrzeć na poszczególne procesy, potraktuj je jako grupy i uwzględnij reguły przypisane tym grupom.
W przypadku CPU jedną z podstawowych takich reguł jest względna waga.
Najważniejsze rzeczy do zapamiętania:
cgroup = grupa procesów
cpu controller = zasady dotyczące CPU dla tej grupy
cpu.weight = względna waga grupy
weight != limit
weight ma znaczenie przy konkurencji
podział jest hierarchiczny
Na tym etapie wystarczy.
Nie ograniczyliśmy jeszcze maksymalnego CPU, nie wprowadziliśmy quota i nie mówimy jeszcze o Kubernetes.
Po prostu nauczyliśmy kernel patrzeć na wiele procesów jak na jedną grupę.