Патерни тюнінгу GC (збирача сміття Java)

Коментарі · 29 Перегляди

GC-тюнінг – це не магія сеньйорів, а набір повторюваних патернів. Ти перестаєш гадати, чому сервіс підвисає щоранку о 9:03, і починаєш керувати паузами як дорослий інженер.

GC-тюнінг – це не магія сеньйорів, а набір повторюваних патернів.
Ти перестаєш гадати, чому сервіс підвисає щоранку о 9:03, і починаєш керувати паузами як дорослий інженер.


Правильні патерни тюнінгу GC дають три бонуси:

  1. стабільні latency без раптових фризів,

  2. менші рахунки за залізо,

  3. аргументи, щоб не дати менеджеру спихнути все на «ну ти ж щось там не так накрутив у JVM».

Інструменти

Патерн / інструментКлючовий показник (metric)Типова ціль / орієнтирКоли порівнювати
1Мінімізація паузGC pause P95, ms< 50–100 ms для онлайн сервісівПри аналізі latency API, UI, платежів
2Максимізація пропускної здатностіОброблені запити/події за годинуМаксимум при прийнятних паузахДля batch job, ETL, масових розрахунків
3Контроль розміру heapHeap usage %, Xms/Xmx60–85% при стабільному навантаженніПри плануванні RAM, виборі лімітів у контейнерах
4Робота з young generationMinor GC / сек, young gen usage %Часті, але короткі minor GC без promotion failureВеб/мікросервіси з великою кількістю короткоживучих обʼєктів
5Робота з old generationOld gen occupancy %, GC pause P99Old gen < 70–80%, паузи стабільніСервіси з кешами, сесіями, довгоживучими даними
6Оптимізація Eden spaceEden refill time, MB/s allocEden не заповнюється миттєво, але й не простоюєПіки навантаження, спайки трафіку
7Тюнінг Survivor spaceSurvivor usage %, promotion rate %Немає mass-promotion в old, survivor не переповненийПри рості old gen після піків навантаження
8Зменшення promotion failureКількість promotion failure / год≈ 0 подій за годинуПісля збільшення навантаження або зміни схеми кешування
9Уникнення частих Full GCFull GC / добу< 1–2 Full GC / добуПри скаргах на раптові фризи системи
10Обмеження алокацій у пікових потокахAllocation rate, MB/sЗниження alloc rate без втрати бізнес-логікиАналіз гарячих endpoint-ів та потоків обробки
11Pre-allocation структурPre-allocation hit ratio, %> 80% звернень до вже виділених структурПарсери, цикли, обробка логів
12Оптимізація object poolingPool reuse ratio, %> 70–80% reuse без contentionГарячі обʼєкти з частим створенням/знищенням
13Зменшення тимчасових обʼєктівОбʼєктів/операцію, alloc MB/sВдвічі менше обʼєктів/операцію у гарячих шляхахПісля профайлінгу коду (JFR, async-profiler)
14Tuning G1GC pause targetsMaxGCPauseMillis, msTarget 50–200 ms під SLOG1GC на проді, коли паузи стрибають
15Tuning G1GC regionsРозмір регіону, MB1–4 MB для більшості сервісівПри аналізі ефективності mixed/young GC в G1
16Tuning G1GC mixed cyclesMixed GC / хвилину, reclaimed MBСтабільний reclaim без піків паузКоли old gen повільно, але впевнено зростає
17Tuning G1 InitiatingHeapOccupancyPercentIHOP %, heap at cycle start %Старт циклу при 30–45% heapКоли GC запускається або занадто рано, або запізно
18Tuning G1 ReservePercentReserve %, old gen occupancy %5–15% резерву без OOMПри тупиках через забитий old gen
19Tuning ParallelGC threadsGC threads countGC threads ≈ CPU cores або трохи меншеДля batch, heavy CPU job з ParallelGC
20Tuning ZGC heap fragmentationFragmentation %, relocation thrashНизька фрагментація при паузах < 10 msLow-latency сервіси з ZGC
21Tuning ZGC relocation setRelocated MB / циклРелокація не створює піків CPUВисоке навантаження на heap з ZGC
22Tuning ZGC concurrent cyclesZGC cycles / хвилинуСтабільна частота циклів без starvationПрод-трафік з нерівномірними піками
23Tuning Shenandoah evacuationEvacuated MB / циклЕфективний репак без росту паузJVM на Shenandoah у Linux/RedHat
24Tuning Shenandoah pacingPacing overhead %, pause timeНизький pacing overhead при стабільному latencyКоли Shenandoah дає непередбачувані спайки
25Tuning metaspace limitsMetaspace MB, GC for metadata / годМетадані без неконтрольованого росту, немає OOMДинамічне завантаження класів, плагіни
26Tuning compressed oopsHeap size GB, oops enabledВикористання compressed oops до ~32 GBВеликі сервіси з heap 8–32 GB
27Tuning compressed class pointersClass space MBCompressed class pointers увімкненіВелика кількість модулів / класів
28Зменшення частоти safepointSafepoint time %, events / хвSafepoint < 5% CPU часуПри підозрі, що стопи не від GC
29Уникнення частих stop-the-worldSTW pause P95, msРідкі та короткі STW-паузиКоли юзери бачать реальні фризи
30Розподіл heap по поколінняхYoung/old ratio %, survivor size %Баланс без promotion storm і OOMПісля змін моделі памʼяті або трафіку
31Правильний розподіл thread stackThread stack size KB, threads countНемає stack overflow й зайвого overcommitБагатопотокові сервіси, thread pools
32Tuning DirectByteBuffer usageDirect memory MB, buffer countDirect memory < ліміту, без OOM Direct bufferNetty/NIO сервіси
33GC-friendly data structuresGarbage/op, allocations/opМенше обʼєктів на бізнес-операціюПерехід на primitive/легкі колекції
34Уникнення розмитих посиланьRef-cleared events, cache hit ratioКеш не здувається при першому GCКеші на Soft/Weak reference
35Tuning reference processingReference queue lag, msМалий lag обробки reference-чергІнтенсивне використання Soft/Weak/Phantom
36Tuning string deduplicationDedup ratio %, string heap MBВисокий dedup ratio та економія heapJSON/XML heavy сервіси, логування
37Tuning interned stringsIntern table size, lookups/secТаблиця intern без вибухового ростуDSL, SQL/JPQL, власні мови
38Tuning off-heap кешівOff-heap cache MB, hit ratio %Значна економія heap при стабільному hit ratioВеликі кеші, каталоги, карти
39Tuning netty buffer sizesAvg buffer size, fragmentation %Мінімум дрібних алокацій, низька фрагментаціяNetty/gRPC/кастомні протоколи
40Tuning thread-local cachesThreadLocal hit ratio %, memory per threadВисокий hit ratio без зайвого споживання памʼятіГарячі серіалізатори, форматери, парсери
41Tuning allocation rateAllocation rate MB/s, GC cycles / хвЗниження alloc rate → менше GC циклівПісля ідентифікації гарячих алокацій
42Tuning object lifetime distributionShort-lived vs long-lived %, objectsЧітко розділені профілі життєвого циклуМікс кешів, потоків подій, сесій
43Tuning finalizers avoidanceFinalizable objects count~0 обʼєктів з фіналізаторамиЛегасі-код, старі бібліотеки
44Tuning cleaner tasksCleaner queue lag, cleaned objects/secСтабільний cleanup без піків паузКанали, сокети, файли
45Tuning GC logging verbosityGC log volume MB/добуЛоги дають картину, але не забивають дискПрод-сервіси з аналізом логів
46Аналіз GC логівК-сть проаналізованих релізів/кварталКожен великий реліз має GC-ревʼюРегулярні релізи, зміни трафіку
47Інтеграція GC метрик у PrometheusGC metrics coverage %, кількість дашбордівЄ дашка з GC-паузами, heap, Full GCУсі важливі прод-сервіси
48Контроль GC пауз у SLA% запитів у SLO> 99% запитів укладаються у latency SLOПлатіжні, критичні сервіси з контрактами

Порівняння

Якщо спростити до людських категорій, у нас є три великі сім’ї підходів:

  1. Патерни «я не хочу пауз»
    Мінімізація пауз, уникнення Full GC, tuning G1GC pause targets, tuning ZGC/Shenandoah, контроль safepoint.
    Це для всього, де користувач живий і нервовий: фінтех, маркетплейси, ігри, будь-який UI чи API, який бачить юзер.

  2. Патерни «я хочу прокачати throughput»
    Контроль розміру heap, ParallelGC threads, оптимізація Eden/Survivor, pre-allocation, object pooling.
    Тут ми дозволяємо довші паузи, але хочемо втиснути максимум роботи в одиницю часу: batch job, nightly репорти, ETL.

  3. Патерни «я хочу, щоб GC менше чіпав мій код»
    Зменшення тимчасових об’єктів, GC-friendly data structures, off-heap кеші, Netty-буфери, ThreadLocal кеші.
    Це вже інженерна дієта: ми не тільки тюнимо JVM, а й переписуємо код, щоб він не поводився як пилосос для heap.

Реальне життя, звісно, суміш: ти водночас хочеш і latency, і throughput, і щоб на проді не палало. Але завжди корисно чесно відповісти собі:
«Що мені важливіше саме в цьому сервісі: час відповіді чи кількість оброблених запитів?»
Як тільки з’являється відповідь – стає ясно, які патерни в пріоритеті.

Best practices

  1. Починати не з флагів JVM, а з GC-логів і метрик. Спочатку дивимось, потім крутити.

  2. Міняти один параметр за раз і фіксувати результат, а не влаштовувати «оптом 15 флагів на удачу».

  3. Тримати heap достатньо великим, але не гігантським на всяк випадок. Закон «а раптом знадобиться» вбив не один прод.

  4. Спостерігати GC-паузи під реальним бойовим навантаженням, а не під локальним «я натиснув F5 пару разів».

  5. Виносити патерни в чеклисти й knowledge base, щоб команда не повторювала одні й ті ж помилки кожен реліз.

  6. Чітко розділяти «я міняю код» і «я тюню JVM». Спочатку проста оптимізація коду, потім магія флагів.

  7. Домовитися з командою: будь-який серйозний GC-тюнінг – через профайлер і метрики, а не через «мені здається, так буде краще».

  8. Використовувати G1GC як «дефолт для нормальних людей», а ZGC/Shenandoah – коли ти точно знаєш, що робиш і навіщо.

  9. Окремо стежити за metaspace – він любить вистрілити в голову тоді, коли всі вже дивляться тільки на heap.

  10. Регулярно переглядати GC-настройки після росту трафіку, появи нових фіч чи міграції в інший cloud/hardware.

Типові помилки

  1. Включити екзотичний GC «бо десь у статті написали, що він найшвидший» і забути, що ваша система взагалі інша.

  2. Скопіювати флаги JVM з блогу якогось євангеліста і не розуміти, що вони роблять.

  3. Закрутити MaxGCPauseMillis до смішних значень і дивуватися, чому GC жере CPU, як голодний.

  4. Робити гігантський heap, щоб «рідше прибирався», і потім дивуватися, чому одна пауза триває пів хвилини.

  5. Заливати все в Soft/WeakReference і називати це «розумним кешем», який після першого ж стресу здувається, як повітряна кулька.

  6. Ігнорувати GC-логи, логи валити в /dev/null, а потім у проді грати в детектив.

  7. Ставити десятки лейєрів кешів (кожен зі своєю політикою) і потім звинувачувати GC, що він не розуміє «геніальну архітектуру».

  8. Залишати фіналізатори та сподіватися, що JVM все за вас прибере. JVM не домробітниця.

  9. Ввімкнути GC-логування на максимум деталізації й убити диск логами, а потім їх ще й ніхто не читає.

  10. Плутати «latency проблеми» з «GC проблемами» і не дивитися в базу/мережу/локи, де насправді болить.

Сценарії, де це рятує життя

  1. Прод у піку трафіку (чорна п’ятниця, зарплатний день, реліз нової фічі) і раптово latency стрибає в космос. GC-патерни дозволяють швидко зрозуміти: це GC чи щось інше.

  2. Мікросервіс у Kubernetes, який регулярно викидають за OOM, хоча «пам’яті ж наче хватає». Тюнінг heap, metaspace та алокацій часто рятує поди від небуття.

  3. Легасі моноліт, який всі бояться чіпати. GC-тюнінг стає єдиним безпечним способом витиснути ще трохи життя з динозавра.

  4. Високонавантажений batch-процес, який не вкладається у вікно overnight. Після тюнінгу GC і алокацій він раптом закінчується не о 10 ранку, а в 3:40.

  5. Продукт, де менеджери вже нав’язали ідею «переписати все на іншу мову, бо Java повільна». Пара вечорів з GC-логами іноді дешевші, ніж «rewrite всього світу».

  6. Сервіс, який тримає мільйони підключень/сесій. Без нормального патерну роботи з heap та off-heap кешами він або падає, або душить залізо.

  7. Кейси, де треба виконувати обіцянку по SLA, а не щось абстрактне типу «ну воно ж якось працює». GC-патерни дають ті самі цифри, за які потім можна сперечатися з менеджментом.

Чеклист “як вибрати рішення”

  1. Що болить?
    – довгі паузи,
    – OutOfMemory,
    – нестабільне latency,
    – «CPU на 100% у java-процесу».
    Поки немає чіткої відповіді – не ліземо в флажки.

  2. У нас важливіше latency чи throughput?
    – latency критичний: дивимось у патерни мінімізації пауз, G1/ZGC, heap помірний, багато метрик;
    – throughput важливіший: Parallel/агресивний G1, великі batch-job в нічний час.

  3. Які GC вже стоять?
    – G1GC за замовчуванням: зазвичай тюнимо паузи, heap, young/old, дивимося логи;
    – щось екзотичне: спочатку розуміємо, навіщо його взагалі включили.

  4. Чи є GC-логи і метрики?
    – ні: включити;
    – так: завести хоча б просту дашку з паузами, heap usage, кількістю GC.

  5. Чи є гарячі місця з дикими алокаціями?
    – так: спочатку оптимізуємо код (зменшуємо тимчасові об’єкти), потім тюнимо JVM;
    – ні: фокусуємось більше на конфігурації GC.

  6. Чи готові ми міняти код?
    – ні: працюємо з «м’якими» патернами – heap, покоління, паузи, логи;
    – так: додаємо GC-friendly структури, off-heap, ThreadLocal-кеші за розумом.

  7. Наскільки команда розуміє обраний GC?
    – нуль: краще залишитися на G1GC і не гратися в ZGC/Shenandoah;
    – норм: можна пробувати нові збирачі, але тільки з rollback-планом.

  8. Є rollback-план?
    – якщо ні – це не тюнінг, це російська рулетка.
    – якщо так – можна пробувати змінювати флаги у невеликому відсотку інстансів і дивитися на метрики.

Жарт

GC-тюнінг в корпорації часто виглядає так:

Менеджер:
«У нас latency підскочило. Зроби щось із Java, але без downtime, без ризиків і бажано до кінця дня».

Розробник:
«Може, вимкнемо три зайві прокладки між сервісами, приберемо логування всіх полів у debug і перестанемо серіалізувати пів бази в кожному запиті?»

Менеджер:
«Ні, це довго. Ти ж там просто флажки покрути. Я вчора статтю читав, там людина параметр міняла – і в неї все полетіло».

А потім, звичайно, GC винен.
Не те, що мікроменеджмент, три рівні абстракції й десять мідлів, які «хотіли як краще» :)

Нормальний підхід: тюнити GC так, щоб коли наступного разу хтось у чаті напише «знов та ваша Java лагає», ти мовчки кидаєш графік з паузами в 5–15 мс і питаєш:
«Ну що, шукаємо проблему далі?»

 
 
Коментарі