Wasze Historie #3: Ku Pamięci. Dla Zdrowia.

2

Zaczęło się to wszystko tak: Klient, był w posiadaniu dosyć prostej biznesowo aplikacji, która służyła do monitorowania w czasie rzeczywistym pewnych danych. Jak nietrudno się domyślić przy tego rodzaju aplikacji, bardzo łatwo o nieszczęśliwy wypadek w postaci braku pamięci.

Tak też było w tym przypadku: mniej więcej co 3-4 tygodnie, w zależności od intensywności eksploatacji, aplikacja wykrzaczała się. W związku z tym jeden z pracowników klienta był zobowiązany do restartowania serwera aplikacji, na którym była uruchomiona aplikacja zanim ta się rozkraczy. Pewnie zrobiło się to uciążliwe i monotonne, bo poproszono mnie o pomoc.

I wtedy wchodzę ja, cały na biało…

Niniejszy post jest częścią cyklu "Wasze Historie".
Autor: Kunka

Nieee to jeszcze nie teraz… Stos technologiczny – w postaci angulara i atmosphere’a (websocket) z przodu i scali, springa, camela, akki i rabbita z tyłu – już znałem, dlatego na wstępie wytłumaczono mi, na czym polega błąd i od jak dawna występuje. Jak się okazało, jest to po prostu klasyczny OutOfMemoryError i nic specjalnego z aplikacją robić nie trzeba. Wystarczy jej używać. A sam problem występuje od bardzo dawna i że radzono sobie z tym do tej pory w sposób, jaki opisałem wyżej.

Na moje pytanie o to, czy są jakieś dumpy pamięci z ostatnich crashy otrzymałem tylko pełną skrępowania odpowiedź, jasno dającą mi do zrozumienia, że nie mają nic takiego… No dobra, trzeba będzie się trochę bardziej pobrudzić.

Dostałem wjazd na środowisko developersko/testowe, którego produkcja jest klonem. Zacząłem od konfiguracji serwera aplikacji, tak ażeby ten zrzucał stan pamięci w przypadku krachu. Zrobiłem, przetestowałem. Działa? Działa!

Zabrałem się więc za skrypt do selenium, który klikając po apce zreprodukuje mi błąd. Klikając z 8 kart przeglądarki udało mi się apkę wykrzaczyć w 3 doby. Niezły wynik swoją drogą.

Chodząc po bagnie

Jako, że dużego doświadczenia nie mam w przeglądaniu zrzutów pamięci, to czułem się jak w bagnie. W tym podejściu nie znalazłem w zasadzie nic co by mnie interesowało.

Oprócz tego, że instancje char[] i String zajmowały mniej więcej 60% zasobów pamięci w momencie crasha.

Operacja na żywym organizmie

Jako, że analiza dumpa doprowadziła mnie donikąd, postanowiłem podpiąć się debuggerem i przejść dokładnie krok po kroku od momentu otrzymania wiadomości z rabbita, przez parsowanie, po wypchnięcie danych do websocketa, by sprawdzić co się tam dzieje.

Scala+Camel+Akka to niezłe wyzwanie dla debuggera w IDE na którym pracowałem. Prawie niemożliwe było przejście z instancji klasy, która wiadomość wysyłała do instancji klasy, która wiadomość miała otrzymać. Grzęzłem w applyach, wewnętrznych traitach i klasach frameworku… By móc efektywnie poruszać się po aplikacji, musiałem wiedzieć w instancji której klasy wiadomość zostanie odebrana.

Pierwszy trop

Całość zajęłaby mi jeszcze dłużej, gdyby nie przypadek, że zerwało mi połączenie VPN z siecią klienta. Grzebiąc w klasach odpowiedzialnych za połączenie z rabbitem natrafiłem na ręczny mechanizm sprawdzania “stanu zdrowia” połączenia, tzw. health-check. Z changelogów rabbita i biblioteki klienckiej do niego wynikało, że gdy aplikacja była pisana, nie bylo jeszcze wsparcia dla health-checków.

Mechanizm był dosyć dziwny. W przybliżeniu była to nieskończona pętla, która sprawdzała stan flagi, która mówiła o tym, czy otrzymaliśmy wiadomość z rabbita w przeciągu ostatnich 8 sekund. Jeśli nie było takiej wiadomości, flaga miała wartość false i połączenie było zrywane. Zrywane było połączenie a nawet trochę więcej… Więcej, bo odrejestrowywana była cała ścieżka camela, która była zarejestrowana przez akkę jako endpoint dla rabbita, a następnie znowu rejestrowana, by ustanowić nowe połączenie.

Dziwnie, co? Co dziwniejsze, mechanizm wydawał się działać mimo swojej absurdalnej natury, dlatego też zostawiłem to tak jak było. Po co naprawiać coś, co działa?

Drugie podejście do dumpa

Siedząc w beznadziei postanowiłem wrócić do przeglądania dumpa, którego wypluła mi wirtualna maszyna, zanim wyzionęła ducha.

Przy drugim podejściu rzuciły mi się w oczy dwie rzeczy. Spora liczba obiektów odpowiedzialnych za utrzymywanie websocketów atmosphere’a przy życiu i obiekty DeadLetter z wnętrza Akki. Na Akkę nikt wcześniej nie narzekał, a o Atmoshpere słyszałem różne rzeczy od współpracowników.

Więc na ruszt jako pierwszy poszedł Atmoshpere. Sprawdziłem changelogi od wydania wersji, z której apka korzysta, do teraz. Ku mojej uciesze znalazłem poprawki, których oczekiwałem “Fixed, memory leaks Issues…” – BINGO! Uradowany zrobiłem migrację biblioteki na wyższą wersję, podniosłem wersję serwletu do 3.0 tak by amosphere był z nim w stanie gadać na tomcat. Gimnastyki byłoby sporo, gdyby nie współpracownik, który już to kiedyś robił. Budowa paczki, wdrożenie, testowanie i… ROZCZAROWANIE!

Apka wytrzymała nieco ponad 2 tygodnie, więc rezultat i tak był całkiem niezły. Przez 2 tygodnie zdążyłem zapomnieć o tym co widziałem w dumpie, a co dotyczyło Akki, więc znowu byłem w punkcie zero.

Cudowny VPN

Podczas przeglądania dumpa i debuggowania naprzemiennie, po raz kolejny VPN odmówił mi posłuszeństwa i niczym mistyczne bóstwo starał się mnie nakierować na rozwiązanie całej zagadki. Okazało się, że zerwane połączenie VPN, którego nie odnowiłem, doprowadziło do reakcji łańcuchowej w mechanizmie health-check. Brak danych -> flaga -> odrejestruj endpoint -> zarejestruj endpoint -> brak danych -> flaga -> odrejestruj -> zajerejstruj…

I tak było to powtarzane przez przeszło godzinę, aż w końcu Camel z Akką na czele również zaczęli odmawiać posłuszeństwa. Camel przestał rejestrować endpoint, który zlecała mu Akka – twierdził, że został odrejestrowany niepoprawnie, a odrejestrowanie nic nie dawało.

Przyczyna?

Przyczyną były mechanizmy dead-letters Camela i Akki. Składowały one w pamięci wiadomości, które najprawdopodobniej nie zostały doręczone do endpointa. Endpointa, który nie istniał (bo został odrejestrowany).

Niezła faza, co? A byłem tego tak blisko przy drugim podejściu do analizy dumpa pamięci.

Rozwiązanie

Jako, że dużo wcześniej została podniesiona wersja rabbita, z którego korzystała aplikacja, udało się zrobić Health-Check “Po Bożemu” i pozwolić klientowi rabbita obsługiwać pulę połączeń i same połączenia we własnym zakresie. Oczywiście wyrzucając wcześniej kawałek kodu odpowiedzialny za ręczne sprawdzanie “stanu zdrowia” połączenia co ostatecznie rozwiązało problem.

No i w zasadzie nie wszedłem na biało ;)

Share.

About Author

Programista, trener, prelegent, pasjonat, blogger. Autor podcasta programistycznego: DevTalk.pl. Jeden z liderów Białostockiej Grupy .NET i współorganizator konferencji Programistok. Od 2008 Microsoft MVP w kategorii .NET. Więcej informacji znajdziesz na stronie O autorze. Napisz do mnie ze strony Kontakt. Dodatkowo: Twitter, Facebook, YouTube.

2 Comments