PSI: jak Linux mierzy realny CPU pressure
- data
- kategoria
- Observability
- także w
- Capacity & Performance
- czytanie
- 4 min / 801 słów
W poprzednim artykule doszliśmy do sytuacji:
task ma pracę
|
v
task jest runnable
|
v
CPU wykonuje inne taski
|
v
task czeka
To właśnie CPU contention.
Problem z klasycznym CPU usage jest taki, że pokazuje głównie czas, który workload dostał.
Nie pokazuje bezpośrednio czasu, który workload stracił, ponieważ chciał się wykonywać, ale nie miał dostępu do CPU.
Linux ma do tego osobny mechanizm:
PSI
czyli:
Pressure Stall Information
PSI mierzy czas, przez który taski nie mogły wykonywać pracy z powodu niedostępności zasobu. Linux udostępnia PSI dla CPU, memory i I/O; tutaj interesuje nas wyłącznie CPU.
CPU usage i CPU pressure odpowiadają na inne pytania
Załóżmy workload:
CPU usage = 2 CPU
Ta liczba mówi:
workload otrzymał około 2 CPU czasu wykonania
Nie mówi jednak, czy chciał:
2 CPU
czy:
8 CPU
Jeżeli workload chciał 8 CPU, ale node był przeciążony i scheduler dawał mu tylko 2 CPU, różnica materializuje się jako czekanie runnable tasków.
PSI próbuje uchwycić właśnie ten czas.
Możemy więc myśleć:
CPU usage:
ile pracy scheduler pozwolił wykonać
CPU pressure:
jak często workload miał pracę,
ale brak CPU opóźniał jej wykonanie
To dwie różne osie obserwacji systemu.
/proc/pressure/cpu
Na poziomie całego systemu Linux udostępnia CPU PSI przez:
/proc/pressure/cpu
Przykładowy wynik:
some avg10=12.50 avg60=8.20 avg300=3.10 total=91827364
full avg10=0.00 avg60=0.00 avg300=0.00 total=0
Mamy tutaj dwa podstawowe pojęcia:
some
full
oraz cztery wartości dla każdego z nich:
avg10
avg60
avg300
total
some
Dla CPU some oznacza czas, w którym co najmniej jeden task był opóźniany przez brak dostępnego CPU.
Załóżmy cztery CPU:
CPU 0 -> A
CPU 1 -> B
CPU 2 -> C
CPU 3 -> D
E -> runnable, czeka
System nadal wykonuje pracę.
Cztery taski są running.
Ale E chciał również dostać CPU i nie mógł.
Mamy więc CPU pressure:
some
To bardzo istotne.
PSI nie pyta:
czy CPU robi cokolwiek?
Pyta:
czy brak CPU opóźnia jakiś runnable workload?
Jak czytać avg10
Załóżmy:
some avg10=25.00
Nie oznacza to:
CPU utilization = 25%
Ani:
25% tasków czeka
Oznacza mniej więcej:
W ostatnim 10-sekundowym oknie workload doświadczał przynajmniej częściowego CPU stall przez około 25% czasu.
Czyli w uproszczeniu:
10 sekund × 25%
=
2.5 sekundy
przez około 2.5 sekundy tego okna co najmniej jeden task czekał na CPU.
To zupełnie inna informacja niż CPU utilization.
avg10, avg60, avg300
PSI raportuje trzy moving averages:
avg10
avg60
avg300
czyli trendy z około:
10 sekund
60 sekund
300 sekund
Przykład:
some avg10=42.00 avg60=12.00 avg300=3.00
To sugeruje gwałtowne, świeże pogorszenie sytuacji.
Ostatnie kilka minut wyglądały stosunkowo spokojnie:
avg300 = 3%
ale ostatnie kilkanaście sekund:
avg10 = 42%
wygląda już zupełnie inaczej.
Możemy mieć też odwrotną sytuację:
avg10=1.00
avg60=12.00
avg300=20.00
czyli pressure był wcześniej duży, ale właśnie spada.
Linux utrzymuje te wartości jako krótkoterminowe, średnioterminowe i dłuższe trendy pressure.
total
Obok średnich mamy:
total
Przykład:
some ... total=91827364
total jest skumulowanym stall time wyrażonym w mikrosekundach.
Nie jest moving average.
To licznik rosnący w czasie.
Dzięki temu możemy policzyć przyrost pomiędzy dwoma momentami.
Przykład:
t1:
total = 1000000
t2:
total = 1300000
Różnica:
300000 µs
=
300 ms
Czyli pomiędzy pomiarami doszło około 300 ms dodatkowego CPU stall time.
total jest szczególnie przydatne, jeśli chcemy budować własne rate'y albo obserwować krótkie zdarzenia, które mogą zostać mocno wygładzone przez avg10.
A czym jest full?
PSI posiada również:
full
Ogólna definicja full oznacza sytuację, w której wszystkie non-idle taski są jednocześnie stalled i workload jako całość nie wykonuje produktywnej pracy.
Dla memory i I/O jest to bardzo użyteczna informacja.
Dla CPU istnieje jednak ważny szczegół.
Na poziomie całego systemu:
/proc/pressure/cpu
CPU full nie ma sensownej interpretacji odpowiadającej memory czy I/O. Kernel dokumentuje, że system-wide CPU full jest raportowane jako zero ze względów kompatybilności.
Dlatego typowy system-level wynik wygląda:
some avg10=18.20 avg60=10.40 avg300=5.60 total=...
full avg10=0.00 avg60=0.00 avg300=0.00 total=0
I nie oznacza to:
nie ma poważnego CPU pressure
Dla system-wide CPU najważniejsze jest przede wszystkim:
some
PSI na poziomie cgroup
PSI nie musi być mierzone tylko globalnie.
W cgroup v2 każda cgroup może posiadać:
cpu.pressure
czyli pressure policzone dla tasków należących do konkretnej grupy.
Przykład:
/sys/fs/cgroup/.../cpu.pressure
może zwrócić:
some avg10=31.20 avg60=15.70 avg300=8.30 total=...
full avg10=7.50 avg60=3.20 avg300=1.10 total=...
To dużo ciekawsze niż wyłącznie globalny pressure node'a.
Node może mieć wiele workloadów, ale problem może dotyczyć tylko konkretnej części hierarchii cgroups.
Dzięki temu możemy pytać:
czy cały host ma CPU pressure?
oraz osobno:
czy ten konkretny workload doświadcza CPU pressure?
some=0 kontra some=50
Rozważmy dwa przypadki.
Przypadek A
CPU usage = 100%
CPU PSI some = 0%
CPU pracuje cały czas.
Ale runnable workload praktycznie nie czeka na dostęp do procesora.
To może być całkowicie zdrowa sytuacja.
Na przykład jeden CPU-bound worker wykorzystuje cały przypisany CPU:
RUN RUN RUN RUN RUN
Nie ma kolejki konkurujących tasków.
Przypadek B
CPU usage = 100%
CPU PSI some = 50%
CPU również pracuje cały czas.
Ale przez znaczną część czasu przynajmniej jeden dodatkowy task chciał się wykonywać i nie mógł.
Czyli:
running:
A B C D
waiting:
E F G
Usage w obu przypadkach może wyglądać podobnie.
Pressure jest zupełnie inne.
Dlaczego PSI jest tak użyteczne
Klasyczne utilization świetnie odpowiada na pytanie:
jak mocno wykorzystujemy CPU?
Ale przy capacity problems ciekawsze pytanie brzmi:
czy brak CPU już przeszkadza workloadowi w wykonywaniu pracy?
PSI odpowiada właśnie na to drugie.
To pozwala rozróżnić:
CPU jest zajęte
od:
CPU jest zasobem ograniczającym wykonywanie workloadu
Te dwie rzeczy nie są równoważne.
Przykład
Załóżmy node z:
8 CPU
i workload generujący dokładnie:
8 runnable CPU-bound threads
W idealnym przypadku:
CPU 0 -> T1
CPU 1 -> T2
...
CPU 7 -> T8
CPU utilization:
~100%
ale niewiele tasków musi czekać.
Pressure może być niewielkie.
Teraz dodajmy kolejne osiem runnable threadów:
T9 .. T16
Capacity pozostaje:
8 CPU
Demand rośnie w okolice:
16 CPU
CPU utilization nadal wynosi:
~100%
Nie może przecież wynosić 200% całego node'a.
Ale teraz część tasków stale czeka.
To właśnie zobaczymy w rosnącym:
CPU PSI some
Usage praktycznie przestało być w tym miejscu użyteczne do określania skali contention.
PSI nadal się zmienia.
CPU PSI w Kubernetes
Na współczesnym Kubernetes PSI może być zbierane przez kubelet na poziomie:
node
pod
container
Kubernetes udostępnia te dane przez Summary API oraz endpoint metryk kubeleta. W Kubernetes 1.36 obsługa PSI przez kubelet jest już funkcją stabilną.
Sam mechanizm nadal pochodzi jednak z Linuksa.
Kubernetes tylko zbiera i wystawia dane wygenerowane przez PSI w kernelu.
Model mentalny
CPU utilization:
ile czasu CPU wykonywało pracę
CPU PSI:
jak często brak CPU opóźniał runnable pracę
Najważniejszy sygnał:
some
oznacza:
przez tę część czasu
co najmniej jeden task chciał CPU,
ale musiał czekać
Natomiast:
avg10
avg60
avg300
pokazują trend pressure w różnych oknach czasowych, a:
total
jest skumulowanym stall time.
Dlatego PSI nie jest kolejną metryką utilization.
Mierzy coś znacznie ciekawszego:
czas stracony z powodu niedostępności CPU