W tym wpisie będą subiektywne odczucia odnośnie komfortu pracy z kodem. Dokładniej – skupię się na wsparciu dla reaktywności oraz porównaniu ze Spring Bootem.
Reaktywność
O ile główna część projektu nad którym pracuję jest blokującą i niereaktywna, o tyle poeksperymentowałem ze wsparciem Quarkusa dla operacji nieblokujących w tej części kodu, która ostatecznie i tak zostanie usunięta.
Quarkus z założenia pod spodem jest napisany reaktywnie, jednak pozwala zarówno na programowanie reaktywne, jak i imperatywne. W przypadku programowania reaktywnego Quarkus korzysta z biblioteki Mutiny, który jest odpowiednikiem Reactora lub RxJava. Interfejs tej biblioteki jest wspierany zasadniczo w znacznej części pluginów – jeśli jakaś biblioteka nie wspiera sama z siebie Mutiny (choć przykładowo Hibernate Reactive wspiera), to pluginy zapewniają adaptacje do Mutiny tworząc spójny jednolity reaktywny interfejs. Jeśli jakaś biblioteka nie wspiera Mutiny, to istnieje jeszcze możliwość samodzielnego implementowania adaptera do Mutiny, z czego jednak nie korzystałem.
Nieco teorii
Zasadniczo w aplikacjach nieblokujacych stosowana jest koncepcja Event Loop. Wiele mądrych wpisów napisano na ten temat, acz tu jest po polsku 😉 choć wpis jest już nieco stary, bo trzyletni.
W standardowym blokującym podejściu każdy request gdy trafia do serwera, dostaje jeden wątek na przetworzenie i odesłanie odpowiedzi. Takie podejście nazywane jest „thread-per-request”. Jeśli musimy pobrać jakieś dane (wczytać z pliku, wykonać zapytanie do bazy danych, odpytać inny serwis) to musimy wątek zablokować do czasu uzyskania tych danych. A są to cenne milisekundy, ich dziesiątki lub nawet setki. Jeśli takich operacji wejścia/wyjścia mamy w każdym żądaniu dużo, lub oczekiwanie na dane trwa długo, to takie blokujące wątki podejście może być mało wydajne.
Pewnym rozwiązaniem jest podejście nieblokujące. Otrzymując żądanie dostajemy wątek z puli, jednak ten wątek nie jest przypisany do niego do końca przetwarzania. Gdy tylko trafi na operację wejścia/wyjścia przetwarzanie zadania jest wstrzymywane do czasu uzyskania odpowiedzi. Wątek jednak nie jest blokowany, lecz zajmuje się innymi zadaniami (nowe żądania, kontynuacja przetwarzania innych wątków, dla których operacje I/O się zakończyły i dane są dostępne itd.).
W takim podejściu stosuje się znacznie mniejszą pulę wątków (roboczo nazwijmy te pule wątków Event Loop Thread Pool). Takie operacje powinny trwać krótko (absolutny max, to kilka sekund). Jednak jeśli mamy do wykonania długotrwałe operacje, to nie powinniśmy wykonać jej na puli Event Loopy – potrzeba do tego osobnej puli wątków. Do takich operacji jest przeznaczona pula wątków Worker Thread Pool.
Oczywiście nic nie stoi na przeszkodzie, byśmy tych pul wątków mieli więcej – w zależności od potrzeb.Choć prócz tego operacje Vert.x’owe mają własne pule wątków Event Loopy i workerowe…
Ale w czym problem?
Quarkus (a w zasadzie to Vert.x) bardzo dba, by pula wątków Event Loopy nie zajmowała się długo trwającymi zadaniami. Jeśli jakaś czynność trwa zbyt długo (kilka sekund), wyświetla w logach warning z jej aktualnym stacktracem. Jest to fajny feature dev-friendly zapobiegający zagłodzeniu Event Loopy.
Jednak samo dbanie, jakie operacje powinny się wykonywać na jakiej Thread Pooli jest dodatkowym kosztem. Do tego dochodzi dodatkowa nauka obsługi kodu reaktywnego. Propagowanie kontekstów (CDI, reaktywne transakcje bazodanowe lub choćby przekazywanie spanId/traceId) również wymaga konfiguracji oraz zrozumienia jak to działa. Ostatecznie zdarza się też, że niektóre operacje są wykonywane na jednej konkretnej puli wątków i trzeba wymuszać, bt przetwarzanie było kontynuowane na tej puli, na której chcemy.
Chociaż możliwe, że znów dotykałem tych niestandardowych 20% przypadków, których wsparcie jest ograniczone 😉
Quarkus i Spring
Twórcy Quarkusa są świadomi rzeczywistości, w której dotychczas królował Spring. Chcąc ułatwić migracje do Quarkusa istniejących projektów dodali wsparcie dla najbardziej powszechnych części Springa – Spring MVC, Spring Data, Spring Security oraz kilku innych. Wykaz wspieranych funkcjonalności Springa dostępne pod tym linkiem (między innymi, bo w zasadzie jest tam cały Cheat Sheet dla Quarkusa)
Jeśli chodzi o porównanie Spring Boota z Quarkusem, to różnic jest bardzo wiele.
Pod kątem wydajnościowym (zasoby, wydajność itp.) polecam zerknąć na porównanie na stronie Baeldung. Jakkolwiek trudno wyłonić jednoznacznie zwyciężcę tego porównania.
Jeśli chodzi o wsparcie społeczności, to Spring Boot zdecydowanie wygrywa. Łatwo znaleźć w internecie rozwiązania problemów, choć nie zawsze są to rozwiązania aktualne. Czasem zdarza się znaleźć rozwiązania problemów w starych wersjach frameworku, a szukając czegoś w dokumentacji trzeba zwracać uwagę, czy dokumentacja dotyczy naszej wersji. W przypadku Quarkusa – o ile trudno znaleźć rozwiązanie problemu, o tyle rzadko zdarza się trafić na przedawnione rozwiązanie.
Quarkus i Spring – technologie
Różnice między tymi dwoma frameworkami są również stricte techniczne. Wybierając Spring Boota należy określić, czy chcemy korzystać ze starego dobrego Spring WebMVC (thread-per-request), czy Spring Webflux (Event Loop). Są to wykluczające się technologie. Ciekawostką jest, że w WebMVC również istnieje możliwość asynchronicznego zwracania rezultatu (w innym wątku, niż obsługujący cały request).
W Quarkusie teoretycznie nie ma wyboru co do modelu thread-per-request/Event Loop – pomimo możliwości wyboru między programowaniem reaktywnym, a imperatywnym wszystko działa pod spodem na Event Loopie.
Spring Boot pozwala również na wybór serwera obsługującego żądania HTTP – może to być Tomcat, Jetty, Undertow lub dla stosu reaktywnego Netty, lecz także Tomcat, Jetty, Undertow. Quarkus korzysta z Undertow lub Netty i nie daje takiego wyboru jak Spring Boot.
Wydaje się, że Quarkus stara się dostarczyć spójne współdziałające technologie, ale niekoniecznie zależy na różnorodności technologii. Taka różnorodność mogłaby zwiększać ilość kodu, liczbę abstrakcji, a przez to zwiększać zajętość pamięci oraz spowalniać działanie aplikacji, co jest priorytetem dla Quarkusa. Spring Boot dostarcza integracje dla większej liczby technologii dając większy wybór być może kosztem wydajności.
Swego czasu największą przewagą Quarkusa nad Springiem była możliwość kompilacji do kodu natywnego. Tę przewagę Quarkus utracił wraz z końcem listopada 2022, kiedy wyszedł Spring Boot 3 ze wsparciem dla kompilacji AOT.
Aktualnie jedyną przewagą Quarkusa nad Springiem wydaje się zorientowanie na tzw. Dev Experience. Quarkus oprócz wspomnianej wcześniej konsoli z możliwością przeładowania kodu, uruchomienia wszystkich testów, zmiany poziomu logowania udostępnia również tzw. DevServices, czyli prekonfigurowane TestContainers w czasie developmentu. W skrócie polega to na tym, że zamiast instalować wszystkie niezbędne dla projektu zewnętrzne serwisy (bazy danych, cache, message’ing) można zdefiniować takie serwisy w ramach Quarkusa z preinicjalizowanymi danymi, a przy uruchomieniu konsoli deweloperskiej wszystkie serwisy zostaną wystartowane.
Podsumowanie…
… zostanie udostępnione w następnym wpisie, a tymczasem:
Pax et Bonum 😉