Quarkus – luźne przemyślenia po 500h developmentu (cz. 2 – GraalVM – Javascript i Native Image) 0 (0)

W poprzednim wpisie podzieliłem się ogólnymi uwagami dot Quarkusa. W tym chciałbym opowiedzieć o technikaliach współpracy Quarkusa z GraalVMem i Javascriptem.

Javascript

Teoretycznie w dokumentacji nie znajdziemy informacji, że Quarkus wspiera Javascript. GraalVM wspiera JSa, to tak, ale Quarkus o tym nie wspomina.

Jeszcze w zeszłym roku instalując GraalVMa otrzymaliśmy wsparcie dla Javascriptu out-of-the-box. Aktualnie, aby skorzystać z dobrodziejstwa JSa należy go doinstalować – podobnie jak wcześniej było ze wsparciem dla Pythona, R czy Ruby’ego. Wydzielenie Javascriptu z bazowej paczki (stało się to w wersji 22.2) było spowodowane chęcią jej odchudzenia.

To odłączenie powoduje dwa problemy – jeden z Dockerfilem dla wersji wykorzystującej JVMa, drugi – w wersji skompilowanej do kodu natywnego.

Aby to wyjaśnić najpierw muszę cofnąć się i dopowiedzieć nieco o budowaniu projektów z Quarkusem.

Proces budowania obrazu z JVM

Budowanie projektu do postaci JVMowej to nic innego jak zbudowanie go Mavenem/Gradlem. Jeśli chcemy skorzystać z Dockera, to musimy uruchomić budowanie obrazu z Dockerfile’a.

Jednak bazowy obraz, z którego korzystamy dockeryzujac nasza apke, to nie standardowy GraalVM CE dostarczona przez Oracle’a, a maksymalnie odchudzona wersja OpenJDK -dostarczane przez RedHata. Zatem jeśli chcemy korzystać z GraalVMa- musimy zmienić obraz bazowy na GraalVMowy (a istnieją obrazy dostarczane przez Oracle’a). Musimy się jednak pogodzić, że nasz obraz będzie cięższy i może nieco wolniejszy.

Jeśli chcemy skorzystać z Javascriptu, to musimy go doinstalować do naszego obrazu bazowego, gdyż preinstalowanych obrazów z Jsem nie ma. Wystarczy jednak dodać kilka linijek w Dockerfile’u, zatem bez tragedii…

Proces budowania obrazu z programem natywnym

Budowanie projektu do postaci natywnej jest już bardziej skompikowane. Zamiast standardowego kompilatora potrzebujemy GraalVM z dodatkiem „Native Image”. Instalacja dodatku wygląda analogicznie do dodania wsparcia dla Javascriptu.

Budowanie kodu natywnego ma pewien mankament – jest on natywny, czyli specyficzny dla danej platformy. Innymi słowy – na Windowsie nie zbuduje się źródeł dla Linuxa. Problem ten da się rozwiązać poprzez wymuszenie kompilacji do kodu natywnego z użyciem obrazu dockerowego, co też Quarkus udostępnia. Standardowo wykorzystywany do tego celu jest obraz z Mandrillem – forkiem GraalVMa stworzonym na potrzeby Quarkusa.

Jak można się spodziewać Mandrill nie wspiera Javascriptu. Można jednak podmienić obraz budujący wykorzystywany na GraalVMowy, uprzednio wzbogacając go o wsparcie dla JSa. Jednak tak zmodyfikowany obraz trzeba trzymać w jakimś docker registry.

Podsumowując – wszystko się da, jednak musimy się pożegnać z różnego rodzaju optymalizacjami, ułatwieniami i poświęcić nieco czasu w znajdywanie różnych rozwiązań niestandardowych problemów. Wymaga to też pewnej kreatywności – jak robić, by zrobić, ale się nie narobić 😉

Użycie niewspieranej biblioteki w trybie Native

Jak już wspominałem w poprzednim wpisie, jest bardzo dużo bibliotek posiadających własne Pluginy do Quarkusa. Wówczas użycie takiej biblioteki nie sprawia żadnych problemów.

Użycie niewspieranej biblioteki w trybie JVMowym również nie jest zbytnim wyzwaniem. Prawdopodobnie wystarczy zarejestrować odpowiednie klasy jako beany z użyciem CDI ewentualnie inicjalizując.

Problemy zaczynają się, gdy taka niewspieraną bibliotekę próbujemy użyć w trybie natywnym. Problemów dostarcza zarówno Quarkus, jaki i sam GraalVM.

GraalVM umożliwia inicjalizację pól statycznych oraz uruchomienie bloków statycznych na etapie budowania zamiast w runtime (więcej o tym tu). Pozwala to ograniczyć czas inicjalizacji aplikacji. Quarkus w ramach optymalizacji deklaruje, że wszystkie możliwe pola statyczne finalne mają być zainicjalizowane. Jednak nie wszystkie pola możemy tak zainicjalizować. Chociażby generator licz losowych nie powinien być zainicjalizowany na etapie budowania, gdyż spowodowałby to generowanie tych samych liczb w tej samej kolejności przy kolejnych uruchomieniach aplikacji. Jeśli zatem chcemy zainicjalizować klasę w runtime’ie, musimy to jawnie zadeklarować, co wymaga dodatkowej pracy.

Za to powszechnym wymaganiem wynikacjących z użycia GraalVM Native Image jest wymienienie wszystkich klas, które muszą posiadać wsparcie dla refleksji, proxowania, serializacji, JNI lub które są resource’ami i które trzeba zawrzeć w aplikacji natywnej. Można próbować samemu stworzyć wymagane pliki json, jednak jest to karkołomne zadanie.

Twórcy GraalVMa stworzyli agenta, który sam spisuje za nas dla jakich klas używamy refleksji, jakie proxy tworzymy itd. Należy jednak zadbać o to, aby po uruchomieniu „wywołać” te miejsca (użycia refleksji itd.), aby agent mógł je zarejestrować. Innymi słowy trzeba przetestować całą aplikacje wzdłuż i wszerz… Takie testy regresyjne należałoby przeprowadzać przynajmniej przy dodawaniu nowej biblioteki lub zmiany konfiguracji. Zatem to rozwiązanie jest mocno uwierające.

Oprócz tego dość niedawno pojawiło się ogólnodostępne repozytorium z metadanymi dla najbardziej popularnych bibliotek. Nie miałem jednak okazji skorzystać z tego dobrodziejstwa, jednak wydaje mi się, że nie rozwiąże to problemów dla niszowych bibliotek…

Czas to pieniądz…

W kontekście kompilacji do kodu natywnego to powiedzenie można rozumieć wielorako.

Kod natywny praktycznie się nie musi rozgrzewać, inicjalizacja aplikacji to dosłownie milisekundy. W zasadzie jest od razu gotowy do użycia. Jeśli przewaga naszego produktu polega na szybszym uruchamianiu, skalowaniu, to kompilacja do kodu natywnego ma sens. Zatem widziałbym sens takiego rozwiązania w serverless, gdzie czas inicjalizacji ma znaczenie, lub narzędziach uruchamianych na desktopach (mvnd, czyli maven na sterydach – polecam). Sensowne byłoby też wykorzystanie programu natywnego na sprzęcie o małych dostępnych zasobach – aplikacja natywna minimalizuje zużycie pamięci oraz wielkość aplikacji. Można by również rozważyć w krótkich Jobach uruchamianych raz dziennie, aby minimalizować ilość używanej pamięci, lub aplikacjach o małym natężeniu ruchu, które JVM nie zdąży rozgrzać w czasie między kolejnymi deployami.

W przypadku długożyjących aplikacji wybór Native Image jest nieuzasadniony, (o ile nie błedny). Kompilacja AOT co prawda działa szybciej na początku, to ostatecznie jest wolniejsza, gdyż nie ma dostępu do informacji o działaniu aplikacji w runtime (chociaż w GraalVM Enterprise Edition wydaje się, że można dostarczyć jakieś dane z działania aplikacji, jednak wciąż nie może dorównać to JITowi).

Należy jednak brać również pod uwagę dodatkowy czas kompilacji do kodu natywnego – zawsze jest to przynajmniej kilka minut. Można optymistycznie założyć, że kompilacja będzie wykonywana w ramach CI/CD (czyli nie przez dewelopera), ale jeśli coś się wysypie, to i tak deweloper musi to poprawić. Dodatkowo jeszcze wspomniany czas na testy regresyjne w celu poinformowania agenta GraalVMa o klasach rejestrowanych do użycia refleksji.

Liczby

Z własnego doświadczenia mogę podzielić się pewnymi danymi. Nie są to jednak dokładne benchmarki wykonywane na czystym środowisku. Jednak dają pogląd na rząd wielkości i przyspieszenie/spowolnienie względem trybu native/jvm. Ich udostępnienie ma dać jedynie ogólny pogląd, a wszelka zbieżność imion i nazwisk – przypadkowa 😉

Jeszcze jeden szczegół, który może mieć wpływ na wyniki – operacje te wykorzystują Javascript. Zarówno w trybie natywnym, jak i JVMowym GraalVMowy Javascript posiada swój JIT, gdy kod Javy podlega JITowi wyłącznie w trybie JVM. Może to spowodować, że różnice czasów mogą być mniejsze (amplituda), aniżeli gdybyśmy wykorzystywali tylko kod napisany w Javie.

Operacja nr 11234
JVM2m 08s1m 20s56s51s
native1m 40s1m 32s1m 22s1m 11s
Czas wykonania operacji nr 1 w kolejnych ponowionych żądaniach
Operacja nr 21234(…)min
JVM15s3.5s2.3s2s0,7s
native7s3s2s1.6s1,1s
Czas wykonania operacji nr 2 w kolejnych ponowionych żądaniach.
Min oznacza minimalny otrzymany czas wykonania operacji.

Widać zatem pewną logiczną zależność – pierwsze operacje są wykonywane szybciej w trybie natywnym, ale już od 2 lub 3 czasy są porównywalne, a na dłuższą metę tryb JVM jest kilkadziesiąt procent szybszy.

Mogę się jeszcze podzielić jeszcze jednym wynikiem. Otóż operacja nr 1 była wykonywana z różnymi danymi kilka tysięcy razy. W trybie JVM całościowy czas to było 16 minut, gdy w trybie natywnym – 43 minuty.

Podsumowanie

GraalVM Native Image jest bezsprzecznie ciekawą technologią, która w pewnych przypadkach może mieć pozytywny wpływ na produkt – chodzi zarówno o czas inicjalizacji, jak ilość pamięci potrzebnej do działania aplikacji. Trzeba być jednak świadomym dodatkowych kosztów – rosnącego skomplikowania technicznego projektu oraz spowolnienia dewelopowania spowodowanego dłuższym czasem kompilacji. Warto zatem rozważyć, czy tryb natywny jest koniecznie niezbędny w naszym przypadku biznesowym.

Pax et bonum.

Quarkus – luźne przemyślenia po 500h developmentu (cz. 1) 0 (0)

Łatwo znaleźć informacje o pierwszych doświadczeniach z daną technologią. Jeśli chodzi o doświadczenia po spędzeniu z nią większej ilości czasu, to jest tychże znacznie mniej…

W tym wpisie przedstawię subiektywne odczucia odnośnie frameworku Quarkus.

Kontekst projektu

Projekt polega na ogólnie mówiąc generowaniu wydruków (czyt. więcej przetwarzania niż oczekiwania na I/O). Mimo to interakcja z innymi serwisami jest również konieczna. Dodatkowym wymaganiem jest wsparcie dla Javascriptu na backendzie.

Dlaczego Quarkus?

Kluczowe byłyby dwa wymagania: wsparcie dla Javascriptu oraz szybkość generowania wydruku.

Pierwszy wymóg mógł zostać łatwo spełniony poprzez wybór GraalVM jako JVM – GraalVM zapewnia wsparcie dla różnych języków w ramach całej platformy. Co więcej – nie jest to symulowanie działania danego języka wewnatrz JVMa poprzez interpretowanie kolejnych instrukcji, ale to JVM (właściwie to GraalVM) rozumie dany język i wykonuje instrukcje na dość niskim poziomie. Stąd wydajność rozwiązania jest znacznie lepsza niż w wersji interpretowanej.

Alternatywnie wsparcie dla Javascriptu można było zapewnić poprzez dodanie zależności mavenowej/gradle’owej Graal.js. Ta wersja pozwala na wykorzystanie zwykłej JVMki zamiast GraalVMa, jednak kosztem wydajności – uruchomienie skryptów Javy (hi hi) odbywa się poprzez interpretowanie kodu.

Drugie wymaganie dotyczyło prędkości generowania wydruku. Należałoby tu rozważyć dwie sytuacje – na krótszą i dłuższą metę. Czas pierwszych wydruków możnaby zminimalizować korzystając z GraalVMa Native Image. Za to na dłuższą metę można skorzystać ze standardowej GraalVM bez kompilacji do kodu natywnego, gdyż JIT zapewnia lepszą wydajność niż kompilacja Ahead-of-time (o więcej o tym później).

Na etapie wyboru technologii Spring Native nie był jeszcze wydany, zatem najbardziej powszechny framework musiał zostać odrzucony. Był oczywiście jeszcze Micronaut, jednak wybór padł na Quarkusa. Jeśli szukacie porównania tych technologii, polecam artykuł o wyborze frameworku dla Stargate v2.

Jak poukładany jest Quarkus

Quarkus w swych marketingowych materiałach używa sentencji

Developer Joy: Unified configuration, Zero config, live reload in the blink of an eye, Streamlined code for the 80% common usages, flexible for the 20%, No hassle native executable generation, live coding.

Jest w tym dużo racji. Jest bardzo wiele tutoriali, które ogrywają standardowe sytuacje i działają jak należy. Live reloading odświeża kontekst aplikacji i przeładowuje źródła (choć dopiero przy wysłaniu requesta, a nie na zapis pliku, co może być nieintuicyjne). Generator Quarkusa oprócz hierarchii katalogów i wydmuszki aplikacji, tworzy również gotowe profile developerskie, produkcyjne, kompilacje do Native Image, a także obrazy dockerowe – właściwie to Dockerfile’e (z apka w wersjach JVM oraz natywnej).

Sam Quarkus jest skonstruowany tak, by możliwie najwięcej działań potrzebnych mu do uruchomienia było wykonanych przed uruchomieniem aplikacji. Chociażby określenie dostępnych beanów odbywa się poprzez indeksowanie ich w czasie kompilacji, a nie poprzez skanowanie classpatha w czasie inicjalizacji aplikacji, jak to ma miejsce w Spring Boot. Generowanie Proxy do obsługi przykładowo @Transactional równiez generowane są na etapie kompilacji źródeł. Wszystkie beany są domyślnie inicjowane w trybie Lazy – dopóki czegoś nie potrzebujemy, to nie jest to tworzone. Dodatkowo same biblioteki frameworku i sam Quarkus jest ładowany przez osobny classloader niż klasy naszej aplikacji. Dzięki temu przy przeładowaniu kodu wszystkie biblioteki nie muszą być ponownie ładowane.

Oszczędzamy dzięki temu wszystkiemu czas na przeładowaniu kontekstu i w efekcie apka wstaje szybciej.

Tryb deweloperski, a właściwie jego konsola jest dość przydatna. Oprócz wymuszenia przeładowania i puszczenia wszystkich testów w projekcie jest tam bardzo przydatna funkcja zmiany poziomu logowania z użyciem jednego klawisza w czasie działania serwera. Opcja powszechna w różnego rodzaju JMXach, Actuatorach, havt.io i innych, ale jednoklawiszowej opcji dotychczas nie spotkałem 😉

Tryb deweloperski ma jeszcze jeden smaczek: jeśli port jest zajęty, to Quarkus podpowie komendę netstat która sprawdzi PID procesu zajmującego ten port – mała rzecz, a cieszy 🙂

Jakkolwiek muszę też wspomnieć o tych 20%, gdzie trzeba wyjść przed szereg i skonfigurować coś niestandardowego. Jeśli mamy jakąś starą zależność (ot, jakieś JMSy sprzed 10 lat), z której musimy korzystać, bo cała reszta naszej platformy z niej korzysta, to jest to uciążliwa sprawa. Wszytko jest „zrabialne”, ale aby wykonać to porządnie, trzeba się mocno napracować. Wsparcie dla trybu deweloperskiego, natywnego, współpraca miedzy wątkami, przekazywanie kontekstu – wszystko zrobić się da, acz niezerowym nakładem.

Największym dramatem jest to, że większość tych customowych problemów da się rozwiązać, jednak dopiero po trafieniu na odpowiednie miejsce w internecie, które co prawda istnieje, jednak nawet google nie wie jak tam trafić… Najbardziej jednak boli to, że to przeważnie kilka linijek kodu…

Pluginy

Jak już wspomniałem, Quarkus posiada bardzo liczne integracje z popularnymi bibliotekami. Dodawanie integracji do projektu odbywa się poprzez dodanie odpowiedniego „pluginu” (plugin to nic innego jak zwykła zależność mavenowa/gradle’owa). Każdy plugin jest automatycznie przystosowany zarówno do trybu deweloperskiego, jak i produkcyjnego (jej i native). Dokumentacja ogólna, jak korzystać z pluginu, jak i szczególna szczegółowe propertiesy konfigurujące plugin są dostępne na tej stronie (jest też opcja wyszukiwania).

Podstawowe technologie

Quarkus integruje ze sobą przeróżne wypróbowane technologie i na nich się koncentruje. Dla obsługi wystawiania serwisów – adnotacje JAX-RS z serializacja Jacksonem. Wstrzykiwanie zależności – CDI. Konfiguracja projektu oparta jest na Smallrye Config. Logowanie – JBoss Logging (acz dostępne adaptery dla Log4J i SLF4J). Domyślny ORM to Hibernate, z opcją Hibernate Reactive. Jak już mowa o reaktywności to tutaj wykorzystywana jest biblioteka Mutiny, co więcej wszystkie reaktywne pluginy wystawiają interfejs z użyciem Mutiny. Całość postawiona jest na Vert.x.

Używanie bibliotek uznawanych za standard ma swoje zalety, lecz również wady. Przede wszystkim używanie bibliotek porządnie wygrzanych na produkcji jest potencjalnie bardziej odporne na błędy, niż pisanie swoich bibliotek. Odchodzi też kwestia utrzymania, dokumentacji i rozwoju – zajmuja się tym inne organizacje, a Quarkus jedynie integruje wszystko razem.

Problem z tym podejściem jest taki, że nie wszystko znajdziemy na stronie Quarkusa. Trzeba czasem szukać podpowiedzi na stronie danej biblioteki, a nawet specyfikacji standardu, który biblioteka implementuje (przykładowo CDI). Zdarza się czasem taka szara strefa jeśli chodzi o odpowiedzialności, kto za co odpowiada. Przykładowo jeśli dodamy OpenTelemetry z traceId, które logujemy, to w przypadku użycia vert.x’owych dodatkowych feature’ów, ten traceId się gubi na kilka wpisów w logach. A to właśnie za sprawą tego, że vert.x’owe dodatki działają na osobnej Poli wątków, których uogólniając OpenTelemetry nie jest świadome.

Ciąg dalszy nastąpi…

Tyle by było ogólnych informacjo-opinii na temat Quarkusa. W kolejnym wpisie/kolejnych wpisach będzie więcej szczegółów technicznych, a i ostatecznego podsumowania się spodziewajcie 😉

Pax et bonum

Spring Boot Native, GraalVM i JHipster 0 (0)

Spring Boot na GraalVMie? Brzmi ciekawie, ale czy to jest możliwe?

Według wpisu sprzed 2 miesięcy prosty projekt można postawić na GraalVM Native Image. Jednak prosty CRUD, to nie system produkcyjny, który wymaga różnych bibliotek. Pytanie, jak to wygląda z nieco bardziej skomplikowanym stackiem technologicznym?

JHipster

Nie będę oczywiście pisał wszystkiego od zera. W zeszłym roku napisałem szybką aplikację (i wpis o niej). W niej wykorzystałem generator kodu JHipster. Taki generator to jest dobry dla szybkich, krótkich projektów, gdyż oprócz wygenerowanego modelu dostarcza wielu standardowych wymagań takich jak uwierzytelnienie, autoryzacja, metryki, obsługa logowania, a nawet stworzenie obrazu Dockerowego. Warto spojrzeć na ten projekt oraz co nim się da wygenerować również po to, żeby poznać technologie, zaczerpnąć ewentualnie patternów zaimplementowanych w takim projekcie.

Posiadając już apkę z Spring Bootem w wersji 2.2.7 postanowiłem zastosować w nim Spring Boot Native. Pierwszym wymogiem jest podbicie wersji Spring Boota do 2.4.5. Oczywiście podbicie wersji Spring Boota pociąga za sobą podbicie innych wersji bibliotek używanych przez Springa. Wchodząc do tego piekła ciężko z niego wyjść. Zatem od razu sobie odpuściłem manualne podbijanie, gdy w kolejnej wersji JHipstera (w wersji 7.0) podbicie Spring Boota do wersji 2.4.x jest zrobione out-of-the-box.

Zatem przeszedłem do ponownego wygenerowania na podstawie metadanych JHipstera. Rzecz jasna po tym należało zmerge’ować wygenerowany kod z moimi zmianami. Rzecz jasna to też nie było przyjemnie, ale wiązało się z mniejszym ryzykiem, niż manualne podbicie wersji.

Spring Boot + Spring Boot Native

Po podbiciu Spring Boota można było wprowadzić zmiany wymagane przez Spring Boot Native. Po uaktualnieniu pom.xml można było spróbować pierwszy raz uruchomić proces budowania. Pierwszy raz budowanie zakończyło się porażką na etapie budowania Mavena z komunikatem podobnym do Nie udało się, bo się nie wysypało. Pierwszym rozwiązaniem okazało się dodanie do parametrów kompliacji Mavena paramteru <forceJavacCompilerUse>true</forceJavacCompilerUse> . Po drugiej próbie kompilacji komunikat już był precyzyjny. Tym razem problem był po mojej stronie – źle zmerge’owałem źródła.

Trzecia kompilacja zakończyła się wielkim sukcesem – po 15 minutach mielenia na moim starym lapku projekt skompilował się! Uruchomienie przyniosło jednak szybkie rozczarowanie – logback nie może dostać się do pliku z konfiguracją. Po kilku próbach (czyt. kilkukrotnym czekaniu po 15 minut) i przeczesaniu internetu na temat tego błędu, zmigrowałem konfigurację na taką, która bazuje na przekazanych propertiesach.

Czwarta (15 minut) kompilacja zakończyła się porażką – uruchomienie aplikacji natywnej zakończyła się problemem z Audytowaniem uwierzytelniania. Nie jest to kluczowa funkcjonalność tego projektu, zatem szybkie zakomentowanie @EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware") rozwiązało problem.

Piąta (15 minut) kompilacja zakończyła się kolejną porażkę. Tym razem okazało się, że Swagger to zbyt duże wyzwanie dla Spring Native. Rozwiązanie – zakomentowanie uruchamiania profilu słagerowego.

Szósta (15 minut) kompilacja zakończyła się znów porażką. Tym razem kontekst Springa nie wstał. Po prostu. Bez informacji, co nie działa, gdzie i o co chodzi. Tutaj się poddałem.

Teoretycznie jest jakiś ticket na dostosowanie JHipstera do Spring Native, a nawet można dostać 500$ za dostarczenie tego feature’u. Można podejrzewać, że dostosowanie do Spring Native ostatecznie zostanie wykonane. Ale to póki co, to pieśń przyszłości.

Inne rozwiązania

JHipster jest już dość dojrzałym projektem – na frontendzie oferuje nie tylko Angulara, ale również Reacta i Vue.js, za to na backednzie wsparcie obejmuje Spring Boota, a także Micronauta i Quarkusa. Co ciekawe język programowania wymienić na Kotlina (ze Spring Bootem), C# (.Net), czy Javascript (NestJS).

W przypadku Micronauta niestety wsparcie nie obejmuje kompliacji do Native Image. Szkoda, bo Micronaut ma API bardzo podobne do Spring Boota.

Został JHipster z Quarkusem. Tutorial tłumaczący, jak stworzyć wygenerować taką aplikację jest dosadnie prosty. Wsparcie obejmuje również Native Image. Zatem do dzieła.

JHipster + Quarkus

Ponownie musiałem przegenerować na podstawie metadanych JHipstera. Niestety API Quarkusa różni się zdecydowanie od Spring Boota.

Począwszy od bazy danych i JPA różni się podejście do „owrapowania JPA„. W Spring Boot mamy bardzo przyjazne Spring Data Quarkus oferuje nam za to Panache, który jest frameworkiem dedykowanym dla Quarkusem. W nim dominującym podejściem do pobierania i zapisywania krotek jest ActiveRecord, choć da się również stworzyć klasy Repository. Jest wsparcie dla paginacji, customowych projekcji, zapytań w JPQL. Jedyne, czego brakuje, to odpowiednik uproszczenia JPA Criteria Query.

Jak mówiłem, podejściem dominującym jest Active Record, to w modelowaniu klas – zamiast klas POJO z getterami i setterami – dominuje klasa z publicznymi polami bez getterów i setterów dziedzicząca po PanacheEntity. Tutaj również musiałem dostosować swoje customowe zmiany.

Wsparcie dla cache’ów jest, ale w moim przypadku nie było potrzebne (i nie działało ;)), zatem je wyłączyłem.

Wsparcie dla security contextu jest, jednak dostęp nie jest przez ThreadLocala zwanego SecurityContextHolderem, a przez dodatkowy parametr wywołania endpointa.

Dodatkowo Quarkus wymusza poznawanie innych frameworków (czyt. RedHatowych, Eclipse’owych). I tak zamiast Jacksona mamy JSONB, zamiast Springa MVC mamy Jax-Rs. Wstrzykiwanie zależności wykonywane jest przez @Inject, choć Spring również wspiera tę adnotację.

Ostatnie, o czym trzeba pamiętać, to żeby do DTOsów i innych obiektów obrabianych refleksją (encje bazodanowe, projekcje) dodać adnotację @RegisterForReflection.

Wynik

Quarkus jest stworzony dla GraalVM Native Image, więc w tym przypadku nie było żadnego problemu z kompilacją do kodu natywnego. Sam czas kompilacji na mojej maszynie, to około 10 minut, czyli o 1/3 szybciej, niż kompilacja Spring Boot Native.

Normalnie na OpenJDK aplikacja wstaje lokalnie w około 5 sekund. Po skompilowaniu do kodu natywnego, uruchomienie aplikacji trwa do 100 milisekund.

Drugą kwestią jest, że taką aplikację można postawić niewielkim Heapie – dałem -Dxmx=64M, co łącznie z OffHeapem łącznie daje 200 MB (tyle przynajmniej pokazuje htop). Obstawiam, że gdyby zrezygnować z wielu JHipsterowych feature’ów, to dało by się zejść z zużyciem pamięci jeszcze niżej.
Spring Boota to z mniejszym Heapem niż 128 to bym nawet nie próbował startować, a jak wiadomo OffHeap też swoje waży. Jakkolwiek istnieją ludzie, którzy ze Spring Bootem potrafią zejść poniżej 80MB mierząc wszystko…

Podsumowanie

Należy pamiętać, że Spring Native jest jeszcze w fazie beta, zatem nie wszystko może tam działać. Możliwe, że tworzenie serwera od początku do końca było by lepszą strategią pozwalającą na znalezienie wszystkich przyczyn błędów.

Jednak każdorazowa rekompilacja trwająca 15 minut skutecznie odrzuca. Jako programista Java jestem przyzwyczajony do kompilacji klasy do bytecodu mierzonej w maksymalnie sekundach. Nawet kompilacja z użyciem Mavena takiego projektu trwa maksymalnie minutę.

Trzeba poczekać na pierwszy release, wtedy będzie można cokolwiek powiedzieć. Póki co jest zbyt wcześnie…

Quarkus – co ciekawego siedzi w środku. 0 (0)

Nie wiem, czy śledzicie wpisy na JvmBloggers, jednak w poprzednim tygodniu były 4 wpisy CRUDowe o Quarkusie. Jeżeli wszyscy o tym piszą, to przecież nie będę gorszy 😉

Jeśli chcecie się dowiedzieć, jak napisać CRUDa to tutaj po polsku, a tu po angielsku. Ja osobiście zajmę się bezużyteczną wiedzą – dlaczego to jest fajne i jaki to ma potencjał. Ale od początku.

GraalVM

GraalVM jest to Oracle’owa maszyna wirtualna bazowana na OpenJDK. Jej celem było stworzenie uniwersalnej maszyny wirtualnej, która obsługuje wiele języków programowania – zarówno JVMowe (Java, Scala, Groovy), jak również skryptowe (Js, Ruby, Python, R) i natywne (C++). Pozwala również na bardzo wydajne wywoływania funkcji zdefiniowanych w różnych językach (takie cross-językowe wywołania).

Jednak stworzenie takiej VMki to była długa droga… Najwięcej działo się przy releasie JDK 9. Wtedy położono fundamenty pod GraalVM – wydzielono w źródłach JDK interfejs kompilatora, poukładano projekt OpenJDK modularnie oraz wprowadzono kompilator Ahead-of-time. Skoro już był wydzielony interfejs kompilatora, to można było wprowadzić nowy kompilator – modularny, piękny i pachnący, o wdzięcznej nazwie Graal. W dodatku cały napisany w Javie (bo kto umie w C++, gdzie nawet GCka nie ma). Uczyniono to w JDK 10 w JEP 317. Tenże kompilator Graal jest w niektórych względach lepszy od C2 – z pewnością lepiej radzi sobie ze różnego rodzaju streamami. Swego czasu popularne było stwierdzenie, że Twitter przeszedł z C2 na Graala i zjadało mu 5% procesora mniej. Jakkolwiek mowa była o Scali, nie o Javie.

W końcu GraalVM wyszedł w maju zeszłego roku. W założeniu miał pozwalać na łatwe wywoływanie międzyjęzykowych funkcji. Standardowym use case’m miało być wydajne wywoływanie funkcji Machne Learningowych napisanych w R lub Pythonie (jak wiadomo Java w tej dziedzinie nie jest zbyt popularna). Innym przykładem może być jednokrotne tworzenie walidacji po stronie frontendu oraz wywoływanie tej walidacji także na backendzie (pozdro Apzumi! 😉 ). Jednak osobiście moim faworytem jest narzędzie do generowania kodu natywnego.

GraalVM Native Image

Narzędzie to pozwala na stworzenie łatwo uruchamianej aplikacji napisanej w Javie skompilowanej do wersji natywnej dla systemu operacyjnego. W pierwszej wersji (19) wspierano tylko Linuxa i MacOSa, w wersji 20 wpierany jest również Windows. GraalVM jest dostępny z JDK w wersji 8 jak i 11. Kompilacja do kodu natywnego jest wykonywana z użyciem wspomnianego wcześniej kompilatora Graal.

W czasie kompilacji do kodu natywnego ma miejsce stworzenie grafu zależności między klasami, polami, metodami, dzięki którego analizie wszystkie nieużywane pola, klasy, metody są usuwane. Wycięcie 90% platformy powoduje, że aplikacja wstaje w milisekundy zamiast sekund. Dodatkowo zużywa znacznie mniej pamięci (mierzone w dziesiątkach megabajtów zamiast setek). Choć na kompilację trzeba poświęcić kilka minut…

Co więcej – GraalVM Native Image nie wspiera wszystkich opcji dostępnych dla standardowego JDK. Brakuje między innymi MethodHandles, InvokeDynamic, serializacji, security managera. W przypadku użyciu refleksji, DynamicProxy, JNI lub dynamicznego ładowania klas wymagana jest rejestracja używanych klas w osobnym pliku konfiguracyjnym. Więcej o ograniczeniach można poczytać na tej podstronie.

Jest kilka rzeczy, które są robione inaczej. Najlepszym przykładem jest wykonywanie inicjalizacji klas na etapie kompilacji źródeł. Dzięki temu GraalVM uruchamia się szybciej. Czasem może jednak to prowadzić do dziwnych błędów. Inną różnicą jest to, że przekazywane propertiesty przy kompilacji są dostępne dla klas inicjalizowanych na etapie kompilacji, a na etapie runtime już nie.

GraalVM umożliwia również wydawanie natywnych bibliotek do systemów operacyjnych, co wcześniej w Javie nie było możliwe.

Jak to w środku działa?

W miarę normalnie – mamy normalny Heap, który co jakiś czas jest odśmiecany. Z tego gdzieś mi się obiło o oczy, a czego znaleźć aktualnie nie potrafię są dostępne 2 GCki. Jeden jest pewną odmianą Parallel GC, drugi jest z rodziny Lowlatency.
Przy uruchamianiu normalnie definiujemy parametry -Xmx oraz -Xms (gdyż normalnie bierze 1/4 całej pamięci 😉 ). Debugować się nie da, chyba, że z użyciem debuggerów natywnych takich jak GDB. VisualVM bodajże jest, choć dla Native Image jest ograniczony. Heap Dump można wykonać, lecz tylko w wersji Enterprise Edition, gdyż z czegoś Oracle musi żyć.

Ciekawe jest to, że na użytek inicjalizacji klas w czasie budowania i obiektów w nim stworzonych jest osobny tzw. Image Heap.

A co na tym można uruchomić?

Z pewnością nie Springa. Bazuje on bowiem z założenia na refleksji, generowaniu bytecode’u, proxy i innych. Nawet gdyby się dało uruchomić, byłoby to wciąż wolne.

Dlatego specjaliści od Red Hata postanowili stworzyć Quarkusa. Sam Quarkus jest nakładką na Vert.x zapewniającą odpowiednie działanie dla GraalVM Native Image. Dodatkowo istnieje wiele „wtyczek” do Quarkusa, w których głównym założeniem jest wykorzystanie pełni potencjału GraalVMa.

Przykładowo mechanizm Dependency Injection ogarnięte jest w czasie kompilacji zamiast w runtime. Inicjalizacja „kontrolerów” webowych również w czasie inicjalizacji.

Nawet Hibernate jest porządnie zoptymalizowany dzięki wtyczce do Quarkusa. A gdy zmigrują Hibernate na Jandexa, wówczas i część inicjalizacji metadanych możnaby przenieść na czas kompilacji.

Podobne pole do optymalizacji widzę również w Jacksonie. Już teraz można cache’ować metadane obiektów w zewnętrznych implementacjach. Nie sprawdzałem jednak, czy w Quarkusie tak się aktualnie dzieje.

Nawet jeśli jeszcze istniejące rozwiązania nie są optymalne, to:1. Projekt ma mniej niż rok, więc spokojnie 😉

2. Już teraz aplikacje uruchamiają się w czasie mierzonym w milisekundach, więc raczej będzie tylko lepiej.