Spis treści
- 11. Gdzie najczęściej boli Spring Boot w chmurze
- 22. JIT kontra AOT: co naprawdę zmienia GraalVM Native
- 33. Kiedy GraalVM Native daje najlepszy zwrot?
- 44. Co Spring Boot 3 robi za Ciebie, a czego nie zrobi?
- 55. Gdzie WebFlux ma nadal bardzo mocny sens?
- 66. Gdzie Loom daje najlepszy stosunek efektu do kosztu?
- 77. Co zmienia się w utrzymaniu systemu po przejściu na GraalVM Native?
- 88. Koszty i realny zwrot z inwestycji
W zespołach rozwijających mikroserwisy w Javie problemy z wydajnością rzadko zaczynają się od samego kodu biznesowego. Najpierw rośnie liczba usług. Potem pojawia się Kubernetes, autoskalowanie, kolejne środowiska i większy ruch. Na końcu ktoś patrzy na koszty chmury albo czasy odpowiedzi pod obciążeniem - i okazuje się, że nawet mała usługa potrzebuje setek megabajtów pamięci, startuje kilka sekund i przy większym ruchu nie zachowuje się tak stabilnie, jak powinna.
Właśnie w tym miejscu Spring Boot 3.x i Java 21+ dają dwie konkretne dźwignie optymalizacji. Pierwsza to GraalVM Native Image, czyli kompilacja Ahead-of-Time, która skraca czas startu aplikacji i zmniejsza zużycie pamięci. Druga to Project Loom, a dokładniej virtual threads - wirtualne wątki, które pozwalają obsłużyć dużo większą współbieżność bez przechodzenia na model reaktywny. Obok tego nadal istnieje WebFlux, ale dziś nie jest już oczywistą odpowiedzią na każdy problem ze skalowaniem.
To nie jest temat tylko dla architekta. Ma bezpośredni wpływ na koszt utrzymania usług, sens autoskalowania, czas wdrożeń i codzienną pracę zespołu. Jeśli jedna decyzja technologiczna potrafi skrócić czas startu z kilku sekund do setek milisekund i wyraźnie obniżyć zużycie pamięci, to nie jest już „optymalizacja dla sportu”. To decyzja, która realnie wpływa na ekonomikę produktu.
Gdzie najczęściej boli Spring Boot w chmurze
W klasycznych wdrożeniach opartych o JVM najczęściej wracają trzy problemy.
- Czas startu - nowa instancja potrzebuje kilku sekund, więc autoskalowanie reaguje wolniej, aktualizacje trwają dłużej, a w modelu serverless koszty rosną i doświadczenie użytkownika się pogarsza.
- Zużycie pamięci - pamięć w chmurze kosztuje, a przy kilkunastu lub kilkudziesięciu mikroserwisach nawet niewielkie zużycie jednej usługi zaczyna realnie obciążać budżet.
- Przewidywalność pod obciążeniem - nie wystarczy, że średni czas odpowiedzi wygląda dobrze.
To dobry punkt wyjścia do decyzji architektonicznej. Zamiast pytać, czy dana technologia jest „nowoczesna” albo „modna”, lepiej skupić się na tym, co realnie poprawi czas startu, zużycie pamięci i stabilność działania przy rosnącym ruchu - przy akceptowalnym koszcie wdrożenia.
JIT kontra AOT: co naprawdę zmienia GraalVM Native
Klasyczna JVM działa w modelu Just-In-Time. Kod jest analizowany i optymalizowany w trakcie działania aplikacji. To dobrze sprawdza się w długotrwałych procesach, które mają czas się „rozgrzać”. W takich warunkach HotSpot potrafi bardzo skutecznie zoptymalizować najczęściej wykonywane fragmenty i osiągnąć wysoką wydajność.
Ma to jednak swoją cenę. Aplikacja potrzebuje czasu na start i optymalizację działania, a także więcej pamięci na samą maszynę wirtualną i jej mechanizmy.
GraalVM Native Image działa inaczej. Aplikacja jest kompilowana wcześniej do natywnej binarki. Po uruchomieniu nie ma klasycznej JVM ani fazy „rozgrzewania”, a liczba elementów działających w tle jest znacznie mniejsza. W praktyce daje to wyraźny efekt:
- czas startu spada z kilku sekund do setek milisekund, często poniżej 0,2 s,
- zużycie pamięci maleje nawet o 70-80%,
- aplikacja jest gotowa do obsługi ruchu niemal od razu po uruchomieniu.
To szczególnie ważne tam, gdzie instancje uruchamiają się często albo gdzie liczba usług jest duża. W modelu serverless oznacza to mniejszy problem z tzw. „cold startem”. W Kubernetes - szybsze włączanie nowych instancji do ruchu. W architekturze mikroserwisowej - realną szansę na obniżenie kosztów pamięci.
Trzeba jednak uwzględnić kompromisy. Native nie wygrywa w każdym scenariuszu. Przy obciążeniach intensywnie wykorzystujących procesor klasyczna JVM po rozgrzaniu potrafi być szybsza. Jeśli instancja działa długo i wykonuje złożone obliczenia, przewaga JIT nadal ma znaczenie. W typowych usługach webowych, API i integracjach opartych o operacje wejścia/wyjścia różnice są zwykle bardziej korzystne dla podejścia natywnego.
Najprostszy wniosek jest taki: jeśli kluczowe są szybki start i niższe zużycie pamięci, GraalVM Native ma duży sens. Jeśli najważniejsza jest maksymalna wydajność pojedynczej, długo działającej instancji, decyzję warto poprzedzić testami.
Kiedy GraalVM Native daje najlepszy zwrot?
Nie każda usługa potrzebuje podejścia natywnego. Najwięcej korzyści widać zwykle w takich scenariuszach:
- usługi działające w autoskalowaniu lub modelu serverless,
- mikroserwisy z dużą liczbą replik,
- komponenty, w których pamięć jest głównym kosztem działania,
- API i integracje, gdzie dominuje operacje wejścia/wyjścia, a nie ciężkie obliczenia,
- systemy, które muszą szybko wejść do ruchu po wdrożeniu lub nagłym wzroście ruchu.
W takich przypadkach GraalVM Native nie jest tylko „ładnym wynikiem w benchmarku”. To sposób na bardziej efektywne wykorzystanie infrastruktury i realne ograniczenie kosztów.
Co Spring Boot 3 robi za Ciebie, a czego nie zrobi?
Jeszcze niedawno wykorzystanie GraalVM Native w Springu oznaczało sporo ręcznej pracy. Dziś sytuacja wygląda dużo lepiej. Spring Boot 3 oficjalnie wspiera Native Image i wprowadza mechanizmy AOT (Ahead-of-Time), które ograniczają użycie refleksji i generują potrzebne wskazówki dla kompilatora.
Dla zespołu oznacza to krótszą drogę do pierwszego działającego prototypu. Nie trzeba już ręcznie opisywać dużej części frameworka. Wiele popularnych bibliotek ma gotowe metadane, a sam Spring wykonuje sporą część pracy w tle.
Nie oznacza to jednak, że każdy projekt „po prostu” przejdzie na GraalVM Native bez analizy. Problemy najczęściej pojawiają się tam, gdzie aplikacja lub używane biblioteki korzystają z mechanizmów dynamicznych:
- refleksji,
- dynamicznego ładowania klas,
- generowania kodu w trakcie działania aplikacji,
- systemów pluginów,
- skryptów i rozbudowanej instrumentacji.
Jeśli system opiera się na takim podejściu, przejście na Native może wymagać dodatkowej konfiguracji - albo po prostu nie być najlepszym wyborem. To jeden z pierwszych elementów, które warto sprawdzić przed rozpoczęciem migracji.
Co decyduje o kompatybilności bibliotek w GraalVM Native?
W podejściu natywnym duże znaczenie ma coś, czego w klasycznej JVM większość zespołów prawie nie zauważa: tzw. reachability metadata. To informacje, które określają, jakie klasy, zasoby i elementy refleksji mają zostać uwzględnione w natywnej binarce.
W modelu AOT obowiązuje zasada „zamkniętego świata”. Jeśli kompilator nie wykryje czegoś na etapie budowania aplikacji, może to nie być dostępne po jej uruchomieniu. To częste źródło błędów, które nie pojawiały się wcześniej na klasycznej JVM.
Dobra wiadomość jest taka, że sytuacja w ekosystemie Springa jest dziś dużo lepsza niż jeszcze kilka lat temu. Dla wielu popularnych bibliotek dostępne są gotowe metadane, a Spring Boot 3 znacząco upraszcza ich wykorzystanie.
Mimo to warto zachować uporządkowane podejście:
- testować realne scenariusze działania natywnej binarki,
- sprawdzać mniej popularne biblioteki i integracje,
- uwzględniać przypadki dynamiczne, które mogą wymagać własnej konfiguracji.
W praktyce oznacza to, że przejście na GraalVM Native nie jest tylko zmianą ustawienia w procesie budowania aplikacji. To decyzja architektoniczna, która wymaga testów i świadomej analizy.
Jak GraalVM Native wpływa na CI/CD i koszty buildów?
To temat, który często wychodzi dopiero przy pierwszym wdrożeniu. Budowanie natywnej wersji aplikacji jest po prostu cięższe niż tworzenie standardowego JAR-a. Trwa dłużej, zużywa więcej pamięci i CPU, a przy większych projektach potrafi wydłużyć pipeline o kilka lub kilkanaście minut.
To nie jest argument przeciwko GraalVM Native. To koszt, który trzeba świadomie uwzględnić i dobrze wpasować w proces.
Najrozsądniejsze podejście wygląda zwykle tak:
- nie budować wersji natywnej przy każdym commicie,
- uruchamiać ją przy merge’u, release’ach albo raz dziennie,
- mieć osobne testy dla wersji natywnej,
- mierzyć czas budowania i porównywać go z oszczędnościami po stronie działania aplikacji.
W praktyce build trwa dłużej i koszt CI rośnie. Z drugiej strony aplikacja zużywa mniej pamięci i szybciej się skaluje. Jeśli usługa działa cały czas i ma kilka lub kilkanaście replik, te oszczędności zwykle są większe niż dodatkowy koszt buildów.
WebFlux vs Loom: gdzie naprawdę jest różnica
Przez lata WebFlux był dla wielu zespołów synonimem „skalowalnego Springa”. Dziś ta rozmowa wygląda inaczej, bo Project Loom daje trzecią opcję: prosty, blokujący model programowania, ale z dużo lepszą współbieżnością niż wcześniej.
WebFlux nadal ma przewagę wtedy, gdy cały system działa w pełni asynchronicznie. Jeśli korzystasz z reaktywnego sterownika bazy, nieblokującego klienta HTTP, streamingu, Server-Sent Events czy WebSocketów, podejście reaktywne daje dużą kontrolę nad zasobami i dobrze radzi sobie przy bardzo dużej liczbie jednoczesnych połączeń.
Problem w tym, że większość systemów biznesowych nie działa w tak „czystym” środowisku. Jest JDBC, są starsze SDK dostawców, są zewnętrzne API, które blokują, i biblioteki, które nigdy nie były projektowane z myślą o modelu reaktywnym.
W takiej sytuacji pojawiają się wąskie gardła. Jeśli część operacji blokuje, trzeba przenosić je do osobnych pul wątków, które przy większym ruchu łatwo się przeciążają.
Loom podchodzi do tego inaczej. Wirtualne wątki pozwalają pisać kod w klasyczny sposób, ale bez kosztu, który kiedyś wiązał się z dużą liczbą wątków. Gdy operacja blokuje, zatrzymuje tylko „swój” wirtualny wątek, a nie całą aplikację.
Dlatego w wielu typowych API i systemach CRUD Loom daje lepszy efekt przy mniejszej złożoności niż WebFlux.
Co zwykle wychodzi w testach?
W testach wydajności dość szybko widać powtarzalny schemat.
W środowisku w pełni asynchronicznym:
- WebFlux bywa minimalnie lepszy pod względem przepustowości,
- wysokie percentyle (np. p99) są bardziej stabilne,
- zużycie zasobów jest bardzo efektywne przy dużej liczbie jednoczesnych połączeń.
W realnym świecie mieszanym:
- Loom często wypada lepiej, bo lepiej radzi sobie z blokującymi elementami (JDBC, SDK, starsze integracje),
- nie przeciąża głównych wątków obsługi i dodatkowych pul,
- pozwala zwiększyć współbieżność bez przebudowy kodu na model reaktywny.
Dla większości systemów opartych na klasycznych API i CRUD-ach to ważna informacja. W wielu przypadkach nie trzeba przechodzić na WebFlux, żeby poprawić skalowalność. Często wystarczy Java 21, Spring Boot 3 i wirtualne wątki.
Gdzie WebFlux ma nadal bardzo mocny sens?
Nie ma sensu traktować Loom jako zastępstwa dla podejścia reaktywnego. To po prostu inne narzędzie. Są obszary, w których WebFlux nadal jest bardzo dobrym wyborem.
Dotyczy to przede wszystkim:
- streamingu danych,
- Server-Sent Events,
- WebSocketów,
- systemów działających w pełni asynchronicznie,
- zespołów, które mają doświadczenie w podejściu reaktywnym i chcą je dalej rozwijać.
Jeśli architektura i kompetencje zespołu są już oparte na Reactorze, nie ma sensu przepisywać wszystkiego tylko dlatego, że pojawił się Loom. Z drugiej strony nie ma też potrzeby wchodzenia w podejście reaktywne „na zapas”. Jeśli system opiera się na blokujących integracjach, często prostsze rozwiązania będą bardziej efektywne i łatwiejsze w utrzymaniu.
Gdzie Loom daje najlepszy stosunek efektu do kosztu?
Dla wielu zespołów to właśnie tu widać największą wartość Java 21. Wirtualne wątki dobrze sprawdzają się w takich przypadkach jak:
- klasyczne REST API,
- systemy z JDBC lub JPA,
- aplikacje z blokującymi integracjami,
- rozbudowane systemy CRUD,
- zespołów, które chcą prostszego debugowania i niższego kosztu utrzymania kodu.
Z biznesowego punktu widzenia to ma duże znaczenie. Nie chodzi tylko o lepszą współbieżność. Równie ważne jest to, że nie trzeba wprowadzać bardziej złożonego modelu programowania tam, gdzie nie daje on realnej przewagi. Dzięki temu łatwiej utrzymać system, szybciej wdrażać zmiany i ograniczyć koszt rozwoju w dłuższym czasie.
Co zmienia się w utrzymaniu systemu po przejściu na GraalVM Native?
Przejście na GraalVM Native wpływa nie tylko na wydajność, ale też na to, jak wygląda życie aplikacji po wdrożeniu. W klasycznej JVM wiele rzeczy można diagnozować dynamicznie. W wersji natywnej więcej trzeba zaplanować z wyprzedzeniem.
Najważniejsze obszary, o które warto zadbać wcześniej:
- metryki aplikacyjne,
- śledzenie zapytań między usługami (tracing),
- logowanie z możliwością zmiany poziomu szczegółowości,
- testy natywnej wersji aplikacji w procesie CI,
- monitorowanie pamięci, czasu startu i zachowania pod obciążeniem.
Jedna z częstszych pułapek dotyczy logowania. Jeśli w procesie budowania aplikacji część ścieżek debug lub trace zostanie pominięta, później nie da się ich po prostu włączyć na produkcji bez ponownego zbudowania aplikacji. Dlatego strategię logowania trzeba ustalić wcześniej - a nie dopiero wtedy, gdy pojawi się problem.
To jeden z tych elementów, które odróżniają działający prototyp od stabilnego wdrożenia produkcyjnego.
Koszty i realny zwrot z inwestycji
To moment, w którym temat zaczyna być istotny dla CTO i osób odpowiedzialnych za budżet. Budowanie wersji natywnej jest droższe, ale działanie aplikacji zwykle tańsze. Kluczowe jest więc sprawdzenie, gdzie pojawia się punkt opłacalności.
Najprostszy sposób patrzenia na to wygląda tak: jeśli jedna usługa po przejściu na GraalVM Native zużywa o około 250-300 MB RAM mniej, a masz 20 usług po 3 repliki, to daje łącznie 15-18 GB pamięci mniej w klastrze
W praktyce może to oznaczać:.
- mniejsze instancje,
- mniej węzłów w klastrze,
- lepsze wykorzystanie zasobów,
- niższy koszt środowisk dodatkowych,
- większy zapas na wzrost bez natychmiastowego dokładania infrastruktury.
Drugi obszar to czas startu aplikacji. Jeśli nowa instancja uruchamia się w ułamku sekundy zamiast kilku sekund, autoskalowanie reaguje szybciej i można ograniczyć liczbę „zapasowych” replik.
W niektórych scenariuszach ma to duże znaczenie, szczególnie przy skalowaniu do zera i w środowiskach serverless.
Dla wielu firm właśnie tutaj pojawia się realny zwrot z inwestycji, nie na poziomie pojedynczej usługi, ale w łącznym koszcie całego systemu.
Kiedy GraalVM Native albo WebFlux nie mają sensu?
Nie każda usługa powinna działać w wersji natywnej. Nie każdy system potrzebuje WebFlux.
Najczęstsze przypadki, w których GraalVM Native się nie sprawdza:
- systemy oparte na dynamicznych pluginach,
- intensywne użycie refleksji i dynamicznego ładowania klas,
- bardzo dynamiczne frameworki i skrypty,
- projekty wymagające rozbudowanego monitorowania przy użyciu agentów JVM,
- aplikacje nastawione na maksymalną wydajność CPU, gdzie czas startu i zużycie pamięci mają mniejsze znaczenie.
Najczęstsze przypadki, w których WebFlux nie jest dobrym wyborem:
- aplikacje z dużą liczbą blokujących integracji,
- zespoły bez doświadczenia w podejściu reaktywnym,
- systemy, które nie potrzebują streamingu ani w pełni asynchronicznego modelu,
- sytuacje, w których WebFlux jest wybierany „na zapas”, bez konkretnego uzasadnienia.
Najgorsze decyzje architektoniczne zwykle biorą się z jednego źródła: technologia jest wybierana dlatego, że brzmi dobrze, a nie dlatego, że rozwiązuje konkretny problem.
Jak podejść do decyzji architektonicznej?
Najrozsądniejsze podejście nie polega na wdrożeniu wszystkiego naraz. Lepiej potraktować to jak kontrolowany pilotaż.
Dobra sekwencja decyzji wygląda zwykle tak:
- najpierw sprawdzić, czy głównym problemem jest czas startu i zużycie pamięci, czy raczej współbieżność i blokujące operacje,
- jeśli system opiera się na klasycznym MVC, JDBC i blokujących integracjach, zacząć od Java 21 i wirtualnych wątków,
- jeśli usługa ma dużo replik lub działa w autoskalowaniu, sprawdzić potencjał GraalVM Native,
- jeśli architektura jest w pełni asynchroniczna i opiera się na streamingu lub bardzo dużej współbieżności, rozważyć WebFlux,
- pilotażowo zmierzyć czas startu, zużycie pamięci, przepustowość, p95/p99 i koszt infrastruktury dla 1-2 usług,
- dopiero na tej podstawie podejmować decyzję o szerszym wdrożeniu.
To podejście opiera się na prostej zasadzie: najpierw pomiar, potem decyzja.
Kierunek dla większości zespołów
Dla większości zespołów dobrym punktem startu jest dziś Java 21, Spring Boot 3.x i wirtualne wątki w klasycznym Spring MVC. To pozwala poprawić skalowalność bez rewolucji w kodzie i bez znaczącego wzrostu złożoności utrzymania.
GraalVM Native warto wdrażać selektywnie - tam, gdzie czas startu i zużycie pamięci realnie wpływają na koszt: w mikroserwisach z dużą liczbą replik, systemach autoskalujących się, rozwiązaniach serverless i tam, gdzie pamięć jest głównym ograniczeniem.
WebFlux najlepiej zostawić tam, gdzie daje rzeczywistą przewagę: w streamingu, komunikacji w czasie rzeczywistym i systemach działających w pełni asynchronicznie - szczególnie jeśli zespół ma już doświadczenie w tym modelu.
Najważniejsze jest jedno: nie wybierać technologii „na zapas”. Wybierać rozwiązania dopasowane do realnego problemu, sposobu działania systemu i kompetencji zespołu.








