Sytuacja kobiet w IT w 2024 roku
10.09.20196 min
Guillaume Blaquiere

Guillaume BlaquiereScrum master / Lead DevVeolia

Wydajność frameworków Javy na Cloud Run

Sprawdź, jak frameworki Javy Spring Boot i Micronaut radzą sobie z zimnym startem na Cloud Run.

Wydajność frameworków Javy na Cloud Run

Latem 2018 r., w Google Next w San Francisco, mieliśmy z kolegami okazję poznać Sterena. Mówił nam o nowym produkcie i pozwolił nam wypróbować go w wersji alpha. Była to pierwsza wersja Cloud Run.

Szczęśliwy i zaszczycony tym, że udało mi się zapakować moją ulubioną aplikację Spring Boot do kontenera, wdrożyłem ją i… CO? 30 sekund uruchamiania…

Zdesperowany, kontynuowałem testowanie produktu, w większości z nowymi aplikacjami (głównie funkcje Pythona spakowane do kontenerów), a nawet nauczyłem się Go, aby uzyskać szybsze uruchamianie i lepszą wydajność przetwarzania.

Jednak jestem programistą Javy od 15 lat i nie odpuściłem sobie tego języka po pierwszym złym doświadczeniu ze światem serverless. Co więcej, mniej stabilny język, taki jak Python, staje się coraz bardziej nieefektywny i bardziej podatny na bugi wg mojego zespołu, który składa się również z początkujących programistów. Go był zbyt „nowy”, aby znaleźć programistę naprawdę doświadczonego w tym języku. Tym samym Java pozostała jednym z najlepszych kompromisów.

Jeśli miałbym wybrać framework Javy o najlepszej wydajności, to na który bym postawił?

Jak porównywać frameworki

Jestem starszym programistą Javy, ale nie znam wszystkich sztuczek optymalizacyjnych każdego z frameworków. Nawiasem mówiąc, aby być jak najbardziej neutralnym, zdecydowałem się napisać endpoint HelloWorld HTTP GET, używając dwóch podstawowych frameworków: Spring Boot i Micronaut.

W rzeczywistości nie napisałem niczego nowego, wykorzystałem gotowe przykłady. Nie będę tu opisywał, jak zostały stworzone od A do Z, gdyż strony frameworków mają naprawdę dobre dokumentacje. Kod znajdziesz na końcu tekstu.

Napisałem również endpoint HTTP w czystej Javie, aby sprawdzić, jak framework wpływa na wydajność.

Każdy test zawarty został w kontenerze i został wgrany na Cloud Run.


Spring Boot

Spring Boot to mój ulubiony framework. Korzystałem z niego przez wiele lat i nadal kocham jego “poręczność” i prostotę. Masz jakieś extra zadanie do zrobienia? Nie ma problemu, spring ma na to komponent! Dodaj adnotację i korzystaj!

Jedną z jego zalet jest automatyczne ładowanie. Dodaj po prostu zależność i a Spring boot przy starcie zeskanuje zależności i automatycznie załaduje wszystko co potrzeba. Twoja klasa to wstrzykiwalny bean? Nie musisz jej deklarować, dodawać adnotacji, a program załaduje się automatycznie. Oczywiście im więcej masz zależności, tym więcej plików trzeba przeskanować i tym dłuższy będzie czas uruchamiania.

Ta cecha w nieskalowalnym środowiskiem, takim jak on-premise, jest idealna. Uruchamiam aplikację raz i pozostaje ona aktywna do następnej wersji. Oczywiście czas uruchamiania jest dość długi (20 do 30 sekund), ale raz w miesiącu jest to całkowicie do przyjęcia.

Jednak siła skanowania biblioteki, skutkująca dodatkowym czasem uruchamiania, jest głównym problemem w środowiskach serverless lub autoskalowalnym.


Micronaut

Odkryłem Micronaut podczas prelekcji na Google Cloud Next '19 w San Francisco. Tylko go wypróbowałem i nie tworzyłem prawdziwej aplikacji za pomocą Micronaut, ale wydaje się, że jest to dobra alternatywa dla SpringBoot.

Składnia adnotacji jest podobna do Spring Boot, a także można zaimportować wrapper adnotacji Spring Boot w tym frameworku. Jest wiele funkcji podobnych jak w Spring Boot. Różnica w stosunku do SpringBoot i największa siła Micronaut polegają na tym, że skanowanie bibliotek odbywa się w czasie kompilacji. Dlatego podczas uruchamiania aplikacja wie, które pakiety załadować, bez konieczności skanowania wszystkich plików.

Dlatego czas uruchamiania jest lepszy niż w przypadku Spring Boot, ale także, jak sądzę (ale nie testowałem), czas uruchamiania powinien być dość stabilny w czasie, nawet jeśli dodasz wiele nowych bibliotek i plików.


Micronaut + GraalVM

Micronaut można używać róznież z GraalVM. Przygotowanie paczki AOT (ahead-of-time) poprawia czas uruchamiania i zmniejsza ilość potrzebnej pamięci.

Ciekawie by było wypróbować również te techniki.


Java servlet

Frameworki są przydatne i naprawdę przydatne dla programistów. Ale często są one uciążliwe i mogą zużywać dużo pamięci i procesora (wpływając na czas uruchamiania) w przypadku niektórych nieużywanych funkcji.

Dla prostego endpointa HTTP helloWorld, użyłem czystej Javy z serwerem Jetty. Tutaj nie ma adnotacji, tylko Java, dziedziczenie i nadpisywanie. Wszystko robiłem na własną rękę.


Spring Boot Webflux

Znowu Spring Boot? Tak. Jak można zobaczyć w zaobserwowanych wynikach, zwykły Spring Boot ma słabą wydajność. Byłem tym zirytowany, ponieważ naprawdę podoba mi się ten framework.

Oglądałem na Youtube sesję Next '19 z Rayem Tsangiem. Wyjaśnił, jak korzystać z SpringBoot na GCP i zaoferował, że skontaktuje się z nim na Twitterze, jeśli będziemy mieli pytania, a ja to zrobiłem. Ray jest super fajny i pomógł mi poprawić wydajność Spring Boot HelloWorld dzięki sztuczkom webflux i pom.xml. Dlatego też chciałbym się tym podzielić.


JIB Plugin

Gdy nie używam GraalVM, zawsze używam Dockefile i JIB by utworzyć kontener i porównać wydajność.

JIB to wtyczka Maven and Gradle opracowana przez ludzi z Google w celu łatwego i wydajnego pakowania kodu Javy do kontenera. Nie musisz pisać w Dockerfile, a zamiast tego możesz użyć kilku opcji konfiguracyjnych, o ile chcesz go dostosować do swoich potrzeb.

Wydajność

Procedura testowania i pomiar wydajności

Wszystkie testy są przeprowadzane na Cloud Run w regionie US-Central1. Kontener działa w trybie bezpiecznym (opcja allow-unauthenticated została wyłączona) i przy domyślnych parametrach (pamięć, brak env var, domyślne konto usługi,…)

Czas uruchamiania został przetestowany 3 razy dla każdej wersji. Wartość jest podana w logach Cloud Run z komentarzem „To żądanie spowodowało uruchomienie nowej instancji kontenera, a zatem może potrwać dłużej i zużywać więcej procesora niż typowe żądanie”. Średnia wartość z tych 3 testów to wartość zimnego rozruchu. 3 może wydawać się mało, ale w rzeczywistości wydajność Cloud Run jest dość stabilna, a zatem wystarczy by wnioskować o czasie zimnego startu.

Średni czas odpowiedzi i zużycie pamięci są próbkowane po teście obciążenia wykonanym za pomocą Hey. Aby zapobiec skalowaniu kontenerów Cloud Run, ustawiam takie parametry Hey:

  • współbieżność z 1, aby uniknąć wielu połączeń z Cloud Run w tym samym czasie
  • żądania na sekundę do 5, aby ograniczyć obciążenie i tym samym uniknąć tworzenia nowej instancji.


Test obciążenia wykonuje 2500 żądań ze środowiska Cloud Shell. Jak widać, globalne opóźnienie wynosi około 115 ms. Globalne, czyli od Cloud Shell do Cloud Run, czas przetwarzania i wszystkie procesy w drugą stronę. Z wyjątkiem uruchomienia, w logach Cloud Run opóźnienie jest stabilne, około 6 ms. Reszta czasu jest marnowana na komunikację sieciową (a ja jestem w regionie Europy).

Średni czas odpowiedzi zapewnia Hey. Wykorzystanie pamięci zapewnia metryka konsoli Cloud Run. Rozmiar kontenera zapewnia Rejestr Kontenerów Google.


Rezultaty

Otrzymane rezultaty znajdziesz tutaj.

Oto kilka konkluzji na podstawie otrzymanych rezultatów.

Po pierwsze

Kontenery używają tej samej ilości pamięci w testach obciążenia, a opóźnienie żądania jest równoważne.

Po drugie

Z wyjątkiem GraalVM, wtyczka JIB jest bardziej wydajna (lub równoważna z Servlet) niż ręcznie utworzony plik Dockerfile. Jeśli nie potrzebujesz niczego niestandardowego w Dockerfile, najpierw użyj JIB!

Być może istnieją optymalizacje w celu poprawy wydajności z Dockerfiles. -> Być może istnieją optymalizacje, które pomogą poprawić wydajność używają Dockerfile.

Po trzecie

Nie był zaskoczeniem fakt, że bez frameworków osiąga się lepszą wydajność, ale jednocześnie wymaga to bardziej złożonego kodu i więcej umiejętności. Z frameworkami wydajność Micronaut + GraalVM jest niesamowita dla czegokolwiek w Javie.

Po czwarte i ostatnie

Mój ulubiony SpringBoot jest całkiem akceptowalny po zoptymalizowaniu (czas uruchamiania 5 sekund), ale jest to tylko endpoint HelloWorld. Nie wiem, jaka będzie wydajność w prawdziwej aplikacji, z większą liczbą zależności. Nie jestem pewien, czy to rozwiązanie pasuje do wszystkich przypadków użycia i wszystkich wymagań.

Podsumowanie

Mój kod dostępny jest na Githubie. Możesz także użyć go jako bazy dla swoich eksperymentów. Możesz także użyć JIB do zbudowania kontenera i wypchnięcia(?) go domyślnie do Rejestru Kontenerów Google.

Wszystkie instrukcje kompilacji i deploymentu znajdują się w pliku Readme.md.


Oryginał tekstu w języku angielskim przeczytasz tutaj.

<p>Loading...</p>