Napotkałem pewien problem pisząc aplikację w Javie która za pomocą JNI odwoływała się do natywnego kodu c++ który generował dużo wątków (ponad 1000) oraz alokował miliony obiektów, grupował i sortował. Aplikacja chodziła na kontenerze w kubernetes na maszynie która ma 64 CPU i 250 GB RAM, natomiast kontener ma do dyspozycji 8 CPU i 8 GB RAM. W pewnym momencie zauważyłem, że aplikacja zaczyna zjadać coraz więcej ramu, aż dochodzi do 8 GB i kubernetes restartuje kontener. Kod c++ był sprofilowany za pomocą valgrind więc założyłem że to nie to, skoro tam nie ma wycieków. Następnym krokiem była analiza JVM, tam też nie znalazłem żadnego wycieku pamięci.

Rozpocząłem analizę procesu w systemie i okazało się, że pmap pokazuje mi:

total         45999828K

czyli proces może sobie zaalokować ponad 43 GB RAMU, a z faktu, że system operacyjny węzła ma wolne ponad 200 GB to proces sobie alokuje coraz więcej pamięci. Z czego to wynika?

Standardowy alokator libc alokuje pamięć per CPU – 8 przestrzeni (Area) * 64 MB. Jest to widoczne w sytuacji gdy tworzonych jest bardzo dużo wątków. Alokator optymalizuje alokowanie pamięci i nie zwalnia bloków pamięci, które potem wykorzystuje do kolejnych operacji. Brzmi nieźle póki aplikacja widzi tyle CPU i RAM co może wykorzystać, niestety w przypadku kubernetesa widzi całość zasobów.

Nie pozostało nic innego jak zacząć szukać rozwiązania i tak doszedłem do jemalloc – doskonale sobie radzi z pamięcią. Nagle aplikacja która po kilku nastu godzinach wykładała się przy 8 GB zaczyna zużywać 1 GB RAM. pmap pokazuje:

total          6979264K

Benchmarki wykonane przez h2load również pokazują wzrost wydajności o około ~10%

Wkręcając się w alokatory pamięci szukałem dalej, może coś jest jeszcze lepsze. W ten sposób trafiłem na tcmalloc

Powtórzyłem wszystkie testy, zużycie RAMu nieco większe niż w przypadku jemalloc, ale mniejsze zużycie CPU.

pmap pokazał następującą wartość:

total          9884268K

czyli więcej niż w przypadku jemalloc natomiast benchmark wykonany przez h2load pokazał kolejny wzrost wydajności, tym razem o blisko 25% od jemalloc

Żeby mieć pewność wykonałem dodatkowe testy i sprofilowałem aplikacje. GC w JVM za wiele nie pokazał ponieważ tam nie było zbytniego zużycia pamięci. G1 działa wystarczająco sprawnie, choć daje się, że tym razem jemalloc ma największy wpływ na poprawę wydajności choć jest to znikomy przyrost

Kolejnym testem to benchmark kodu w c++

libc:

Widać, że funkcja, której głównym procesem jest alokowanie pamięci (13.5%) jest droższa od funkcji sortującej

jemalloc:

Tu widać, że funkcja sortująca jest droższa niż funkcja alokująca pamięć, funkcja alokująca pamięć zajmuje 4.8%

tcmalloc:

tutaj widać, że różnica pomiędzy funkcjami wynosi 3%, również całkowy czas obu operacji był najkrótszy.

Wywołania funkcji malloc w valgrind:

libc

jemalloc

tcmalloc

Jak widać ogromna przepaść pomiędzy libc a jemalloc, natomiast między jemalloc a tcmalloc aż tak dużej różnicy nie ma.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *