Śledzenie zużycia pamięci

 
Obecnie programiści dużo rzadziej zwracają uwagę na to, ile pamięci zużywa aplikacja, nad którą pracują. Komputery osobiste mają gigantyczne zasoby RAMu, spokojnie wystarczające do obsługi zasobochłonnych aplikacji, głównie gier. Pamięć wirtualna pozwala z kolei uwolnić się od zmartwień o pamięć fizyczną, ot najwyżej dysk będzie trochę chrobotał od czasu do czasu. Programiści z kolei mają dodatkowo do dyspozycji języki programowania, w których praktycznie wcale nie muszą się martwić o zarządzanie pamięcią. Problem poprawnej alokacji i dealokacji załatwiają przeróżne garbage collectory.
Są jednak takie obszary, w których programiści muszą się liczyć z ograniczonymi zasobami. Jednym z takich obszarów jest oprogramowanie serwerowe, szczególnie takie, które w założeniu musi być wysokowydajne, na przykład takie, które relizuje strumieniowanie multimediów. Aplikacja musi utrzymywać po swojej stronie znaczne bufory na ramki wideo dla poszczególnych plików/kanałów. Zbyt wielu użytkowników naraz i nagle pamięć fizyczna ulega wyczerpaniu, co doprowadza do swap’owania aktualnie nieużywanych stron pamięci na dysk. To z kolei powoduje olbrzymi spadek wydajności aplikacji i może doprowadzić do problemów z funkcjonowaniem usług.
Drugim obszarem są systemy wbudowane (embedded). Z reguły zasoby takich komputerów są mocno ograniczone i o rzędy wielkości mniejsze, niż zasoby komputerów osobistych. Mimo, że w wielu architekturach jednostka zarządzania pamięcią (MMU – Memory Management Unit) jest dostępna i wykorzystywana przez system, to może się okazać, że nie ma co liczyć na swap. Ale nierzadko MMU zwyczajnie nie ma, a pamięć fizyczna jest adresowana wprost. Wtedy zarządzanie pamięcią staje się jednym z kluczowych zadań – programista nagle zaczyna zwracać uwagę na takie subtelności, jak wielkość pliku wykonywalnego, wielkości bibliotek, rozmiary stosów, alokacje statyczne podczas inicjalizacji.
Niezależnie od charakteru tworzonej aplikacji, zawsze warto wiedzieć, jak sprawdzić zużycie pamięci przez proces, stąd kilka rad.

1. ps u

Dlaczego nie skorzystać z ps-a? Wyświetla sporo informacji o procesach, a z argumentem ‘u’ dodatkowo podaje pewne informacje na temat zużycia pamięci.

VSZ jest tutaj całkowitym zaalokowanym rozmiarem pamięci wirtualnej. Wielkości podawane są w kilobajtach. Widać, że myApp ma dość kosmiczne wymagania, a to głównie dlatego, że linkuje się z kilkunastoma wielkimi bibliotekami do przetwarzania multimediów. RSS określa tutaj zestaw roboczy stron, czyli wielkość fizycznej pamięci, zajmowanej aktualnie przez proces. VSZ można by rozumieć jako deklarację procesu odnośnie tego, ile miejsca mógłby zająć, gdyby chciał się zmieścić w RAMie w całości. RSS mówi ile pamięci fizycznej jest aktualnie w użyciu.
Po obciążeniu aplikacji myApp masą roboty uzyskamy całkiem inny obraz:

VSZ podskoczył do prawie 800 MB, a RSS aż do 100 MB. Teraz zastanawiam się, czy mój program cały czas zużywał 100MB RAMu, czy może bywał bardziej zachłanny w trakcie swojego działania.

2. top

Co powie mi top? W zasadzie to samo, VIRT to inaczej VSZ, RES to RSS, a SHR to pamięć współdzielona z innymi procesami. Niewątpliwie zaletą jest tutaj stały podgląd zużycia zasobów, procesora i pamięci na tle innych procesów. Jeśli w aplikacji znalazłby się znaczny wyciek pamięci, to dałoby się to zaobserwować statytykach top-a.

 3. /proc/<pid>/status

Naprawde przydatne informacje odnośnie pamięci kryją się w /proc/<pid>/status:

 

VmSize  jest znaną skądinąd wielkością pamięci wirtualnej zaalokowanej przez proces ale już VmPeak podaje szczytową wartość zaalkoowanej pamięci wirtualnej, co jest dużo przydatniejszą informacją. Podobnie sprawa wygląda z wielkością pamięci fizycznej wykorzystywanej przez proces, czyli VmRSS.  VmHWM jest szczytowym zużyciem pamięci fizycznej przez proces (HWM to skrót od high water mark co dosłownie oznacza najwyższy odczyt na wodowskazie, analogia dość celna). Mamy też dość bezużyteczne pola jak VmData i VmStk. VmStk podaje wielkość stosu wątku głównego, dość bezużyteczna informacja w przypadku aplikacji wielowątkowych. Z kolei VmData zmienia się proporcjonalnie do VmSize, ponieważ VmData jest sumą rozmiarów segmentów kodu (text), segmentów danych statycznych (bss) i segmentu danych (data) – zawierającego stertę oraz rozmiaru stosu. Wartość jest zawsze niższa niż VmSize ale bardziej wiarygodna, ponieważ VmSize podaje również wielkość stron mapowanych w pamięci wirtualnej ale jednocześnie nieużywalnych (PROT_NONE). Tak więc VmSize w zasadzie daje nam informację o całkowitym stopniu zużycia przestrzeni adresowej, podczas gdy VmData podaje ile z tej przestrzeni adresowej w ogóle może być użyte przez proces, ponieważ zawiera kod lub dane. Tutaj jest to dośc wyczerpująco opisane.
Jest jeszcze VmSwap, które podaje wielkośc pamięci wyswapowanej na dysk. Niestety nie ma tutaj informacji o szczytowej wielkości swap’u. Jednak gdyby zdarzyło nam się zaobserwować VmSwap > 0, to znaczy, że nie mieścimy się w fizycznej pamięci, co może być alarmujące.

4. /proc/<pid>/smaps

Tutaj znajdziemy mapę pamięci naszego procesu – wylistowane wszystkie obszary pamięci wirtualnej, jakie alokuje nasz proces. Do tych obszarów zaliczają się też te zaalokowane w bibliotekach współdzielonych. W moim przypadku każda z bibliotek, a jest ich kilkanaście, alokuje dodatkowe 2MB pamięci wirtualnej której nie używa i nigdy nie użyje (patrz VmSize wyżej) – jest to pewien mechanizm linkera, wyjaśniony tutaj, który ma zapewnić poprawne współdzielenie bibliotek między procesami. To niestety powoduje, że nawet małe zgrabne aplikacyjki, ale wykorzystujące bardzo wiele bibliotek dynamicznych, podczas działania pożerają gigantyczną częśc przestrzeni adresowej, mimo, że faktycznie nie zużywają nawet promila tego, co pokazuje VmSize.

“VmFlags: rd ex” oznacza obszar pamięci z kodem wykonywalnym,  “VmFlags: rd” to dane tylko do odczytu, natomiast “VmFlags: rd wr” to pamięć swobodnie dostępna, czyli np. stos lub sterta. Przy czym sterta niekoniecznie musi mieć w wydruku z /proc/xxx/smaps etykietę [heap]. Większe alokacje, powyżej MMAP_THRESHOLD na pewno nie będą jej miały, a dodatkowo biblioteki współdzielone mogą implementować własne zarządzanie pamięcią, pomijając malloc/new i korzystając z własnych mechanizmów a następnie z mmap().

Widać tutaj też tę, zagadkową do niedawna, 2MB “dziurę” (2044 kB) w przestrzeni adresowej między obszarem kodu a obszarem danych.

Dzięki smaps możemy przekonać się, jaka jest struktura pamięci wirtualnej wykorzystywanej przez proces i skąd bierze się wartość VmSize i VmRSS. Możemy się dowiedzieć również, które obszary są aktualnie wyswapowane na dysk. Jednak przydałaby się możliwość stałego monitorowania zużycia pamięci w czasie. Są do tego niezliczone skrypty, w większości korzystające z danych w /proc/ . Zamiast jednak obserwować proces z zewnątrz, można skorzystać z profilera pamięci, czyli w pewnym sensie obserwować zjawiska zachodzące wewnątrz procesu.

5. Valgrind Massif

Valgrind jest aktualnie najbardziej wszechstronnym profilerem do debugowania problemów z pamięcią. Głównie załatwia się przy jego pomocy wycieki pamięci i mazanie po stercie (heap corruption) – na problemy ze stertą, szczególnie w oprogramowaniu wielowątkowym, naprawdę nie ma zbyt wielu sposobów, oprócz Valgrinda jest jeszcze dmalloc lub Electric Fence. Jednak nie nadają się one do monitorowania zużycia pamięci. Do tych zadań Valgrind ma narzędzie o nazwie massif. Domyślnie rejestruje ono wszelkie alokacje i dealokacje wywołane za pomocą malloc()/free() i new/delete, jednak można skłonić to narzędzie do rejestrowania na niższym poziome, czyli alokacje poprzez mmap/mremap/brk (–pages-as-heap=yes), które operują na całych stronach (4kB).

Po wywołaniu Valgrinda z Massifem dostajemy plik z próbkami, w zasadzie nieczytelny, stąd jak w przypadku innych profilerów mamy narzędzia do wizualizacji danych. Valgrind znacznie spowalnia naszą aplikację, w wielu przypadkach z aplikacji w ogóle nie da się korzystać podczas profilowania, co nie znaczy, że nie da się wyśledzić problemów i ich przyczyn.

Z narzędzia ms_print dostajemy nawet toporny ale czytelny wykresik, pokazujący zużycie pamięci w czasie, a niżej Valgrind próbuje wytknąć winowajców co większych alokacji. W pierwszej próbce za 79% zaalokowanej pamięci odpowiada enkoder x264, zresztą w kolejnych próbkach było podobnie. x264 po prostu jest żarłoczny.

A oto ładna wizualizacja wyników z massif’a, za pomocą massif-visualizer:

massif

Valgrind odpowie nam na wiele pytań, na które nie byłyby nam w stanie odpowiedzieć narzędzia z pkt. 1-4 – na przykłąd skąd się biorą poszczególne alokacje. Jednak korzystanie z niego jest dość skomplikowane, a do tego nie zawsze udaje się go zastsosować i pożera ogromne zasoby. Jednak zdecydowanie warto spróbować, nie tylko do szukania wycieków pamięci czy szkodliwego kodu pląsającego po stercie zanieczyszczając cudze bufory.

 

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *