Konrad Kowalski (rootsher)Principal Platform & Reliability Architect011101110001100110100100010000010000111010110111

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:

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

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

text
A1

Aplikacja B:

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

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

text
aplikacja A = jedna grupa
aplikacja B = druga grupa

i najpierw podzielić CPU pomiędzy grupy:

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

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

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

text
organizację procesów
+
hierarchię

Dopiero controllers implementują zachowanie dotyczące konkretnych zasobów.

Możemy więc mieć kontrolę dotyczącą między innymi:

text
CPU
memory
I/O
pids

W tym artykule interesuje nas wyłącznie:

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

text
A1
A2
B1
B2

Przy identycznych warunkach możemy uprościć sytuację do:

text
A1 ≈ 25%
A2 ≈ 25%
B1 ≈ 25%
B2 ≈ 25%

Sumarycznie:

text
A ≈ 50%
B ≈ 50%

Teraz aplikacja B tworzy kolejne sześć workerów:

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

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

text
group-A ≈ 50%
group-B ≈ 50%

Dopiero wewnątrz grupy A:

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

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

text
cpu.weight

Domyślna wartość wynosi:

text
100

a dostępny zakres:

text
1 .. 10000

cpu.weight określa względną wagę grupy w stosunku do jej aktywnych siblingów.

Załóżmy:

text
group-A:
cpu.weight = 100

group-B:
cpu.weight = 100

Jeżeli obie grupy cały czas potrzebują CPU, oczekujemy w przybliżeniu:

text
A : B
1 : 1

Czyli:

text
A ≈ 50%
B ≈ 50%

Zmieńmy konfigurację:

text
A = 100
B = 200

Teraz proporcja wynosi:

text
1 : 2

czyli przy pełnej konkurencji mniej więcej:

text
A ≈ 33%
B ≈ 67%

Weight jest więc proporcją.

Nie ilością CPU.


Weight nie mówi „ile CPU dostaniesz”

To ważne.

text
cpu.weight = 100

nie oznacza:

text
100% CPU

Ani:

text
1 CPU

Ani:

text
100 ms CPU

Sam weight praktycznie niczego nie mówi bez znajomości konkurencji.

Jeżeli:

text
A = 100
B = 100

to mamy około:

text
50% : 50%

Jeżeli dodamy:

text
C = 100

otrzymujemy około:

text
33% : 33% : 33%

Nie zmieniliśmy konfiguracji A.

A mimo to jego udział się zmienił.

Dlatego weight należy zawsze czytać jako:

text
moja waga
względem wag innych aktywnych grup

Aktywnych jest tutaj bardzo ważne

Załóżmy ponownie:

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

text
A wants CPU
B idle

może dać:

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

text
root
├── A weight=100
│
└── B weight=100
    ├── B1 weight=100
    └── B2 weight=100

Najpierw konkurują:

text
A
B

Jeżeli obie są aktywne:

text
A ≈ 50%
B ≈ 50%

Dopiero udział przypadający B jest dzielony pomiędzy:

text
B1
B2

czyli mniej więcej:

text
B1 ≈ 25% całego CPU
B2 ≈ 25% całego CPU

Finalnie:

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

text
cpu.weight

mogłeś spotkać:

text
cpu.shares

Idea była podobna.

Przykład z dokumentacji kernela:

text
multimedia = 2048 shares
browser    = 1024 shares

oznacza względną proporcję:

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

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

text
scheduler
   |
   +-- task A
   +-- task B
   +-- task C
   +-- task D

Z CPU cgroups:

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

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