Profilowanie wydajności aplikacji w Linuxie: perf, top i flamegraphy w praktyce

0
41
5/5 - (1 vote)

Nawigacja:

Od „aplikacja wolno działa” do precyzyjnego pytania wydajnościowego

Rozmyte „wolno” kontra konkretne cele wydajnościowe

Stwierdzenie „aplikacja wolno działa” jest zbyt ogólne, żeby dało się na jego podstawie sensownie użyć narzędzi takich jak perf, top czy flamegraphy. Profilowanie wydajności w Linuxie zaczyna się od zamiany tego rozmytego wrażenia w konkretne pytanie. Inne techniki zastosujesz, gdy problemem jest opóźnienie pojedynczego żądania, inne przy zbyt małej przepustowości, a jeszcze inne przy nadmiernym zużyciu CPU.

Najczęściej istotne są cztery klasy metryk:

  • Latencja – czas obsługi pojedynczej operacji (np. request HTTP, zapytanie SQL, zadanie batchowe).
  • Przepustowość – liczba operacji na jednostkę czasu (requests/s, jobs/min).
  • Zużycie zasobów – CPU, pamięć, I/O, sieć, deskryptory plików.
  • Stabilność – skoki opóźnień, sporadyczne „przywieszki”, zrywy CPU, kolizje locków.

Bez określenia, co dokładnie jest nieakceptowalne, profilowanie zamienia się w losowe grzebanie w metrykach. Taki „profiling bez pytania” generuje mnóstwo danych, które trudno zinterpretować, a na końcu i tak nie wiadomo, co poprawić.

Podstawowe metryki systemowe: co mówią, a czego nie mówią

Przed sięgnięciem po zaawansowane narzędzia warto odczytać kilka prostych wskaźników systemowych. Dają szybki obraz sytuacji i pomagają sformułować hipotezy.

  • Load average – uśredniona liczba procesów w stanie running lub uninterruptible sleep. Wysoki load nie zawsze oznacza wysokie użycie CPU; na maszynie I/O-bound load potrafi być duży mimo niskiego CPU%.
  • CPU usage – udział czasu CPU w różnych stanach: user, system, iowait, steal. Mała część user/system przy wysokim iowait kieruje w stronę problemów dyskowych lub sieciowych.
  • Context switch rate – liczba przełączeń kontekstu na sekundę. Bardzo wysoka może sugerować nadmierną ilość wątków, intensywną synchronizację lub thrashing.
  • iowait – procent czasu, gdy CPU czeka na I/O. Wysokie wartości wskazują na „zaciągnięcie ręcznego” przez dysk lub sieć, ale nie mówią, która aplikacja jest winna.

Te wskaźniki odpowiadają na pytanie „czy jest problem systemowy” i w przybliżeniu „jakiego typu”. Nie mówią jednak, który kawałek kodu jest gorącym punktem. Do tego właśnie potrzebne są narzędzia profilujące CPU, pamięć i I/O, w tym perf i flamegraphy.

Formułowanie hipotezy: CPU‑bound, IO‑bound, blokady i inne klasy problemów

Praktyczne profilowanie wydajności w Linuxie opiera się na testowaniu hipotez, a nie bezładnym odpalaniu kolejnych narzędzi. Hipoteza powinna wskazywać:

  • obszar – CPU, I/O, pamięć, sieć, synchronizacja, GC, scheduler, konfiguracja systemu, baza danych, itp.,
  • przybliżony mechanizm – np. „kod CPU-bound w pętli przetwarzania danych”, „wątek zablokowany na muteksie”, „opóźnienia w kolejce dyskowej”.

Typowe hipotezy startowe:

  • CPU-bound – proces zjada 100% CPU (lub wszystkie rdzenie), iowait jest niski, load rośnie wraz z liczbą żądań.
  • I/O-bound – wysoki iowait, wysokie czasy odpowiedzi dysku, mało czasu user/system przy wysokim load.
  • Lock contention – CPU niewykorzystane, a wątki aplikacji wiszą w stanie sleep na prymitywach synchronizacji, w logach sporadycznie pojawiają się ostrzeżenia o timeoucie locków.
  • GC / runtime – przy JVM/.NET wysoki czas w GC, przerwy stop-the-world, przy Pythonie nadmierny overhead GIL-a.
  • Błędna konfiguracja – zbyt małe limity workerów, zbyt agresywne limity CPU w kontenerach, niskie limity file descriptors.

Każda z tych hipotez prowadzi do innych narzędzi i innego sposobu używania perf, co znacząco skraca czas diagnozy.

Kolejność działań: od prostego monitoringu do profilera

Zanim do gry wejdzie perf, warto wykonać serię prostych kroków z użyciem lekkich narzędzi. Typowa sekwencja w warunkach produkcyjnych wygląda następująco:

  • Sprawdzenie ogólnej kondycji systemu: top, htop, uptime, free -m.
  • Rozbicie CPU na rdzenie i wątki: mpstat -P ALL 1 (pakiet sysstat).
  • Obserwacja procesów: pidstat -u -r -d -w 1 zidentyfikuje, kto faktycznie używa CPU, pamięci, I/O.
  • Analiza I/O: iostat -x 1 oraz iotop (jeśli dostępny) pokaże, które dyski i procesy są pod obciążeniem.
  • Dopiero po wstępnym zawężeniu problemu – uruchomienie perf stat, perf top, perf record na docelowym procesie lub całym systemie.

Taka kolejność chroni przed błędem częstym u początkujących: „profiluję sekcję kodu, która wcale nie jest wąskim gardłem”. Narzędzia typu perf są dokładne, ale mierzenie nieodpowiedniego fragmentu aplikacji prowadzi do błędnych wniosków i pozornej optymalizacji.

Pułapka „mierzę wszystko, ale nie wiem, o co pytam”

Jednym z częstszych problemów jest gromadzenie gigantycznej ilości danych – pełne trace’y, ogromne pliki perf.data, szczegółowe flamegraphy – bez zdefiniowanego celu. Skutkiem jest paraliż decyzyjny: dane są, ale trudno przełożyć je na konkretne zmiany w kodzie czy konfiguracji.

Bez jasno postawionego pytania dochodzi też do nadinterpretacji. Na przykład widok kosztownej funkcji w flamegraphie może kusić do jej optymalizacji, mimo że przy realnym obciążeniu aplikacja spędza tam niewielki ułamek całkowitego czasu. Profilowanie wydajności ma sens tylko wtedy, gdy prowadzi do decyzji w stylu: „jeśli zredukujemy koszt tej funkcji o 50%, całkowity czas przetwarzania spadnie o X%”.

Praktyczny schemat jest prosty: najpierw pytanie, potem narzędzie. Nie odwrotnie.

Podstawy profilowania w Linuxie – sampling, tracing, statystyki

Sampling kontra tracing – dwa różne podejścia do pomiaru

Profilowanie wydajności w Linuxie można z grubsza podzielić na dwa style: sampling (próbkowanie) i tracing (śledzenie zdarzeń). Narzędzia typu perf w trybie CPU profiler wykorzystują sampling: co określony interwał (np. 99 razy na sekundę) rejestrują stos wywołań aktualnie wykonywanego kodu. Summowanie wielu próbek daje statystyczny obraz gorących ścieżek.

Tracing idzie w inną stronę – zapisuje każde lub wybrane zdarzenie: wejścia/wyjścia do funkcji, zdarzenia jądra (syscall, sched_switch, irq), punkty śledzenia w konkretnych miejscach kodu. Narzędzia takie jak ftrace, eBPF (BPF), LTTng, czy systemtap rejestrują dokładne sekwencje zdarzeń, z czasem ich wystąpienia i dodatkowymi danymi.

Sampling jest zwykle tańszy i wystarczający, gdy pytanie dotyczy „gdzie zużywany jest czas CPU”. Tracing jest cięższy, ale konieczny, kiedy trzeba zrozumieć zależności przyczynowo-skutkowe, precyzyjnie prześledzić opóźnienia między komponentami albo analizować rzadkie, krótkotrwałe zdarzenia, które sampling może pominąć.

Profilowanie systemowe, procesowe i kontenerowe

Profilowanie systemowe obejmuje cały host: wszystkie procesy, wątki i jądro. Komendy typu perf top bez argumentów lub perf record -a działają w tym trybie. To dobry punkt wyjścia, gdy nie wiadomo, który proces jest winny lub gdy podejrzewa się interakcje między usługami.

Profilowanie procesowe skupia się na konkretnym PID lub grupie procesów. Przykładowe użycie perf:

  • perf stat -p <PID> – statystyki dla jednego procesu.
  • perf record -p <PID> -g – próbki stosów wywołań dla danego procesu z callgraph.

W środowiskach kontenerowych (Docker, Kubernetes) dochodzi jeszcze profilowanie w ramach cgroup. Nowsze wersje perf wspierają profilowanie cgroup, co pozwala zbierać dane dla konkretnego kontenera, nawet jeśli PID-y są „przepisane” przez przestrzeń nazw.

Bez świadomości, na jakim poziomie zbierane są dane, łatwo wysnuć błędne wnioski. Przykład: profilując CPU systemu, można dojść do wniosku, że aplikacja jest „lekka”, podczas gdy jest po prostu „schowana” za warstwą innego procesu (np. reverse proxy, baza danych), który wykonuje większość pracy.

Koszt pomiaru i zniekształcenia wprowadzane przez profilowanie

Każde narzędzie profilujące ma swój overhead. Nawet stosunkowo lekkie sampling z perf powoduje dodatkowe przerwania i przełączenia kontekstu, które mogą delikatnie przesunąć profil czasowy systemu. W większości przypadków overhead jest akceptowalny (rzędu kilku procent), ale są scenariusze, w których staje się istotny.

Typowe źródła zakłóceń:

  • Wysoka częstotliwość próbkowania (-F 999 i więcej), szczególnie na wielu rdzeniach.
  • Profilowanie na maszynach o ograniczonych zasobach (mało rdzeni, mało pamięci).
  • Zapisywanie ogromnej ilości danych do dysku (duże pliki perf.data, intensywny tracing).
  • Współdzielenie hosta z innymi krytycznymi usługami – profilowanie może zaburzać ich działanie.

Do tego dochodzi zjawisko cache perturbation: dodatkowe odczyty danych (np. z pamięci struktur kernela podczas zbierania próbek) ingerują w zawartość cache L1/L2/L3, co lekko zmienia zachowanie aplikacji. W praktyce ten efekt zwykle jest pomijalny, ale przy bardzo wrażliwych pomiarach mikrobenchmarkowych może mieć znaczenie.

PMU, sygnały i przerwania – co faktycznie robi perf

perf wykorzystuje Performance Monitoring Unit (PMU) procesora oraz mechanizmy jądra do generowania przerwań sprzętowych w odpowiedzi na określone zdarzenia (np. określona liczba cykli, instrukcji, cache-missów). Przy każdym takim przerwaniu kernel zbiera aktualny licznik, adres instrukcji i – jeśli zażądano – stos wywołań. Te dane są buforowane, a następnie zapisywane do pliku perf.data lub wyświetlane „na żywo”.

W trybach statystycznych (perf stat) perf używa eventów PMU do zliczania zdarzeń w czasie. W trybach sample’ujących (perf record, perf top) wykorzystuje zakładane przerwania do zbierania próbek. Z kolei tracing (np. z eventami schedulera) może wykorzystywać wewnętrzne mechanizmy kernela i tracepointy, zamiast przerwań sprzętowych.

Zrozumienie, że perf bazuje na PMU i przerwaniach, tłumaczy m.in. różnice między typami eventów, ograniczenia sprzętowe (liczba liczników, multiplexing) oraz rozbieżności między „rzeczywistymi” wartościami a tym, co widzi narzędzie, gdy musi multipleksować wiele zdarzeń na ograniczonej liczbie liczników.

Kiedy sampling nie wystarczy i trzeba sięgnąć po tracing

Sampling w stylu perf record -g świetnie pokazuje, gdzie zużywany jest czas CPU, ale gorzej radzi sobie z:

  • rozłożonymi w czasie zależnościami między zdarzeniami (np. request przechodzący przez kilka usług),
  • krótkotrwałymi, rzadkimi zjawiskami (nagły skok latencji raz na kilka minut),
  • precyzyjną analizą opóźnień w poszczególnych warstwach (sieć, kolejki, locki, scheduler).

W takich sytuacjach wchodzi w grę tracing: ftrace (w jądrze), eBPF (np. bpftrace, narzędzia od Brendan Gregg), LTTng, perf trace i eventy śledzące jądra. Pozwalają one na:

  • śledzenie konkretnych syscalls i ich czasów trwania,
  • mierzenie opóźnień na poziomie kolejek (np. async I/O, scheduler),
  • dołączanie dodatkowych informacji kontekstowych (PID, nazwa wątku, parametry wywołań).

top, htop i spółka – szybka diagnoza zanim pojawi się perf

top jako „stetoskop” – co faktycznie oglądają liczby

top bywa traktowany jak kolorowy wykresik CPU, ale da się z niego wyciągnąć znacznie więcej – o ile wiadomo, co oznaczają pola w nagłówku i jak je czytać krytycznie. Pierwszy wiersz (load average) nie jest bezpośrednim „użyciem CPU”, tylko średnią długością kolejki gotowych do działania procesów. Wysoki load przy niskim zużyciu CPU często oznacza blokadę na I/O, mutexach albo innych zasobach, a nie „brak mocy obliczeniowej”.

Bardziej użyteczny w kontekście profilowania jest rozwinięty widok CPU. Typowe pola:

  • us – czas w przestrzeni użytkownika,
  • sy – czas w jądrze (syscall, obsługa przerwań dolnego poziomu),
  • id – bezczynność,
  • wa – oczekiwanie na I/O (CPU ma co robić, ale proces czeka na dysk/sieć),
  • st – „ukradziony” czas wirtualizacji (hypervisor zabiera rdzeń).

Zestawienie wysokiego us z dominującym jednym PID-em to kandydat pod CPU-profiling (perf record, perf top). Duży sy może wskazywać na częste syscall-e (np. intensywny networking), zbyt agresywne context switch-e albo „brudny” dostęp do dysku. Wysokie wa każe raczej patrzeć na iostat, iotop i system plików niż na kod aplikacji.

Pułapką jest fetysz jednego numerka. Pojedynczy snapshot top łatwo przeinterpretować. Bardziej sensowne jest uruchomienie go w trybie wsadowym: top -b -d 1 -n 60 > top.log i późniejsza analiza trendu, a dopiero potem sięgnięcie po perf. Krótkotrwałe skoki, które znikają po sekundzie, rzadko opłaca się ścigać na poziomie mikroinstrukcji.

htop i filtry – szybkie zawężenie „podejrzanych”

htop ułatwia filtrowanie i sortowanie, co przydaje się na gęsto obciążonych serwerach. W praktyce kilka nawyków wystarcza, by przed użyciem perf zredukować liczbę podejrzanych procesów do garstki:

  • Sortowanie po %CPU i TIME+ jednocześnie – proces, który chwilowo skacze na 200% CPU, ale łącznie ma znikomy czas, zwykle nie jest głównym winowajcą.
  • Widok wątków (często klawisz H) – aplikacja wielowątkowa obciążająca tylko jeden wątek zwykle ma problem z paralelizacją, nie „brakiem rdzeni”. To wpływa później na interpretację flamegraphów.
  • Kolumna STATE / S – wątki wiszące w stanie D (uninterruptible sleep) pokazują problemy I/O, w S (interruptible sleep) – możliwe oczekiwanie na synchronizację, w R – faktyczną pracę na CPU.

To proste sito pomaga uniknąć profilowania nieistotnych procesów, które tylko „mrugają” w statystykach.

Uzupełniające narzędzia: vmstat, pidstat, iostat

Zanim wejdzie cięższa artyleria w postaci perf, przydatne jest krótkie „skanowanie perymetru” systemu:

  • vmstat 1 – szybki pogląd na run queue, context switch-e, page faulty, swap. Wysokie cs z niskim użyciem CPU sugeruje dużo drobnych zadań – optymalizacja pojedynczej funkcji w takim środowisku może niewiele zmienić.
  • pidstat -u -r -d -w 1 – rozbicie obciążenia na procesy z perspektywy CPU, pamięci, I/O i przełączeń kontekstu. Przydaje się, gdy na hostcie działa kilkadziesiąt usług.
  • iostat -x 1 – pokazuje, czy problemem jest realnie dysk (wysokie await, svctm, saturacja %util), czy jedynie percepcja aplikacji.

Jeśli w tym zestawie nie widać wyraźnych anomalii, wchodzi pytanie: „czy problem jest w CPU, pamięci, czy w czymś bardziej subtelnym (locki, kolejkowanie, scheduler)?”. Dopiero na takim tle output z perf będzie miał sensowny kontekst.

Laptop z wyświetlonym kodem i wykresami wydajności, okulary na klawiaturze
Źródło: Pexels | Autor: Daniil Komov

Wprowadzenie do perf – najważniejsze komendy i ich sens

Architektura rodziny komend perf

perf to zbiór podkomend, które korzystają z tego samego mechanizmu PMU i tracepointów, ale odpowiadają na inne pytania. Najczęściej używane w praktyce:

  • perf stat – „ile zdarzeń wystąpiło” w trakcie działania programu lub na poziomie systemu.
  • perf record + perf report – nagranie próbek i ich późniejsza analiza, odpowiednik „profilera offline”.
  • perf top – profilowanie „na żywo”, coś jak top dla symboli/funkcji.
  • perf script – eksport próbek do formatu tekstowego, wykorzystywany m.in. do flamegraphów.
  • perf trace – śledzenie syscall-i i eventów, zbliżone do strace, ale zorientowane na metryki wydajności.

Dobór podkomendy ma wynikać z pytania. Jak długo trwa pojedyncze zadanie i ile instrukcji zużywa? perf stat. Które funkcje dominują w czasie CPU? perf record/report lub perf top. Czy proces spędza czas w syscalach i jakich? perf trace lub eventy jadra (np. sys_enter_*, sys_exit_*).

Eventy sprzętowe i programowe – co właściwie liczy perf

Konfigurując perf, trzeba rozróżniać kilka klas zdarzeń:

  • Hardware events – cykle CPU, instrukcje, cache misses, branch misses itp. Definiowane przez PMU.
  • Software events – np. context switch-e, page faulty, przerwania zegara, zliczane przez kernel.
  • Tracepointy – zdefiniowane w jądrze „haczyki” (np. schedulera, I/O, sieci), przydatne w tracingu.
  • Raw events – surowe kody PMU, używane, gdy domyślne aliasy nie wystarczają lub są błędnie zmapowane.

Dla podstawowych analiz CPU zwykle wystarczy zestaw: cycles, instructions, branches, branch-misses, czasem dołączony cache-misses. Z kolei dla analizy wysokiego sy w top przyda się obserwacja syscall-i i eventów schedulera.

Należy liczyć się z tym, że nie wszystkie eventy są dostępne na każdym CPU, a ich semantyka może lekko różnić się między generacjami. Dlatego interpretacja „magicznych” wskaźników typu IPC (instructions per cycle) czy LLC miss rate wymaga ostrożności i czasem kalibracji na konkretnym sprzęcie.

Profilowanie całego systemu kontra wybranych procesów

Typowy schemat pracy z perf ma dwa etapy. Najpierw nagranie lub obserwacja całego systemu, by złapać ogólny obraz, a potem przejście na profilowanie konkretnego procesu lub cgroup:

  • perf top lub perf record -a – pełny host, wszystkie procesy i jądro.
  • perf stat -p <PID>, perf record -p <PID> -g – zawężenie do jednej aplikacji.
  • perf stat --cgroup <path> – w nowszych jądrach: profilowanie kontenera zamiast gościa wirtualnego PID.

Pomijanie tego pierwszego kroku bywa źródłem błędnych wniosków. Profilowanie tylko jednej aplikacji, gdy realne wąskie gardło leży w innym procesie (np. w bazie danych na tym samym hoście), prowadzi prosto do pozornej optymalizacji: „przyspieszono” funkcję, która nie dominowała w czasie całkowitym.

perf stat – szybkie statystyki wydajności pojedynczego programu

Podstawowy użytek: start/stop wokół programu

Najprostszy scenariusz działania: uruchomienie programu przez perf stat i odczytanie statystyk po jego zakończeniu:

perf stat ./moja_aplikacja --tryb=benchmark

Wyjście zawiera liczniki zdarzeń oraz całkowity czas rzeczywisty i CPU. Domyślne eventy obejmują cykle, instrukcje, przerwania, page faulty i context switch-e. Do podstawowej diagnostyki często wystarcza porównanie dwóch wersji programu (np. przed i po zmianie algorytmu) pod kątem:

  • całkowitego czasu,
  • liczby instrukcji,
  • IPC (wg linii instructions / cycles).

Przykładowo, jeśli czas spadł, ale liczba instrukcji wzrosła, możliwa jest poprawa lokalności pamięci (mniej cache-missów) lub lepsze wykorzystanie równoległości. Jeśli czas niemal się nie zmienił, a instrukcje spadły znacząco – może wąskim gardłem nie jest CPU, tylko I/O albo locki.

Rozszerzone zestawy eventów – kiedy wychodzić poza domyślne

Gdy potrzeba precyzyjniejszej diagnozy, warto jawnie podać zestaw liczników:

perf stat -e cycles,instructions,branches,branch-misses,cache-misses 
  ./moja_aplikacja --tryb=benchmark

Z takiego kompletu da się wnioskować o kilku klasach problemów:

  • Niskie IPC i dużo cache-missów – prawdopodobne problemy z lokalnością danych (nieuporządkowane struktury, duże skoki po pamięci).
  • Niskie IPC i dużo branch-missów – skomplikowana logika warunkowa, brak przewidywalności gałęzi.
  • Wysokie IPC, mało instrukcji, ale czas nadal duży – CPU niekoniecznie jest wąskim gardłem, trzeba sprawdzić I/O i blokady.

Interpretacja powinna być ostrożna. Ten sam odczyt może mieć inne znaczenie dla aplikacji CPU-bound przetwarzającej dane w pamięci i inne dla usługi sieciowej, która większość czasu czeka na pakiety.

Profilowanie długotrwałych procesów – tryb dołączania

Nie zawsze można lub chce się restartować proces. perf stat obsługuje tryb dołączania:

perf stat -p <PID>

W takim wypadku liczniki są zliczane od momentu startu komendy do jej zakończenia (np. po wciśnięciu Ctrl+C). W praktyce daje to uśredniony obraz aktywności procesu w wybranym oknie czasowym. Jeśli w tym czasie oczekuje się specyficznego obciążenia (np. test wydajności z generatora ruchu), wyniki mają sens jako porównanie między wersjami kodu lub konfiguracjami.

Trzeba jednak liczyć się z tym, że długie okna czasowe wygładzają krótkotrwałe zjawiska. Spajk trwający sekundę w procesie działającym godzinę praktycznie „rozmyje się” w statystykach. Stąd praktyka wykonywania wielu krótszych pomiarów zamiast jednego bardzo długiego.

Multiplexing i „group events” – gdy liczników jest za mało

Procesory mają ograniczoną liczbę liczników sprzętowych. Gdy poprosi się perf stat o więcej eventów niż jest rejestrów, narzędzie musi je multipleksować – liczyć część zdarzeń na zmianę. Skutkiem są wartości skalowane, o czym informuje kolumna time enabled / time running lub adnotacja o multiplexingu.

Jeśli odczyt eventu był aktywny przez ułamek czasu (time running < time enabled), perf skaluje go do pełnego okresu. To rozsądne przy równomiernym obciążeniu, ale już przy „burstowych” zachowaniach może zafałszować obraz. Rozwiązaniem bywa:

  • rozbicie pomiaru na kilka przebiegów z różnymi zestawami eventów,
  • użycie grup wydarzeń (-e '{cycles,instructions,branches}'), aby liczono je jednocześnie.

W kontekście profilowania aplikacji biznesowych na typowych serwerach to ograniczenie rzadko bywa krytyczne, ale przy analizie mikrokodów czy bibliotek numerycznych lepiej mieć je z tyłu głowy.

perf top i perf record/report – polowanie na gorące ścieżki

perf top – szybki podgląd hotspota bez nagrywania pliku

perf top przypomina top, ale operuje na symbolach i adresach instrukcji zamiast PID-ów. Domyślnie zbiera próbki z całego systemu, grupując je według funkcji. Typowy użytek:

Podstawowe wywołania perf top w praktyce

Najprostszy start to zwykłe wywołanie bez parametrów:

sudo perf top

Ekran domyślnie pokazuje procent udziału symboli w próbkach CPU (event cycles) z całego systemu. W pierwszych minutach pracy z perf top przydaje się zmiana kilku parametrów:

  • -p <PID> – filtrowanie na pojedynczy proces, gdy interesuje tylko jedna aplikacja:
sudo perf top -p 12345
  • -K lub --no-kernel – ukrycie symboli jądra, jeśli celem jest wyłącznie user space.
  • -g – włączenie próbkowania stosu (call-graph), co pozwala zobaczyć, skąd wywoływana jest dana funkcja.
  • -F <freq> – ustawienie częstotliwości próbkowania (próbek na sekundę). Umiarkowane 99–199 Hz zwykle wystarcza:
sudo perf top -p 12345 -g -F 199 --no-kernel

Takie wywołanie daje całkiem czytelny obraz gorących funkcji w procesie, z informacją o tym, które ścieżki wywołań prowadzą do hotspotów.

Interpretacja wyników z perf top – na co patrzeć w pierwszej kolejności

Interfejs perf top pokazuje procentowy udział symboli w ogólnej liczbie próbek. Typowy początek analizy to kilka prostych pytań:

  • Czy dominują funkcje z przestrzeni użytkownika, czy jądra?
  • Czy widoczne są oczekiwane miejsca (np. pętle obliczeniowe), czy raczej „klej” i biblioteki pomocnicze?
  • Czy procenty są skoncentrowane w kilku symbolach, czy rozproszone po całym kodzie?

Jeżeli na górze listy widać głównie funkcje typu schedule(), różne warianty sys_read(), sterowniki I/O albo mutex_lock(), a kod aplikacji jest daleko w dół – CPU prawdopodobnie spędza sporo czasu w blokadach, kolejkach lub na granicy syscalle/jądro. Wtedy samo mikrooptymalizowanie pętli w user space nie przyspieszy całości.

Jeśli z kolei na czele stoją funkcje z biblioteki standardowej lub frameworka (np. sortowanie, serializacja JSON, ORM), często lepszym ruchem jest zmiana strategii użycia biblioteki (inną metodą, inny format, caching) niż szukanie cykli w kodzie własnym.

Ograniczenia perf top – kiedy zrezygnować na rzecz nagrania

perf top działa w trybie ciągłym i „zapomina” o przeszłości, koncentrując się na ostatnich próbkach. To dobre przy długotrwałym, stałym obciążeniu (serwer HTTP pod stałym ruchem), gorzej przy krótkich burstach. Kilka typowych sytuacji, gdy lepiej sięgnąć po nagranie:

  • Problem pojawia się sporadycznie – raz na minutę przeciąga się request, ale perf top zwykle „patrzy” akurat wtedy, gdy nic szczególnego się nie dzieje.
  • Istotna jest korelacja z innymi zjawiskami (I/O, kolejki, scheduler), co wymaga późniejszej, spokojnej analizy.
  • Trzeba udokumentować wynik (raport, ticket, PR) – zrzut ekranu z perf top bywa mało przekonujący.

W takich przypadkach lepiej poświęcić chwilę na perf record z sensownym oknem czasowym, a następnie przejrzeć dane przez perf report lub flamegraphy.

Podstawy pracy z perf record – nagrywanie próbek

perf record tworzy binarny plik z próbkami (domyślnie perf.data), który później można analizować wielokrotnie. Najpierw prosty przypadek – uruchomienie programu pod kontrolą:

sudo perf record -g -- ./moja_aplikacja --tryb=benchmark

Najważniejsze opcje na start:

  • -g – call-graph, czyli zapisywanie stosu wywołań dla każdej próbki. Bez tego widać tylko pojedynczą funkcję, w której padła próbka, bez kontekstu.
  • -F <freq> – kontrola częstotliwości; wyższa częstotliwość daje lepszą rozdzielczość, ale zwiększa narzut i rozmiar pliku.
  • -p <PID> – attach do już działającego procesu:
sudo perf record -g -p 12345

W tym trybie nagrywanie kończy się zwykle po Ctrl+C, co wyznacza „okno obserwacji”. Długość okna ma istotne znaczenie: zbyt krótkie może nie złapać typowych ścieżek, zbyt długie rozmyje krótkie anomalie.

Wybór eventu i zakresu – nie tylko cykle CPU

Domyślne próbkowanie odbywa się zwykle po zdarzeniach cycles, co odpowiada pytaniu „w których miejscach spędzamy czas CPU”. To odpowiedni wybór w większości scenariuszy CPU-bound, ale nie zawsze:

  • Dominują pipeline stalls/cache misses – można spróbować próbkowania po cache-misses albo bardziej szczegółowych eventach PMU (LLC misses, stalled cycles front-end/back-end), jeżeli sprzęt je udostępnia:
sudo perf record -g -e cache-misses -p 12345
  • Problem dotyczy systemu plików lub sieci – sensowniejsze może być nagranie tracepointów jądra (np. syscalls:sys_enter_*, block:block_rq_issue, net:), a następnie analiza z użyciem perf script lub wyspecjalizowanych narzędzi.

Pomysł „próbkujmy wszystko naraz” jest kuszący, ale kończy się mieszanką eventów o różnej semantyce i nieczytelnym raportem. Zwykle lepiej zadać jedno konkretne pytanie i dobrać pod nie 1–3 eventy, zamiast losowego zestawu kilkunastu.

Call-graph i tryby rozwiązywania stosu

Rekonstrukcja stosu nie jest trywialna; zależy od ABI, kompilatora, obecności ramek (frame pointers) i wsparcia HW. perf ma kilka mechanizmów call-graphów:

  • --call-graph fp – oparty na wskaźniku ramki (frame pointer). Daje niezły rezultat, jeśli binaria były kompilowane z zachowanymi ramkami (-fno-omit-frame-pointer albo domyślnie na niektórych platformach).
  • --call-graph dwarf – wykorzystuje informacje debug (DWARF). Zwykle dokładniejszy, ale cięższy, wymaga obecności symboli debug w systemie.
  • --call-graph lbr – na niektórych CPU (Intel) wykorzystuje Last Branch Records, jest bardzo dokładny przy stosunkowo niskim narzucie, lecz zależny od konkretnej architektury.

Na typowym serwerze x86_64 z aplikacją pisaną w C/C++ dobrym punktem startu jest próba z fp, a jeśli stos wygląda ucięty lub dziurawy – przejście na dwarf i włączenie paczek -dbg / -debuginfo dla bibliotek i jądra.

Analiza nagrania przez perf report

Po zebraniu próbek następny krok to interaktywna analiza:

sudo perf report

Domyślny widok pokazuje listę symboli z procentowym udziałem w nagranych próbkach. W praktyce kilka funkcji interfejsu jest naprawdę przydatnych:

  • Nawigacja po hierarchii – wejście do konkretnego symbolu i przełączenie widoku na call-graph („drzewko” pokazujące, kto wywołuje tę funkcję i kogo ona wywołuje).
  • Filtry – zawężenie do konkretnego procesu, biblioteki, symbolu lub eventu. Pozwala np. skupić się na libstdc++ albo tylko na jądrze.
  • Sortowanie – przełączanie między widokiem „po symbolach”, „po plikach”, „po modułach”. Ułatwia wyłapanie np. bibliotek, które nieproporcjonalnie obciążają CPU.

Przy pierwszym kontakcie interfejs perf report może wydawać się toporny, ale daje jedną ważną rzecz: możliwość spokojnej, powtarzalnej analizy tego samego nagrania, bez presji czasu jak w perf top.

Symbole, debug info i „anonimowe” stosy

Bez symboli binarne wyglądają w raportach jak zlepek adresów lub nic niemówiących nazw. Typowy scenariusz produkcyjny wymaga kilku kroków:

  • Instalacja pakietów z symbolami debug dla jądra (linux-image-*-dbgsym lub odpowiednik) oraz bibliotek (na dystrybucjach RPM – -debuginfo).
  • Zadbanie, by aplikacja była zbudowana z symbolami (choćby w osobnym pakiecie) i by perf miało do nich dostęp na maszynie analitycznej.
  • Przy pracy z kontenerami – upewnienie się, że profilowanie odbywa się na hoście i że ścieżki do binariów są zgodne z tymi, które widzi perf.

W środowiskach z JIT (JVM, .NET, Go ze swoimi trickami, JavaScript) sprawa jest bardziej złożona. Potrzebne jest wsparcie po stronie runtime (mapowanie kodu JIT do symboli) lub użycie specjalizowanych narzędzi (async-profiler dla Javy, pprof dla Go). W przeciwnym razie duża część czasu CPU może zostać przypisana do anonimowych regionów pamięci.

Łączenie perf record z flamegraphami

Standardowy widok perf report bywa mało intuicyjny dla osób przyzwyczajonych do graficznych narzędzi. Jednym z popularnych sposobów wizualizacji jest flamegraph. Podstawowy pipeline wygląda tak:

  1. nagranie próbek z call-graph:
sudo perf record -F 199 -g -- ./moja_aplikacja --tryb=benchmark
  1. eksport do tekstu:
sudo perf script > out.perf
  1. przetworzenie skryptami z pakietu flamegraph (Brendan Gregg):
./stackcollapse-perf.pl out.perf > out.folded
./flamegraph.pl out.folded > flame.svg

Uzyskany plik SVG pokazuje stosy wywołań w postaci „płomieni”: szerokość prostokąta odpowiada liczbie próbek (udziałowi w czasie CPU), wysokość – głębokości stosu. Do analizy mikrooptymalizacji i ogólnego pejzażu hotspota jest to często wygodniejsze niż klikanie po drzewkach w perf report.

Typowe pułapki przy interpretacji flamegraphów

Flamegraph kusi prostą narracją: „najszerzszy słupek to nasz główny problem”. Bywa, że to prawda, ale nie zawsze:

  • Jeśli szeroka funkcja jest naturalnie ciężką częścią algorytmu (np. konwolucja w bibliotece numerycznej), to nie ona jest problemem, lecz może złe parametry wejściowe, niepotrzebne wywołania lub błędna architektura.
  • Słupki z jądra lub bibliotek mogą być konsekwencją złego wzorca użycia po stronie aplikacji. Przykład: bardzo szeroki memcpy w libc może wynikać z niepotrzebnych kopiowań w layerze aplikacji, a nie z tego, że memcpy działa „za wolno”.
  • Brak segmentów w pewnych częściach stosu (dziury) często oznacza problemy z symbolami lub call-graph, nie to, że „tam nic się nie dzieje”.

Rozsądne podejście to zestawienie flamegrapha z wcześniejszymi obserwacjami (np. z perf stat, top/htop, logami) i szukanie spójnego wyjaśnienia, zamiast sztywnego trzymania się „najszerszy słupek = winny”.

Minimalizowanie narzutu profilowania

Profilowanie zawsze wprowadza pewien narzut. W przypadku perf bywa on niewielki, lecz przy wysokich częstotliwościach próbkowania, głębokim call-graphie i bogatych eventach można łatwo przesadzić. Kilka prostych zasad ostrożności:

  • Unikanie zbyt wysokich częstotliwości (-F) na produkcji, jeśli nie ma wyraźnej potrzeby – częściej wystarcza 99–199 Hz zamiast 1000 Hz.
  • Profilowanie w wąskich oknach czasowych, zamiast stałego nagrywania przez wiele minut/godzin.
  • Wyłączanie zbędnych eventów i złożonych call-graphów, gdy celem jest ogólne rozpoznanie kierunku, nie analiza na poziomie mikroarchitektury.

Bywa, że błąd wydajnościowy jest wrażliwy na timing i profilowanie go „zamyka” (tzw. heisenbug). W takich przypadkach pomocne są krótsze, powtarzalne nagrania oraz porównywanie ich między sobą, zamiast polegania na jednym „idealnym” przebiegu.

Najczęściej zadawane pytania (FAQ)

Od czego zacząć, gdy „aplikacja wolno działa” na Linuxie?

Najpierw trzeba doprecyzować, co dokładnie znaczy „wolno”. Czy problemem jest wysoka latencja pojedynczych żądań, zbyt mała przepustowość (requests/s), czy może skoki opóźnień raz na jakiś czas. Bez takiego zawężenia każde narzędzie – czy to perf, top czy flamegraph – będzie jedynie generowało dane, których nie da się sensownie zinterpretować.

Praktyczny schemat jest prosty: definiujesz metrykę (latencja, przepustowość, zużycie CPU/I/O, stabilność), określasz, co jest nieakceptowalne, a dopiero potem dobierasz narzędzia. Inaczej lądujesz w trybie „profiling bez pytania”, który kończy się mnóstwem wykresów i brakiem decyzji, co realnie zmienić w kodzie lub konfiguracji.

Jak poprawnie używać perf: stat, top, record – który tryb kiedy wybrać?

Najczęstsza kolejność jest taka: najpierw perf stat, potem perf top, a dopiero na końcu perf record z flamegraphami. perf stat daje szybki, liczbowy przegląd: ile jest cykli, instrukcji, missów cache, branch mispredicts itp. To dobry filtr: czy problem w ogóle wygląda na CPU-bound.

perf top pokazuje „na żywo”, które funkcje zużywają najwięcej czasu CPU. Dopiero gdy wiesz, że patrzysz na właściwy proces i konkretny scenariusz obciążenia, ma sens uruchomienie perf record -g i późniejsze generowanie flamegraphu. Inaczej uzyskasz piękny, ale bezużyteczny obraz niewłaściwego kodu (typowy błąd początkujących).

Jak rozpoznać, czy aplikacja jest CPU-bound czy IO-bound na Linuxie?

Podstawą są proste metryki systemowe, zanim odpalisz cokolwiek cięższego. Jeśli CPU jest stale bliskie 100% (głównie w trybie user/system), iowait jest niski, a load average rośnie wraz z liczbą żądań, to typowy obraz CPU-bound. Z kolei przy I/O-bound widać wysoki iowait, wydłużone czasy odpowiedzi dysków w iostat -x oraz względnie niskie user/system CPU przy wysokim load.

W praktyce sytuacje mieszane są częstsze niż „czyste” CPU-bound/IO-bound. Na przykład: API teoretycznie jest CPU-bound, ale przy pikach ruchu zaczyna blokować się na bazie i profil CPU wprowadza w błąd. Dlatego diagnoza nie powinna kończyć się na jednym odczycie top – trzeba zestawić CPU, I/O, kontekst przełączeń oraz zachowanie aplikacji pod obciążeniem.

Kiedy użyć sampling (perf) zamiast tracingu (ftrace, eBPF) i odwrotnie?

Sampling (typowy tryb perf record) odpowiada na pytanie „gdzie średnio spędzamy czas CPU”. Działa statystycznie: pobiera próbki stosu wywołań co określony interwał. Jest tańszy wydajnościowo i zwykle wystarcza, gdy problemem jest ogólna wydajność CPU, a nie pojedynczy incydent czy szczegółowa sekwencja zdarzeń.

Tracing (ftrace, eBPF/BPF, LTTng, systemtap) jest potrzebny, kiedy liczy się kolejność i dokładny czas zdarzeń: złożone zależności między wątkami, rzadkie „przywieszki”, tajemnicze opóźnienia między warstwami systemu. Tracing generuje jednak znacznie więcej danych i przy nieostrym pytaniu badawczym łatwo utopić się w logach bez sensownych wniosków.

Jak nie utonąć w danych z perf i flamegraphów?

Podstawowy filtr to pytanie „co zmienię w systemie na podstawie tej informacji?”. Jeśli nie umiesz na nie odpowiedzieć, to zbierasz za dużo i zbyt szczegółowo. Zanim uruchomisz perf record na godzinę, ustal: jaki scenariusz obciążenia profilujesz, na jaką metrykę chcesz wpłynąć i o ile (choćby szacunkowo).

Klasyczna pułapka: widzisz „dużą” funkcję na flamegraphie i zaczynasz ją optymalizować, mimo że przy realnym ruchu odpowiada za niewielki fragment całkowitego czasu. Rozsądny nawyk to cross-check: zanim wejdziesz w mikrooptymalizacje, sprawdź w prostych metrykach (latencja, throughput, CPU%, iowait), czy poprawa tego fragmentu w ogóle może mieć zauważalny wpływ na cel biznesowy.

Jak profilować aplikacje w kontenerach (Docker, Kubernetes) za pomocą perf?

W kontenerach trzeba rozdzielić dwie kwestie: gdzie uruchomione jest perf i jak mapowane są PID-y oraz cgroupy. Najpewniejsze podejście to uruchamianie perf na hoście i celowanie w odpowiednią cgroup lub PID przypisany do kontenera (nowsze wersje perf obsługują profilowanie per-cgroup). Profilowanie wyłącznie „z wnętrza” kontenera bywa mylące, bo nie widzisz pełnego kontekstu systemu.

Typowy błąd: patrzenie na profil CPU w kontenerze i wniosek „aplikacja jest lekka”, podczas gdy throttling CPU lub inne ograniczenia na poziomie hosta sprawiają, że realnie proces dostaje ułamek czasu procesora. Dlatego dane z perf trzeba zawsze zestawiać z limitami cgroup, konfiguracją kube/Docker oraz metrykami hosta.

Jaka jest sensowna kolejność narzędzi: top, htop, pidstat, iostat, perf?

Rozsądny, mało inwazyjny workflow wygląda najczęściej tak:

  • 1. Szybki rzut oka: top, htop, uptime, free -m – ogólna kondycja systemu, load, użycie pamięci.
  • 2. Rozbicie CPU: mpstat -P ALL 1 – czy wszystkie rdzenie są obciążone, czy tylko część.
  • 3. Szczegóły per proces: pidstat -u -r -d -w 1 – kto realnie zużywa CPU, pamięć, I/O.
  • 4. Analiza dysków: iostat -x 1, opcjonalnie iotop – które urządzenia i procesy są I/O-bound.
  • 5. Dopiero teraz: perf stat, perf top, perf record na wybranym procesie lub cgroup.

Przeskakiwanie od razu do perf record bez wcześniejszej diagnostyki zwykle kończy się szczegółową analizą nie tego fragmentu systemu, który jest faktycznym wąskim gardłem. Kolejność ma znaczenie, bo oszczędza czas i zmniejsza ryzyko błędnych wniosków.