Docker w środowisku Javy. Problemy i wyzwania

Aplikacje biznesowe są coraz bardziej złożone, wymagamy od nich niezawodności oraz ciągłej dostępności (tak zwany Zero Downtime Deployment). Wspomagając biznes często musimy iść w skomplikowane rozwiązania, np. kilka baz danych (w tym relacyjnych i nie relacyjnych). Wymagamy niskich kosztów oraz wysokiej dostępności w razie potrzeby, czyli auto-skalowania. Jednym z narzędzi jakie musimy użyć jest konteneryzacja przy pomocy narzędzia Docker, która bardzo dobrze integruje się z nowoczesnymi rozwiązaniami chmurowymi.

Andrzej Sydor. Team Leader Senior Software Developer w Objectivity. Posiada ponad dziesięcioletnie doświadczenie w pisaniu aplikacji, głównie w języku Java. Zafascynowany kulturą DevOps i architekturą mikroserwisową, a co za tym idzie rozwiązaniami konteneryzacji Docker, Kubernetes i rozwiązań chmurowych. Szkoli, dzieli się wiedzą i mentoruje w szkołach IT. Miłośnik dobrych meetup’ów.


W tym artykule skupimy się na konteneryzacji serca aplikacji, tak zwanego ’backendu’, który najczęściej jest pisany w Javie lub języku działającym na wirtualnej maszynie Javy (JVM).

Java – zapotrzebowanie pamięci i procesora

Pisząc aplikacje w Javie, jak i w innych językach musimy mieć na uwadze zapotrzebowanie na procesor i pamięć. Szeroka gama frameworków nie ułatwia zadania. Od najprostszych aplikacji, w którym sami zarządzamy wątkami, tworzymy je, dbamy o prawidłowy cykl życia i uważamy, aby nie napisać kodu, którego problemem jest wyciek pamięci. Poprzez proste i lekkie frameworki, jak javalin, Dropwizard czy RESTX, kończąc na rozwiązaniach ciężkich, typu ‘enterprise’ wykorzystujących standard Jave EE lub Spring Framework, mogących korzystać z różnych serwerów, np. webowych Apache Tomcat, Netty, jak i enterprise: WildFly lub IBM WebSphere.

Wybierając jedno z powyższych rozwiązań jesteśmy w stanie skonfigurować serwer oraz przekazać parametry dla JVM’a takie jak maksymalna ilość utworzonych wątków oraz możliwą pamięć do wykorzystania dla stosu i sterty w Javie. Tutaj korzystamy między innymi z parametrów Xmx, Xms i Xss’ Biorąc oczywiście jeszcze pod uwagę konkretną wersje Javy, na której uruchamiamy aplikacje.

Prawidłowe i najbardziej optymalne ustawienia wirtualnej maszyny Javy nie jest trywialną sprawą. W przypadku użycia konteneryzacji dochodzi kolejny poziom skomplikowania. Załóżmy, że aplikacja uruchomiona w kontenerze widzi zasoby hosta, a nie przydzielone danego kontenerowi, co tak naprawdę dzieje się w starszych wersjach Javy. Aplikacyjne zasoby są w stanie wyjść poza ramy ustawione dla kontenera, jak w takim przypadku zareaguje nasza aplikacja? Co się z nią stanie? Konkretnej odpowiedzi nie ma. Każdy przypadek jest indywidualny co cechuje inna wersja Javy czy inny problem napotkany dla systemu.

Jedno jest pewne, jest to niekomfortowa sytuacja dla serwera, jak i dla nas programistów. Nieraz mały problem jest w stanie narobić wielkie szkody. Załóżmy, że mamy kontener, który napotkał taki problem. Ustawiony jest na restart w każdej problemowej sytuacji. Jak to będzie oddziaływać na całość systemu, inne mikroserwisy oraz host’a lub chmurę, na której jest nasza aplikacja? O ile nie będę przedstawiał możliwych rozwiązań, to przedstawię na przykładach problem z jakim nasza aplikacja w kontenerze musi się zmierzyć.

Środowisko testowe / uruchomieniowe

Przykłady będę uruchamiał na środowisku testowym http://play-with-docker.com’. Rezultaty mogą być odbiegać jeśli użyjecie własnych środowisk z zainstalowanym Dockerem lub nawet na środowisku testowym przedstawionym w artykule, nie są mi znane dokładne zasady przydzielania zasobów dla konkretnych instancji.

Testy poprawek Javy

Poniżej kod źródłowy pliku budowania obrazu Dockerfile, który będzie nam pomocny przy testach. Przedstawię dwa testy, jeden z poprawkami w nowszej Javie 11 oraz drugi w starszej Javie 7 dla zobrazowania problemu jaki dotyczył Javy jeszcze jakiś czas temu.

Kod źródłowy w języku Java, który wypisuje ilość dostępnych procesorów oraz maksymalna możliwa pamięć do zarezerwowania widoczna dla danego środowiska uruchomieniowego.

Kod źródłowy budujący kontener z prostą aplikacją Javy (jak wyżej) działający na konkretnej siódmej wersji wirtualnej maszyny Javy.

Komendy, które należy wykonać aby zbudować kontener bazując na Dockerfile znajdującym się w aktualnym folderze. Uruchomienie kontenera i w ostatnim kroku zczytanie logów naszego prostego programu. (za <version> podstaw aktualnie testowaną wersję Javy, zapobiegnie to konflikcie nazw kontenerów)

Uzyskane wyniki dla poszczególnych wersji Javy:

Podsumowując ćwiczenie przez jakie przebrnęliśmy i uzyskane wyniki łatwo zauważyć, że można się spotkać z problemem opisanym prędzej. Najlepszą metodą jest zweryfikowanie i przetestowanie konkretnie używanej wersji wirtualne maszyny Javy. Wersja główna Javy, np. 9, która nie obsługuje poprawnie kontrolowanych przez Dockera parametrów, może uzyskać łatki tzw. Patch’e, które naprawią problem w kolejnych wersjach z oznaczeniem minor.

Wydajność Javy na przykładzie Spring Framework

Aby zobrazować lepiej i zmierzyć w bardziej realny sposób wydajność przebrniemy przez kolejny przykład. Zbudujemy aplikacje webową, używając Spring Framework. Wystawimy REST’owy endpoint, który dla zadanej liczby przekazanej w argumencie wywołania stworzy listę wylosowanych liczb całkowitych. Dla każdego elementu listy znajdzie wszystkie odpowiadające liczby pierwsze.

Opisana wyżej funkcjonalność wyliczania liczb pierwszych i endpoint REST’owy. Zrealizowana w Spring Boot.

Dla tego serwisu przygotujemy Dockerfile, który podczas budowania będzie tworzyć projekt ‘maven’owy z wymaganymi przez nas zależnościami Spring Boot’a. Skopiuje przygotowany plik Javy, aby w ostatnim kroku uruchomić całość.

Dockerfile przygotowujący aplikację REST’ową.

Do zbadania wydajności użyję narzędzia ab (Apache Bench). Apache Bench umożliwia wykonanie wielu żądań HTTP [przełącznik -n] przez wiele równoległych wątków [przełącznik -c]. Budujemy odpowiedni obraz Docker’a z przedstawionym narzędziem.

Dockerfile z narzędziem Apache Bench i Curl.

Całość musimy połączyć siecią, aby obrazy widziały się oraz poinformować, że przynależą do tej samej sieci.

Komendy niezbędne dla uruchomienia naszego przykładu.

W kolejnym kroku warto sprawdzić czy na pewno kontenery mogą się komunikować ze sobą.

Zestaw komend testujących sieć pomiędzy kontenerami.

Omówmy uzyskane wyniki załączone poniżej. Odpalone zostały testy, dokładnie tysiąc żądań do serwera, konkretnie naszej opisanej metody wyliczającej liczby pierwsze. Po maksymalnie pięćdziesiąt żądań równolegle. W taki sposób zostały przetestowane obydwa kontenery, czyli z ograniczeniem do jednego wątku i drugi z większą ilością wątków. Łatwo zauważyć, że kontener z ograniczeniem jest prawie o połowę wolniejszy jeśli chodzi o odpowiedzi. Oczywiście współczynników może być więcej, np. jak sam serwer Tomcat’a radzi sobie z różną ilością wątków, który jest używany standardowo przez Spring Boot Framework. Każde podejście trzeba ocenić jednak indywidualnie i przeprowadzić testy wydajnościowe ale często może się okazać, że jest to lepsze rozwiązanie. Czyli zwiększenie zasobów konkretnego kontenera, niż tworzenie kilku instancji.

Wyniki testu kontenera z ograniczeniem ilości wątków do jednego.

Co dalej – Java, Docker…

Oczywiście temat jest bardzo obszerny. Przybliżę tylko kierunki, które można obrać w danym temacie. Przykład jest bardzo prosty, ma zilustrować tylko problem. Sytuacja jest dosyć prosta przy aplikacjach monolitycznych, jeden kontener, jedna wersja wirtualnej maszyny Javy.

W podejściu mikroserwisów, dodatkowo możemy mieć wiele aplikacji, każda działająca na innej wersji wirtualnej maszyny, a nawet napisanych w różnych językach działających na JVM’ie, np. Java, Scala, Kotlin, Groovy… Poziom skomplikowania rośnie.

Kolejnym elementem układanki jest orkiestrator. Do wyboru mamy kilka, będąc w temacie blisko Docker’a musimy zwrócić uwagą na Docker Swarm oraz Docker Enterprise. Jednak największym graczem wydaje się Kubernetes. Przez ostatni czas zyskuje duży udział rynku, wspierany przez największe platformy typu AWS, Azure czy Google Cloud Platform. Orkiestrator zapewnia m.in. zarządzanie, wdrażanie i auto skalowanie aplikacji.


Zdjęcie główne artykułu pochodzi z unsplash.com.

Zapraszamy do dyskusji

Patronujemy

 
 
Polecamy
Kiedy skupić się na długu, a kiedy na funkcjonalnościach?