Strojenie JVM: G1GC, heap i metryki, które naprawdę warto obserwować

0
23
Rate this post

G1GC tuning, heap sizing JVM, pause time GC, young GC i mixed GC, old gen occupancy, allocation rate Java, GC logs analiza, JVM w kontenerach, latency p95 p99 a GC, full GC diagnoza, limity pamięci i OOM, tuning JVM krok po kroku

Nawigacja:

Gdy usługa działa dobrze na co dzień, ale pod skokiem ruchu zaczyna się podejrzenie GC

Typowa sytuacja operacyjna

Scenariusz jest dobrze znany w utrzymaniu usług JVM. Aplikacja Java działa poprawnie przy zwykłym ruchu, dashboardy wyglądają spokojnie, a problem pojawia się dopiero wtedy, gdy rośnie liczba równoległych żądań, uruchamia się batch, zmienia się format serializacji odpowiedzi albo cache zaczyna pracować inaczej niż wcześniej. Nagle p95 i p99 latency idą w górę, zużycie CPU faluje, a pierwszym podejrzanym staje się garbage collector.

To podejrzenie bywa trafne, ale równie często prowadzi na boczny tor. Sam wzrost opóźnień nie oznacza jeszcze, że problemem jest GC. Bardzo podobne objawy dają nasycone CPU, kolejki w pulach wątków, wolniejsze I/O, presja na bazę danych, blokady, a także pamięć poza heapem. Co wiemy? Wiemy zwykle tyle, że aplikacja zwolniła. Czego nie wiemy? Czy zwolniła przez GC, czy GC tylko towarzyszy przeciążeniu.

To ważna różnica, bo strojenie JVM ma sens jako decyzja operacyjna w konkretnej sytuacji, a nie jako odruch polegający na dokładaniu flag. Jeśli usługa pod obciążeniem produkuje nagle dużo obiektów tymczasowych, to G1GC może jedynie ujawnić skalę zjawiska. Nie naprawi sam z siebie zmiany w kodzie, która zwiększyła allocation rate kilkukrotnie.

Krótki przykład sytuacyjny: po wdrożeniu nowej wersji API zaczynają rosnąć pauzy GC. Na pierwszy rzut oka wygląda to jak problem collectora. Po zestawieniu metryk okazuje się jednak, że zmieniono sposób serializacji odpowiedzi i każda odpowiedź generuje więcej krótkotrwałych obiektów. GC nie „zepsuł się”; po prostu dostał więcej pracy.

Pierwszy filtr: korelacja zamiast domysłów

Przy podejrzeniu problemów z JVM dobrze zadać dwa krótkie pytania kontrolne: co dokładnie koreluje z objawem i czego jeszcze nie mierzymy. Jeśli wzrost p99 latency pokrywa się z dłuższymi pauzami GC albo skokiem częstotliwości young GC, to jest punkt zaczepienia. Jeśli opóźnienia rosną, ale GC pozostaje stabilny, szukanie winy w G1GC najpewniej będzie stratą czasu.

Drugi filtr dotyczy limitów środowiska. W nowoczesnych wdrożeniach problem wydajnościowy bardzo często nie wynika z samego heapu, tylko z relacji między heapem, pamięcią poza heapem i limitem kontenera. Aplikacja może mieć jeszcze wolny heap, a mimo to być blisko OOMKill, bo metaspace, direct buffers, stosy wątków i narzut procesu zjadają resztę budżetu pamięci.

Z praktycznego punktu widzenia podejrzenie GC jest sensowne dopiero wtedy, gdy objaw ma ślad w metrykach JVM. Bez tego tuning szybko zamienia się w serię zmian, po których nie wiadomo, co realnie pomogło, a co tylko przesunęło problem w inne miejsce.

Co najpierw sprawdzić, zanim ruszysz G1GC i heap

Co wiemy, a czego jeszcze nie wiemy

Przed zmianą ustawień trzeba uporządkować minimalny zestaw danych. Nie chodzi o pełną analizę internals JVM, tylko o kilka sygnałów, które pozwalają odróżnić problem z GC od przeciążenia aplikacji. Najbardziej użyteczny pakiet startowy wygląda tak:

  • p95 i p99 latency aplikacji, najlepiej w korelacji z ruchem,
  • throughput, czyli liczba obsłużonych żądań lub zadań w czasie,
  • całkowite CPU procesu oraz CPU zużywane przez GC,
  • zużycie pamięci procesu i pamięci heap,
  • liczba GC i łączny czas pauz,
  • old gen occupancy po GC,
  • allocation rate,
  • sygnały restartów, OOM, eviction lub throttlingu w środowisku.

Pojedyncza metryka bardzo łatwo wprowadza w błąd. Sama liczba kolekcji niewiele mówi, jeśli nie wiadomo, jak długie są pauzy i czy aplikacja rzeczywiście odczuwa ich koszt. Sama wysoka pamięć heap też nie przesądza sprawy, jeśli po GC old gen wyraźnie spada, a opóźnienia pozostają stabilne. Korelacja jest tutaj ważniejsza niż dowolna jedna liczba.

Jeśli nie ma jeszcze dobrych dashboardów, nie trzeba od razu budować rozbudowanego systemu obserwowalności. Wystarczy ustalić, czy problem występuje stale czy tylko przy określonym wzorcu ruchu, czy pojawia się full GC, czy old gen po kolejnych cyklach wraca do podobnego poziomu, oraz czy całkowity footprint procesu zbliża się do limitu środowiska.

Krótka checklista przed zmianą flag

Zanim padnie decyzja o strojeniu G1GC, dobrze przejść przez kilka prostych punktów. To zwykle oszczędza więcej czasu niż czytanie długiej listy opcji JVM.

  1. Sprawdź, czy skok opóźnień jest związany z konkretnym wzorcem obciążenia: burst ruchu, batch, harmonogram, większy payload.
  2. Porównaj p95/p99 latency z pause time GC na tym samym przedziale czasu.
  3. Zobacz, czy rośnie allocation rate i czy young GC pojawiają się znacznie częściej.
  4. Oceń, czy old gen po GC spada, czy raczej pełznie w górę cykl po cyklu.
  5. Sprawdź, czy problemem nie jest pamięć poza heapem lub limit kontenera.
  6. Ustal, czy występuje full GC. Jeśli tak, to jest sygnał alarmowy ważniejszy niż drobne strojenie pause target.

Jeżeli brak korelacji między latency i GC, a CPU jest stale wysokie poza aktywnością collectora, pierwszy trop powinien prowadzić do kodu, bazy, cache, locków albo sieci. Tuning JVM ma sens wtedy, gdy metryki naprawdę wskazują JVM jako część problemu, a nie tylko tło dla ogólnego przeciążenia.

Jedna zmiana naraz i porównanie na podobnym obciążeniu

To jedna z niewielu zasad, które działają niemal zawsze. Zmiana heapu, celu pauz i kilku dodatkowych flag jednocześnie utrudnia diagnozę bardziej, niż pomaga. Po takim ruchu zwykle da się stwierdzić tylko tyle, że „coś się zmieniło”. Nie wiadomo jednak, czy zysk dał większy heap, czy po prostu test trafił na spokojniejszy okres ruchu.

Najbezpieczniej traktować tuning JVM jak serię małych eksperymentów. Zbierasz stan wyjściowy, robisz jedną zmianę, porównujesz zachowanie przy podobnym obciążeniu i dopiero wtedy decydujesz o kolejnym kroku. To brzmi zachowawczo, ale właśnie ten tryb najczęściej pozwala dojść do użytecznej odpowiedzi bez chaosu.

Kobieta z laptopem w nowoczesnym centrum danych przy serwerach
Źródło: Pexels | Autor: Christina Morillo

G1GC jako punkt wyjścia, nie religia: kiedy ruszać ustawienia, a kiedy zostawić domyślne

Co G1GC zwykle robi dobrze

Na nowoczesnych wersjach JDK G1GC jest rozsądnym punktem wyjścia dla wielu usług produkcyjnych. Nie dlatego, że jest najlepszy w każdych warunkach, ale dlatego, że zwykle oferuje dobry kompromis między przepustowością a przewidywalnością pauz. W usługach sieciowych, gdzie liczy się nie tylko średnia, lecz także ogon opóźnień, taki kompromis bywa bardziej praktyczny niż pogoń za maksymalnym throughput.

To ważny fakt: domyślne ustawienia G1GC często są wystarczające. Jeżeli aplikacja ma problem z memory leak, skokową alokacją po zmianie kodu albo źle dobranym limitem pamięci kontenera, collector nie jest głównym winowajcą. Zmiana collectora albo agresywne strojenie wielu flag może ukryć objawy tylko na chwilę.

G1GC ma sens stroić wtedy, gdy obraz z metryk jest dość czytelny: young GC są zbyt częste przy silnej alokacji, mixed GC zaczynają być kosztowne, pause time regularnie dobija do poziomu odczuwalnego dla SLA, a old gen zachowuje się niestabilnie bez jednoznacznego wycieku. W takich warunkach można myśleć o korekcie heapu i ostrożnym dostrajaniu.

Kiedy tuning ma sens, a kiedy szkoda czasu

Strojenie G1GC ma sens, gdy problem dotyczy kształtu pracy pamięci, a nie ewidentnej awarii systemowej. Jeżeli usługa wykonuje dużo krótkich alokacji, heap jest napięty, ale pamięć po GC wraca, zwiększenie heapu albo dopasowanie konfiguracji może ograniczyć częstotliwość pauz. Jeżeli mixed GC są długie, a old gen jest pod stałą presją, również jest pole do decyzji operacyjnej.

Inaczej wygląda sytuacja, gdy pojawiają się full GC spowodowane skrajnym brakiem pamięci, regularne OOM albo trwały wzrost old gen bez powrotu po kolejnych cyklach. Wtedy tuning G1GC najczęściej nie rozwiązuje problemu, bo źródło leży głębiej: retencja obiektów, cache bez kontroli, wyciek pamięci, zbyt mały budżet środowiska lub błędne założenia co do obciążenia.

Są też przypadki, w których oczekiwania wobec latency są po prostu ostrzejsze niż to, co rozsądnie da się osiągnąć przy danym heapie i wzorcu ruchu. Jeśli wymagania na pauzy są bardzo niskie, a skala pamięci duża, może pojawić się pytanie o inne collectory, takie jak ZGC czy Shenandoah. To jednak osobna decyzja architektoniczna, nie szybki zamiennik dla uporządkowanej diagnostyki G1GC.

Heap to decyzja systemowa: nie tylko więcej pamięci, ale też koszt pauz i ryzyko poza heapem

Jak czytać zależność między heapem, GC i limitem środowiska

Najczęstszy odruch przy problemach z GC brzmi: zwiększyć heap. Czasem to działa, ale nie jest uniwersalną receptą. Zbyt mały heap zwykle oznacza częstsze young GC, większą wrażliwość na burst alokacji i mniejszy margines bezpieczeństwa pod nagłym ruchem. Z kolei zbyt duży heap może obniżyć częstotliwość kolekcji, ale zwiększyć koszt pojedynczych pauz i podnieść całkowity footprint procesu.

Praktyczne kryterium jest dość proste. Zwiększenie heapu ma sens, gdy aplikacja odzyskuje pamięć, young GC są bardzo częste, old gen po GC nie pełznie stale w górę, a problem wygląda bardziej na presję alokacji niż na retencję. Wtedy większy heap daje collectorowi więcej przestrzeni oddechu i zmniejsza liczbę krótkich przerw.

Zwiększenie heapu nie powinno być pierwszym ruchem, gdy old gen po kolejnych cyklach stale rośnie. Taki obraz mówi częściej o obiektach, które są długo utrzymywane, niż o samym braku miejsca na chwilowe alokacje. Większy heap może wówczas tylko opóźnić objawy i sprawić, że awaria przyjdzie później, ale z bardziej kosztownymi pauzami.

Trzeba patrzeć szerzej niż na samo Xmx. Heap to tylko część pamięci procesu. Dochodzą metaspace, direct buffers, code cache, stosy wątków oraz narzut natywnego runtime. W kontenerze ustawienie heapu bardzo blisko limitu pamięci jest częstym błędem, bo przy zwykłym wzroście liczby wątków albo większym użyciu buforów poza heapem aplikacja może zostać ubita mimo tego, że wykres heap wygląda „bezpiecznie”.

Dwa krótkie przykłady decyzji

Pierwszy scenariusz: po zwiększeniu heapu liczba young GC spada, throughput lekko rośnie, ale pojedyncze pauzy są dłuższe i p99 latency się pogarsza. To nie jest automatyczny sukces. Jeżeli usługa ma krytyczny ogon opóźnień, mniejsza liczba GC nie rekompensuje dłuższych zatrzymań. Taki wynik oznacza zwykle, że heap został zwiększony w dobrą stronę z punktu widzenia częstotliwości, ale nie w dobrą stronę z punktu widzenia celu biznesowego.

Drugi scenariusz: heap occupancy wygląda poprawnie, old gen nie budzi podejrzeń, a usługa mimo to wpada w niestabilność i restarty. Po sprawdzeniu całego footprintu okazuje się, że to pamięć poza heapem podeszła za wysoko względem limitu kontenera. W takiej sytuacji strojenie G1GC nie trafia w problem. Najpierw trzeba uporządkować budżet pamięci procesu.

Bezpieczniejszy punkt startowy dla ustawień pamięci

Jeżeli trzeba wykonać pierwszy ruch konfiguracyjny, zwykle bezpieczniej zacząć od sensownego ustalenia rozmiaru heapu niż od grzebania w wielu zaawansowanych flagach G1GC. Dla stabilności operacyjnej lepsze bywa przewidywalne środowisko pamięciowe niż agresywnie dostrojony collector działający na źle oszacowanym limicie kontenera.

W praktyce oznacza to trzy kroki: oszacować budżet na cały proces, zostawić zapas poza heapem i dopiero potem sprawdzać, czy częstotliwość i koszt GC odpowiadają wymaganiom. To podejście bywa mniej efektowne niż długa lista flag, ale częściej prowadzi do wyniku, który da się utrzymać na produkcji.

Metryki, które naprawdę prowadzą do decyzji

Minimalny zestaw obserwacji na start

Jeżeli trzeba szybko odpowiedzieć sobie, co obserwować przy strojeniu JVM, lista może być krótka. Najważniejsze są: pause time, częstotliwość GC, allocation rate, old gen occupancy po GC, CPU związane z GC oraz relacja tych metryk do p95 i p99 latency aplikacji. To zestaw, który pozwala odsiać większość fałszywych tropów.

Pause time należy czytać nie przez samą średnią, ale przez rozkład. Średnia często wygląda dobrze nawet wtedy, gdy pojedyncze dłuższe pauzy psują doświadczenie użytkownika albo rozbijają SLA. Dla usług online znaczenie ma ogon: czy p95 i p99 opóźnień aplikacji skaczą równolegle z dłuższymi pauzami GC.

Allocation rate pomaga odróżnić dwie różne sytuacje, które z zewnątrz wyglądają podobnie: „GC mieli bez przerwy” oraz „aplikacja produkuje tyle krótkotrwałych obiektów, że collector ledwo nadąża”. To nie to samo. Jeśli alokacja rośnie skokowo po wdrożeniu albo tylko pod konkretnym typem ruchu, pytanie brzmi: co wiemy o zmianie wzorca obiektów, a czego jeszcze nie wiemy o samym kodzie ścieżki krytycznej? W praktyce taki trop częściej prowadzi do profilowania alokacji niż do kolejnej flagi JVM.

Old gen occupancy po GC to z kolei prosty test na kierunek problemu. Jeżeli po kolejnych cyklach poziom wraca do podobnego pułapu, presja może być chwilowa i operacyjnie da się ją opanować rozmiarem heapu albo korektą limitów. Jeżeli jednak linia po GC układa się coraz wyżej, obraz jest inny: rośnie retencja, a nie tylko tempo pracy. Wtedy decyzja jest mniej „stroić” i bardziej „sprawdzić, co zostaje przy życiu”. Bez tej obserwacji łatwo pomylić zdrowy burst z powolnym dochodzeniem do awarii.

Jest jeszcze CPU związane z GC. Sam wzrost użycia procesora nie przesądza o niczym, ale zestawiony z throughput i latency mówi dużo. Gdy CPU idzie w górę, a aplikacja nie robi proporcjonalnie więcej pracy, collector może odbierać zbyt duży kawałek budżetu wykonania. Zdarza się to zwłaszcza tam, gdzie ruch jest nierówny: średnio wszystko wygląda poprawnie, lecz w szczycie system zaczyna „spalać” rdzenie na odzyskiwanie pamięci zamiast na obsługę żądań. To dobry moment, by zadać krótkie pytanie kontrolne: czy problemem są pauzy, czy już koszt ciągłej pracy GC między pauzami.

Najbardziej użyteczny obraz powstaje dopiero po zszyciu danych z JVM i z aplikacji. Jeśli p99 latency pogarsza się dokładnie wtedy, gdy wydłużają się pauzy albo rośnie aktywność GC, związek jest mocny. Jeśli nie ma takiej korelacji, nie ma sensu na siłę obwiniać collectora. W wielu incydentach produkcyjnych to właśnie ten prosty filtr oszczędza najwięcej czasu: najpierw sprawdzić zależność czasową, potem dopiero ruszać konfigurację. Przy JVM rzadko wygrywa najbardziej pomysłowa flaga; częściej wygrywa spokojna sekwencja decyzji oparta na kilku metrykach, które coś rozstrzygają.

Najbezpieczniej zaczynać od obrazu całości: ruch, alokacja, pamięć po GC, CPU i wpływ na latency. Gdy te elementy składają się w jeden wzór, decyzja o heapie albo G1GC zwykle robi się zaskakująco prosta.

Jak czytać objawy bez zgadywania: kilka typowych scenariuszy produkcyjnych

Częste young GC, ale pamięć po nich ładnie wraca

To jeden z bardziej „operacyjnych” przypadków. System zwalnia pod skokiem ruchu, ale po GC heap nie wygląda źle, old gen nie wspina się stale, a problem objawia się raczej dużą liczbą krótkich pauz niż jedną dużą katastrofą. Co wiemy? Aplikacja intensywnie alokuje i collector ma mało przestrzeni na oddech. Czego jeszcze nie wiemy? Czy to naturalny koszt ruchu, czy efekt zmiany w kodzie, która zwiększyła liczbę tymczasowych obiektów.

Pierwszy ruch bywa prosty: sprawdzić, czy heap nie jest po prostu zbyt ciasny względem realnego wzorca pracy. Jeśli jednocześnie limit środowiska zostawia margines poza heapem, ostrożne zwiększenie Xmx może ograniczyć częstotliwość young GC. Jeżeli po zmianie liczba kolekcji spada, p95 albo p99 się uspokajają, a old gen nadal zachowuje się stabilnie, to znak, że problem rzeczywiście był blisko granicy pojemności, a nie w samej retencji.

Mixed GC trwają długo, a old gen jest stale pod presją

Tutaj sygnał ostrzegawczy jest inny. G1GC nie tylko sprząta młode obiekty, ale coraz częściej wraca do regionów old gen, bo tam też zaczyna brakować oddechu. Jeśli po kolejnych cyklach poziom zajętości old gen nie wraca do podobnego pułapu, tylko pełznie w górę, samo „dokręcanie” collectora zwykle nie jest dobrą pierwszą decyzją.

W takiej sytuacji rozsądniej sprawdzić trzy rzeczy przed zmianą flag:

  • czy nie wzrósł udział długo żyjących obiektów po ostatnich zmianach,
  • czy cache mają limity i realną politykę wygaszania,
  • czy nie doszło do zmiany wzorca ruchu, który trzyma więcej danych jednocześnie.

Jeżeli odpowiedzi na te pytania są niejasne, tuning G1GC staje się strzałem w ciemno. Mixed GC potrafią być skutkiem, nie przyczyną.

Pauzy wyglądają źle, ale korelacja z latency jest słaba

To częsty fałszywy trop. W dashboardzie GC widać piki, więc naturalnie pojawia się podejrzenie collectora. Tymczasem opóźnienia aplikacji nie rosną w tych samych momentach albo rosną wcześniej niż aktywność GC. Wtedy trzeba odsunąć GC na bok i spojrzeć na resztę ścieżki: bazę danych, zależności sieciowe, pule wątków, timeouty, blokady, przeciążenie CPU niezwiązane z kolektorem.

Sam fakt, że GC istnieje i bywa kosztowny, nie oznacza jeszcze, że to on psuje usługę. Dobra praktyka jest prosta: jeśli nie ma wyraźnej zbieżności czasowej między objawem biznesowym a zachowaniem pamięci, nie zaczyna się od zmiany flag.

Kiedy logi GC są potrzebne i czego w nich szukać na początku

Moment, w którym metryki przestają wystarczać

Metryki powiedzą, że jest źle. Logi GC pomagają zobaczyć, dlaczego jest źle. Nie są potrzebne do każdej drobnej korekty, ale stają się ważne wtedy, gdy obraz z monitoringu jest niejednoznaczny: pauzy są długie, old gen zachowuje się dziwnie, pojawiają się full GC albo zmiany heapu dają skutki uboczne trudne do wyjaśnienia.

Na początek nie trzeba robić pełnej analizy internalsów JVM. Wystarczy sprawdzić kilka rzeczy:

  • jak często występują young, mixed i full GC,
  • ile pamięci było użyte przed i po danym cyklu,
  • czy collector odzyskuje zauważalną ilość pamięci,
  • czy pauzy rosną stopniowo wraz z presją na old gen,
  • czy pojawiają się oznaki problemów z to-space albo ewakuacją.

To ostatnie jest ważne zwłaszcza wtedy, gdy środowisko jest napięte pamięciowo. Jeżeli G1GC ma za mało przestrzeni na przenoszenie obiektów między regionami, objawy potrafią nagle przestać wyglądać jak „zwykłe GC” i zacząć przypominać walkę o przetrwanie procesu.

Na co nie patrzeć zbyt wcześnie

Łatwo utknąć w szczegółach, które brzmią technicznie, ale nie prowadzą do decyzji. Jeśli na tym etapie nie wiadomo jeszcze, czy problem dotyczy presji alokacji, retencji czy limitu środowiska, rozbieranie pojedynczych faz kolektora na drobne części bywa przedwczesne. Najpierw trzeba ustalić kierunek: czy pamięć wraca, czy nie wraca; czy pauzy są incydentalne, czy systematyczne; czy problem jest skorelowany z ruchem i latency.

Pułapki środowiska kontenerowego, które mylą obraz JVM

Heap ustawiony zbyt blisko limitu pamięci

W kontenerach błąd bywa banalny: heap został ustawiony niemal pod sam limit, bo „przecież chodzi o Javę i pamięć”. Tyle że proces używa więcej niż heap. Gdy dojdą stosy wątków, metaspace, bufory direct i reszta narzutu, margines znika. Efekt może wyglądać jak niestabilny GC, choć realnym problemem jest ogólny memory pressure środowiska.

W takiej sytuacji widok z jednego wykresu heap usage uspokaja, a system i tak wpada w restarty albo jest ubijany przez otoczenie. Dlatego przy każdej decyzji o Xmx trzeba jednocześnie zadać pytanie o całkowity footprint procesu. To szczególnie ważne w usługach z dużą liczbą wątków albo intensywnym użyciem bibliotek sieciowych i buforów poza heapem.

Fałszywy komfort po zmianie tylko jednej warstwy

Zdarza się też odwrotny przypadek: ktoś zwiększa limit kontenera, ale nie zmienia heapu, przez co system nadal wykonuje bardzo częste young GC. Z zewnątrz wygląda to tak, jakby „dano pamięć, a nic się nie poprawiło”. W praktyce poprawiono budżet procesu, ale nie zmieniono przestrzeni, w której collector pracuje na co dzień.

Dlatego decyzje o pamięci dobrze traktować jako zestaw: limit środowiska, heap, zapas poza heapem i obserwacja zachowania po wdrożeniu. Jedna liczba prawie nigdy nie opisuje całego problemu.

Minimalny porządek zmian: jak stroić, żeby wiedzieć, co pomogło

Jedna zmiana naraz, ale z jasnym kryterium sukcesu

Największy bałagan pojawia się nie wtedy, gdy konfiguracja jest skromna, tylko wtedy, gdy kilka rzeczy zmienia się równocześnie. Heap, limity kontenera, nowe flagi G1GC, inny build aplikacji i jeszcze zmiana ruchu testowego. Potem trudno odpowiedzieć, co faktycznie zadziałało.

Bezpieczniejszy porządek wygląda tak:

  • najpierw zapisać stan wyjściowy: pause time, częstotliwość GC, old gen po GC, CPU, p95/p99 latency,
  • ustalić jedną hipotezę, na przykład „heap jest za mały względem burstów alokacji”,
  • zmienić jeden parametr lub jedną grupę ściśle powiązanych parametrów,
  • porównać wynik pod podobnym obciążeniem,
  • zatrzymać zmianę, jeśli poprawa jednej metryki pogarsza ważniejszą dla usługi.

To nie jest procedura akademicka. To sposób na uniknięcie sytuacji, w której system „czuje się trochę lepiej”, ale nikt nie wie dlaczego i na jak długo.

Od czego zwykle zacząć na nowoczesnej JVM

Jeśli aplikacja działa na współczesnym JDK i używa G1GC, punkt startowy bywa mniej egzotyczny, niż sugerują internetowe listy flag. Najpierw porządkuje się rozmiar heapu i budżet pamięci procesu. Dopiero później, gdy dane pokazują konkretny wzorzec problemu, rozważa się korekty związane z celem pauz czy zachowaniem collectora.

To podejście ma jedną zaletę: łatwiej odróżnić problem konfiguracyjny od problemu aplikacyjnego. Jeżeli po uporządkowaniu heapu i limitów nadal widać rosnącą retencję albo nielogiczne skoki alokacji, trop prowadzi już bardziej do kodu, cache, kolejek albo modelu ruchu niż do kolejnej warstwy tuningu JVM.

Przy pierwszej optymalizacji najwięcej daje chłodne pytanie: czy próbujemy pomóc collectorowi pracować w sensownych warunkach, czy próbujemy nim zamaskować problem, którego on nie rozwiąże. Jeśli odpowiedź jest niejasna, lepiej zatrzymać się przy metrykach i logach niż dokładać kolejne flagi.