awesome-everything EN
↑ Обратно к восхождению

Производительность

Настройка GC: пейсинг, форма кучи и наблюдаемость аллокаций

Суть Пейсинг управляет моментом запуска GC, GOMEMLIMIT/MaxGCPauseMillis задают границы. Работу цикла определяет live-set, а не RSS. Дашборды rate аллокаций — ведущий индикатор для дежурных.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Go-сервис запущен в контейнере с лимитом 4 ГБ. GOGC оставлен по умолчанию 100. При нагрузке куча вырастает до 3,8 ГБ, pаcer запускает интенсивные циклы GC, и под падает от OOM. Добавление GOMEMLIMIT=3600MiB решает проблему — без изменения кода и без смены коллектора.

Пейсинг и мягкий лимит памяти

Современные коллекторы управляют собой сами: предсказывают, когда нужно запустить следующий цикл, чтобы успеть завершить его до исчерпания кучи. Go’s pacer (переработан в 1.18, доработан в 1.19 с GOMEMLIMIT) смешивает два сигнала — rate роста кучи и долю CPU в GC — в замкнутый контроллер. Цель G1 по времени паузы направляет его pacer к удержанию пауз ниже заданного.

Knobs Go:

  • GOGC=100 (по умолчанию) — запускать GC, когда живые байты удвоились с последнего цикла. Увеличить — откладывать GC (больше памяти, реже циклы); уменьшить — цикличнее (меньше памяти, больше CPU).
  • GOMEMLIMIT — мягкий лимит на общую память рантайма. Pacer становится агрессивнее по мере приближения к лимиту, принимая меньший throughput ради соблюдения границы. Для контейнеризованных сервисов устанавливать в ~90% лимита контейнера.

Knobs JVM:

  • -Xmx — жёсткий потолок кучи.
  • -XX:MaxGCPauseMillis=N — G1 нацеливается на паузы N мс, регулируя размеры регионов и частоту сборки. Слишком малое значение → G1 пересобирает, теряя throughput. Слишком большое → изредка видим 200 мс спайк.
Почему это работает

В контейнеризованных Go-сервисах OOM-убийства от переполнения кучи часты, когда GOMEMLIMIT не задан. Pacer не знает про cgroup-лимит. Установка GOMEMLIMIT в ~90% лимита контейнера — первый knob при настройке. К GOGC обращаться только после этого.

Форма кучи и live-set

Общая куча (RSS) — не то же самое, что live-set — байты, которые реально достижимы. RSS 4 ГБ может содержать лишь 800 МБ живых данных; оставшиеся 3,2 ГБ — мусор, ожидающий следующего цикла, или фрагментация, или зарезервированные арены рантайма.

Время маркировки пропорционально живым байтам, а не RSS. Частота циклов пропорциональна rate аллокаций. Канонический ориентир: размер кучи ~1,5–2x live-set — меньше и GC работает почти непрерывно; больше и память тратится впустую без пропорционального выигрыша в latency.

РантаймМетрика live-setКак читать
Goruntime.MemStats.HeapLiveБайты, помеченные живыми после последнего цикла
JVMMemoryUsage.getUsed() после GCИспользованные байты старого поколения после full GC
V8 / Nodev8.getHeapStatistics().used_heap_sizeПосле принудительного GC через —expose-gc
.NETGC.GetTotalMemory(true)Принудительная сборка, возвращает живые байты

Workloads с большим изменчивым live-set (in-memory кэши, большие агрегации) нуждаются в верхней части диапазона. Маленькие стабильные сервисы работают экономно при 1,5x.

Наблюдаемость аллокаций по рантаймам

Без инструментации рантайма диагноз ставится вслепую.

Go: пакет runtime/metrics + скрейп /debug/pprof/allocs каждые 30 с. GODEBUG=gctrace=1 — каждый цикл в stderr (полезно при разработке, шумно в prod).

JVM: -Xlog:gc*:file=gc.log:time,uptime,level,tags для ротируемых GC-логов. JFR ObjectAllocationInNewTLAB для профилирования аллокаций. Micrometer экспортирует гистограмму gc_pause_seconds и rate gc_memory_allocated_bytes_total в Prometheus.

V8 / Node: process.memoryUsage().heapUsed каждые 15 с; v8.getHeapStatistics() для детального view. perf_hooks.PerformanceObserver с типом 'gc' перехватывает каждое событие паузы программно.

.NET: rate GC.GetTotalAllocatedBytes() + dotnet-counters monitor для live просмотра gen-0-gc-count, gen-1-gc-count, gen-2-gc-count и alloc-rate.

Кросс-рантаймовый паттерн алерта: rate аллокаций выше порога сервиса (обычно 300–500 МБ/с/ядро) более 5 минут. Пара rate аллокаций с p99 на одном графике — когда расходятся, подозревают GC.

Object pooling: когда работает, когда нет

Пулинг переиспользует аллокации для снижения давления GC. Хорошо работает для объектов, дорогих в создании и кратко используемых на hot path: bytes.Buffer, JSON-энкодеры, regexp-объекты, scratch slices.

Пулинг вредит:

  • Объекты дёшевы в создании (накладные расходы пула превышают экономию).
  • Объекты долгоживущие (пул держит память без освобождения).
  • Координационные расходы потоков превышают стоимость аллокации.

У sync.Pool Go есть преимущество: GC может опустошить пул между циклами, память не заблокирована навсегда. Пулы JVM и .NET держат память до явного освобождения — следите за max-pool-size.

Правило: пулить только то, что профиль отмечает как горячий узел аллокации. Не пулить превентивно.

Викторина

Контейнеризованный Go-сервис периодически падает от OOM при пиках трафика. Куча растёт по умолчанию GOGC=100. Какой knob установить первым?

Викторина

GC-лог JVM-сервиса показывает RSS 4 ГБ, но live-set только 600 МБ после каждого full GC. Максимум кучи установлен в 4 ГБ. Что это говорит о настройке?

Расставь шаги по порядку

Расставьте рычаги снижения давления GC по приоритету — от наибольшего к наименьшему:

  1. 1 Устранить аллокацию (мутация на месте, struct-of-arrays, примитивы)
  2. 2 Пул/переиспользование аллокации (sync.Pool, ObjectPool, сброс bytes.Buffer)
  3. 3 Дать escape analysis выполнить стековую аллокацию (меньший объект, локальная область видимости)
  4. 4 Уменьшить аллокацию (меньший struct, меньший буфер, контейнер с предустановленным размером)
  5. 5 Убрать аллокацию с hot path (кэшировать результат, вычислить один раз)
  6. 6 Настроить knobs коллектора (GOGC, MaxGCPauseMillis, max-old-space-size)
  7. 7 Сменить алгоритм GC (ParallelGC → G1 → ZGC)
Вспомните перед уходом
  1. 01
    Почему max-size кучи должен нацеливаться на ~1,5–2x live-set и что происходит на краях диапазона?
  2. 02
    Когда object pooling снижает давление GC, а когда вредит?
Итог

Пейсинг GC использует сигналы рантайма для выбора момента следующего цикла. В Go GOMEMLIMIT — первый knob для контейнеризованных сервисов: он удерживает процесс в пределах бюджета памяти без изменения кода. В JVM MaxGCPauseMillis направляет G1 к целевому времени паузы. Размер кучи должен нацеливаться на ~1,5–2x live-set: ниже — GC работает непрерывно; выше — память тратится впустую. Наблюдаемость аллокаций — дашборды rate, подключённые к Prometheus, с алертами до пробития SLO — ведущий индикатор, улавливающий регрессии GC. Object pooling снижает давление на горячих путях аллокаций, но вредит для дешёвых или долгоживущих объектов. Сначала профиль rate аллокаций; потом knobs коллектора; смена алгоритма — последнее.

Связанные уроки
встречается в159
Продолжить восхождение ↑Внутреннее устройство GC: tri-color инвариант, write barriers и глубокое погружение в рантаймы
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources5
expand
  1. 01
  2. 02
  3. 03
  4. 04
  5. 05

Trademarks belong to their respective owners. Editorial reference only.