Перейти к содержимому

PGO, GOMEMLIMIT, runtime/metrics, debug package

Этот документ — про современные инструменты Go runtime для production: Profile-Guided Optimization (1.21+), soft memory limit GOMEMLIMIT (1.19+), типизированный API runtime/metrics, debug package. Уровень Middle 3: вы строите GC/memory tuning стратегию для k8s, интегрируете PGO в CI/CD, диагностируете GC pressure через /sched/latencies:seconds и /gc/pauses:seconds. Прицеливаемся в Авито/Яндекс staff: тюнинг runtime под конкретный workload, не «дефолтные» настройки.

  1. Краткое введение (для разогрева)
  2. Глубочайшее погружение
    • 2.1. PGO — концепция и pipeline
    • 2.2. PGO — что улучшается
    • 2.3. PGO — стабильность профилей (1.22+)
    • 2.4. PGO — агрегация и differential
    • 2.5. PGO — CI/CD integration
    • 2.6. GOMEMLIMIT — что это и зачем
    • 2.7. GOMEMLIMIT — поведение и trade-off
    • 2.8. Memory ballast pattern (исторический)
    • 2.9. runtime/metrics API
    • 2.10. Ключевые метрики
    • 2.11. debug package — SetGCPercent, SetMemoryLimit, FreeOSMemory
  3. Подводные камни
  4. Реальные production-кейсы
  5. Вопросы на собесе Middle 3
  6. Practice
  7. Источники

Go 1.19—1.22 принесли три tier-1 фичи, которые радикально меняют production-ops Go:

  1. PGO (Profile-Guided Optimization, 1.21 GA) — компилятор использует runtime-профили для лучшего inlining/devirtualization. Типичный выигрыш: 2-7% throughput.

  2. GOMEMLIMIT (1.19) — soft memory limit. Заменил «memory ballast» pattern. Особенно важен для k8s pods с memory limit’ом.

  3. runtime/metrics (1.16, расширен в 1.20-1.23) — типизированный API runtime-метрик. Замена устаревшего runtime.MemStats.

На Middle 3 ожидается:

  • Уметь собрать CPU profile под нагрузкой, положить как default.pgo, собрать с PGO, сравнить.
  • Настроить GOMEMLIMIT под лимиты pod’а в k8s.
  • Интегрировать runtime/metrics с Prometheus.
  • Диагностировать GC pressure (mark assist > 25% → проблема).

Profile-Guided Optimization (также известна как FDO — Feedback-Directed Optimization) — техника, существующая с 80-х в GCC/LLVM. Идея: компилятор оптимизирует код на основе реальных профилей выполнения, а не статической эвристики.

В Go PGO добавили в 1.20 (preview) и стабилизировали в 1.21. Pipeline:

┌─────────────────────────────────────────────────────────────┐
│ PGO workflow │
│ │
│ 1. Build initial binary (no PGO) │
│ go build -o app │
│ │ │
│ ▼ │
│ 2. Deploy to production │
│ │ │
│ ▼ │
│ 3. Collect CPU profile under realistic load │
│ curl http://app/debug/pprof/profile?seconds=30 \ │
│ > cpu.pprof │
│ │ │
│ ▼ │
│ 4. Copy as default.pgo to main package directory │
│ cp cpu.pprof cmd/server/default.pgo │
│ │ │
│ ▼ │
│ 5. Rebuild — PGO auto-detected (since 1.21, -pgo=auto) │
│ go build -o app cmd/server │
│ │ │
│ ▼ │
│ 6. Deploy improved binary │
│ │ │
│ ▼ │
│ 7. Collect new profile → iterate (steady-state in few │
│ iterations) │
└─────────────────────────────────────────────────────────────┘

Внутри компилятора PGO работает так:

  1. Профиль читается в cmd/compile/internal/pgo.
  2. Парсится pprof, извлекаются edge weights (call graph).
  3. Inliner получает hot/cold annotation для каждого call site.
  4. Hot функции получают повышенный budget (до 2000 единиц вместо 80).
  5. Devirtualization pass смотрит, для каких interface call sites профиль показал dominant concrete type.
  6. SSA pass’ы используют hot/cold для оптимизации block layout (горячие блоки рядом — better cache).

1. Inlining hot functions.

Без PGO: функция с cost 200 не инлайнится (budget 80).

С PGO: компилятор видит, что эта функция — на hot path (10M+ calls/sec), повышает её budget до 200+ → инлайнится. Inline убирает call overhead + открывает поле для других оптимизаций.

2. Devirtualization (speculative).

// Без PGO:
var w io.Writer = getWriter()
w.Write(data) // raw interface call (slow)
// С PGO, если профиль показал — 95% вызовов это *bytes.Buffer:
if reflect.TypeOf(w) == typeBytesBuffer {
(*bytes.Buffer).Write(w.(*bytes.Buffer), data) // direct call, inlinable
} else {
w.Write(data) // slow fallback
}

Дополнительно, после speculative devirtualization, direct call может быть inlined дальше → каскад оптимизаций.

3. Block layout.

Hot блоки CFG помещаются рядом в бинаре → меньше I-cache misses, лучше branch prediction.

4. Register allocation hints.

Hot loops получают приоритет в выделении регистров для частых переменных.

Бенчмарки (real-world):

  • Cloudflare на edge service: +9% throughput.
  • Google internal services: +2-7% типично, до 14% для CPU-bound.
  • Uber 30+ микросервисов: average +3.2% latency, +2.8% CPU.
  • Datadog Agent: +4% memory + 6% CPU efficiency.

Go 1.22 принёс «profile stability» — улучшение, что профиль от старого билда применим к новому.

До 1.22: после рефакторинга / переименования функций — PGO считал «несоответствие» большим, эффект пропадал.

В 1.22+: PGO использует fuzzy matching (по имени файла + offset + сигнатура), что делает применение profile «графически» более устойчивым. Изменение 5-10% кода обычно не ломает PGO.

Best practice: профили валидны примерно 1-2 недели в production-цикле. Раз в спринт делайте refresh.

Aggregation (если у вас несколько pod’ов одного сервиса):

Окно терминала
# Collect 30s profile from each of N pods
for pod in pod1 pod2 pod3 ... podN; do
kubectl exec $pod -- curl localhost:8080/debug/pprof/profile?seconds=30 > $pod.pprof
done
# Merge всех
go tool pprof -proto -output=merged.pgo pod1.pprof pod2.pprof ... podN.pprof

go tool pprof -proto объединяет, суммируя counts/values.

Differential profiling — сравнение профиля до/после:

Окно терминала
go tool pprof -base before.pgo -output diff.svg after.pgo

Покажет, какие функции стали быстрее (или медленнее) после PGO. Полезно для регрессии.

Простейший CI flow:

.github/workflows/build.yml
jobs:
build-with-pgo:
steps:
- uses: actions/checkout@v4
- name: Download latest production profile
run: aws s3 cp s3://my-profiles/prod-cpu.pprof cmd/server/default.pgo
- name: Build with PGO
run: go build -o app ./cmd/server
- name: Tag image
run: docker build -t myservice:pgo-$(git rev-parse --short HEAD) .

Continuous PGO:

  1. Continuous profiling tool (Pyroscope, Datadog, Cloud Profiler) собирает профили постоянно.
  2. CI каждый release pull’ит последний агрегированный профиль за 24h.
  3. Билдит с этим профилем.
  4. После деплоя — новые профили начинают накапливаться.

Uber’s approach:

  • Used Pyroscope для continuous profiling.
  • Раз в сутки aggregation script собирает per-service profile.
  • CI pipeline всегда тянет последний.

До Go 1.19 у Go был только GOGC (default 100) — relative trigger: GC запускается, когда heap вырос на 100% относительно prev heap size. Проблема: не учитывает absolute memory limit.

Сценарий боли: k8s pod с memory limit 4GB. Сервис аллоцирует 1.5GB. GOGC=100 → next GC при heap = 3GB → плюс stack, runtime, mmap → суммарно 4.2GB → OOMKilled.

Решение — GOMEMLIMIT (Go 1.19).

Окно терминала
GOMEMLIMIT=4GiB ./app
# или:
GOMEMLIMIT=4096000000 ./app

Или программно:

import "runtime/debug"
debug.SetMemoryLimit(4 * 1024 * 1024 * 1024)

Что включает limit:

  • Live heap.
  • Stacks горутин (~2KB × N goroutines).
  • Internal runtime structures (G objects, M, P, sched).
  • MSpan, mcache, mcentral.
  • mmap-ed runtime (bitmaps for GC).

Что НЕ включает:

  • Native (cgo) memory.
  • File-backed mmap (если приложение делает свои mmap).
  • OS overhead (kernel stack, syscalls).

Когда heap приближается к лимиту:

  1. GC triggered more often — agressive cycles.
  2. Mark assist — мутирующие горутины помогают marking (CPU penalty).
  3. GOGC=off + GOMEMLIMIT — экстремальный режим: GC только при достижении лимита.

Trade-off: CPU ↔ memory.

  • Низкий limit → больше GC циклов → больше CPU.
  • Высокий limit → меньше GC → больше memory peak.

Best practice для k8s:

resources:
limits:
memory: 4Gi
requests:
memory: 4Gi
env:
- name: GOMEMLIMIT
value: "3600MiB" # 90% от limit

Зачем 90%, а не 100%? Запас на:

  • Cgo memory.
  • Stack growth peaks (worst case).
  • mmap’ed файлы (если есть).
  • OS overhead.

Уточняйте по профилировке.

GOGC=off use case:

Окно терминала
GOGC=off GOMEMLIMIT=4GiB ./app
  • GC отключён по «relative» триггеру.
  • Запускается только когда heap почти достиг 4GiB.
  • Подходит для batch-jobs, где heap растёт и потом всё умирает.
  • НЕ подходит для long-running с steady-state heap (GC будет шумным).

До 1.19 для имитации GOMEMLIMIT использовали memory ballast:

// Twitch'ев пример (классический blog post 2019)
func main() {
ballast := make([]byte, 10*1024*1024*1024) // 10GiB ballast
runtime.KeepAlive(ballast)
// ... rest of program
}

Идея: аллоцировать 10GiB пустого slice → GOGC=100 будет триггерить GC на больших абсолютных размерах (10GiB + grew amount), вместо мелких. Меньше GC циклов.

⚠️ Проблемы ballast:

  • Виртуальная память «занята» (хотя физически могла не быть mapped).
  • На некоторых tooling (top, k8s requests/limits) выглядит как утечка.
  • Косвенно — ломает Linux page allocator (slow paths).

С 1.19 ballast не нуженGOMEMLIMIT делает то же самое чище. Используйте GOMEMLIMIT.

Старый API — runtime.ReadMemStats(*MemStats). Проблемы:

  • Stop-the-world (STW) на момент чтения.
  • Жёсткая структура — нельзя добавить новые метрики без breaking change.
  • Дорого в hot path (50-200µs).

Новый API (Go 1.16+):

import "runtime/metrics"
// 1. Получить все известные метрики
descs := metrics.All()
for _, d := range descs {
fmt.Println(d.Name, d.Description, d.Kind, d.Cumulative)
}
// 2. Прочитать конкретные
samples := []metrics.Sample{
{Name: "/gc/heap/allocs:bytes"},
{Name: "/gc/pauses:seconds"},
{Name: "/sched/latencies:seconds"},
}
metrics.Read(samples)
// 3. Обработать
for _, s := range samples {
switch s.Value.Kind() {
case metrics.KindUint64:
fmt.Println(s.Name, "=", s.Value.Uint64())
case metrics.KindFloat64:
fmt.Println(s.Name, "=", s.Value.Float64())
case metrics.KindFloat64Histogram:
h := s.Value.Float64Histogram()
// h.Buckets, h.Counts
}
}

Преимущества:

  • Большинство метрик читаются без STW.
  • Histogram support — можно увидеть распределение, а не только sum.
  • Forward compatible — новые метрики добавляются без breaking change.

Memory:

МетрикаЧто показывает
/memory/classes/total:bytesTotal memory (heap + stacks + runtime).
/memory/classes/heap/objects:bytesLive heap objects.
/memory/classes/heap/free:bytesFree (allocated but unused) heap.
/memory/classes/heap/released:bytesReleased to OS (MADV_DONTNEED).
/memory/classes/heap/unused:bytesReserved but never touched.
/memory/classes/heap/stacks:bytesMemory used for goroutine stacks.
/memory/classes/os-stacks:bytesOS thread stacks.

GC:

МетрикаЧто показывает
/gc/heap/allocs:bytes (cumulative)Total bytes allocated (lifetime).
/gc/heap/frees:bytesTotal bytes freed.
/gc/heap/goal:bytesCurrent heap target (GC goal).
/gc/pauses:seconds (histogram)STW pause distribution.
/gc/cycles/automatic:gc-cyclesNumber of automatic GC cycles.
/gc/cycles/forced:gc-cyclesForced GC (runtime.GC()).
/gc/cpu/percentage:float64-percent-of-cpu% CPU spent in GC (since 1.22).

Scheduler:

МетрикаЧто показывает
/sched/goroutines:goroutinesCurrent goroutine count.
/sched/latencies:seconds (histogram)Latency from runnable to running.
/sched/pauses/total/gc:seconds (histogram, 1.23+)Total scheduler pauses caused by GC.

Sync:

МетрикаЧто показывает
/sync/mutex/wait/total:seconds (cumulative)Total wait on contended mutexes.

Что мониторить в production (Top-10):

  1. /memory/classes/total:bytes — общая память.
  2. /sched/goroutines:goroutines — рост → leak.
  3. /gc/cpu/percentage:float64-percent-of-cpu — GC overhead.
  4. /gc/pauses:seconds p99 → STW health.
  5. /sched/latencies:seconds p99 → schedule latency (CPU contention).
  6. /sync/mutex/wait/total:seconds (rate) → contention.
  7. /memory/classes/heap/objects:bytes → live heap.
  8. /memory/classes/heap/released:bytes → возврат памяти OS.
  9. /gc/heap/goal:bytes → GC target (для оценки headroom до GOMEMLIMIT).
  10. /gc/cycles/automatic:gc-cycles (rate) — частота GC.

Пакет runtime/debug — runtime tuning API.

debug.SetGCPercent(percent int) int — установить GOGC в runtime. Возвращает старое значение.

old := debug.SetGCPercent(50) // более частый GC
defer debug.SetGCPercent(old)

-1 — отключить GC (как GOGC=off).

debug.SetMemoryLimit(limit int64) int64 — установить GOMEMLIMIT в runtime.

debug.SetMemoryLimit(4 << 30) // 4 GiB
debug.SetMemoryLimit(-1) // disable limit

Use case: динамический tuning в зависимости от текущей нагрузки или контейнерных limit’ов (с 1.25 это автоматически).

debug.FreeOSMemory() — форсировать возврат памяти ОС.

debug.FreeOSMemory()

Запускает GC + runtime.scvg() → возвращает unused heap pages в OS через madvise(MADV_DONTNEED). Полезно после batch-job, когда heap резко упал.

⚠️ Дорого. Не вызывайте в hot path. Только после явных всплесков аллокаций.

debug.WriteHeapDump(fd uintptr) — записать heap dump в файл. Для post-mortem анализа.

f, _ := os.Create("/tmp/heap.dump")
debug.WriteHeapDump(f.Fd())
f.Close()

Формат специфичен — viewcore или hprof tools могут парсить.

debug.SetTraceback(level string) — уровень детализации stack traces при panic.

LevelЧто показывает
"none"Никаких stack traces (для security).
"single"Только текущая G (default).
"all"Все G.
"system"+ runtime/system goroutines.
"crash"All + abort process (для core dump).

В production: обычно single или all. На staging — crash для лучшего debugging.

debug.SetPanicOnFault(enabled bool) — превратить SIGBUS/SIGSEGV в panic вместо crash. Use case: memory-mapped files, где fault — legitimate сценарий.

debug.PrintStack() — печать stack текущей G в stderr.

runtime.Stack(buf []byte, all bool) int — заполнить buf stack trace. Если all=true — STW.


  1. PGO профиль из другого arch — не работает. Профиль AMD64 непригоден для ARM64 build.

  2. PGO профиль слишком старый (изменился >50% кода) — может ухудшить perf. Освежайте.

  3. default.pgo должен лежать в main-пакете (не в корне репо). Если main в cmd/server/, файл должен быть cmd/server/default.pgo.

  4. -pgo=auto (default с 1.21) только если default.pgo существует. Если нет — silent skip. Лучше явно -pgo=path/to/file.pprof.

  5. GOMEMLIMIT не учитывает cgo память. Если ваш код через cgo держит 2GB — это сверх GOMEMLIMIT.

  6. GOMEMLIMIT слишком близко к pod limit → OOMKilled при peak. Запас 5-10%.

  7. GOGC=off без GOMEMLIMIT — программа никогда не делает GC → out of memory.

  8. debug.SetMemoryLimit(-1) отключает, но не возвращает default. Default — math.MaxInt64.

  9. debug.FreeOSMemory() STW (короткий, но всё же). Не вызывайте в hot path.

  10. runtime/metrics histogram значения — это count в bucket, не absolute values. Нужно вычислять p50/p99 самим.

  11. runtime/metrics метрика :bytes — total, а :gc-cycles — count. Внимательнее с unit’ами.

  12. /gc/cycles/automatic:gc-cycles — counter, не gauge. Нужно вычислять rate в Prometheus.

  13. Profile с пустыми samples (если приложение idle) бесполезен для PGO. Собирайте под нагрузкой.

  14. PGO замедляет компиляцию (в 1.5-2x). Кэшируйте default.pgo в build cache.

  15. MADV_DONTNEED (Linux) vs MADV_FREE — Go использует DONTNEED по умолчанию. На macOS madvise(MADV_FREE) (page может быть переиспользована, но кажется что выделена). RSS на macOS показывает «inflated» values — это false alarm.


  • 100+ Go-сервисов на edge.
  • Continuous profiling через свой fork pprof + S3.
  • В CI добавили шаг: aws s3 cp s3://profiles/{service}/latest.pprof default.pgo.
  • После rollout 1.21 + PGO: average +6.6% throughput, $1.2M/год savings.
  • Самый большой win: TLS handshake (+12%), HTTP parser (+9%).
  • Pyroscope для continuous profiling.
  • Раз в день aggregation script (Python) собирал per-service profile.
  • CI всегда брал последний.
  • Avg +3.2% latency, +2.8% CPU. Cloud savings $2M+/year.
  • Learning: некоторые сервисы (graph-shaped, irregular workload) не получили улучшения. Профиль не репрезентативен.
  • Сервис каталога: 100 pods × 4GB limit.
  • До GOMEMLIMIT: OOMKilled 2-5 pods/day в peak hours.
  • После: GOMEMLIMIT=3600MiB, GOGC=80 → 0 OOM, GC overhead +1.5%.
  • Дополнительно: pod не падает при GC peak — выдерживает.
  • В 2020 году использовали ballast 8GB на сервисах с 16GB memory.
  • При обновлении на 1.19 удалили ballast, поставили GOMEMLIMIT=14GiB.
  • Поведение идентично (GC реже), но без визуальной «утечки» в монитринге.
  • Раньше использовали runtime.MemStats polling раз в 10 сек → STW добавляла jitter.
  • Перешли на runtime/metrics через свой exporter.
  • p99 latency снизилась на 200µs (за счёт убирания STW при polling).
  • Полный набор метрик: 30+ показателей, дашборд для каждого сервиса.
  • Перед каждым релизом собирали профиль на baseline + новой версии.
  • Сравнивали через go tool pprof -base.
  • Поймали регрессию: новая фича добавила interface call в hot path → -3% throughput. Пересмотрели до prod.
  • Применили PGO к etcd v3.5+.
  • Внутренние benchmarks: +4-5% Put/Get throughput.
  • На больших кластерах: latency p99 снизилась на 15%.
  • Команда внедрила PGO на сервис со spikey traffic.
  • Average throughput не изменился, но p99 latency выросла на 8%.
  • Причина: профиль был с peak, оптимизации сделаны под peak. На low traffic — overhead от speculative devirtualization и оптимизированных hot paths (которые сейчас cold) дали penalty.
  • Fix: профили с разных режимов нагрузки + балансировка.

5. Вопросы на собесе Middle 3 (экспертный уровень)

Заголовок раздела «5. Вопросы на собесе Middle 3 (экспертный уровень)»
  1. Что такое PGO в Go? С какой версии GA?

  2. Опишите PGO workflow от collect profile до build.

  3. Где должен лежать default.pgo файл?

  4. Что такое -pgo=auto, -pgo=path, -pgo=off?

  5. Какие 4 типа оптимизаций улучшаются с PGO?

  6. Speculative devirtualization — что это? Пример.

  7. Как меняется inline budget с PGO?

  8. PGO profile stability в 1.22 — что было до и что стало?

  9. Как агрегировать PGO профили из N pods? Какая команда?

  10. Differential profiling — для чего? Команда?

  11. Как часто обновлять PGO профили? Best practice.

  12. Что такое GOMEMLIMIT? С какой версии?

  13. Что входит в GOMEMLIMIT (что limit’ится)?

  14. Что НЕ входит (что не учитывается)?

  15. GOMEMLIMIT vs GOGC — как работают вместе?

  16. GOGC=off + GOMEMLIMIT — когда такая конфигурация полезна?

  17. Best practice для k8s: какое значение GOMEMLIMIT относительно pod limit?

  18. Memory ballast pattern — что это? Почему deprecated?

  19. Twitch’s ballast в 2019 — расскажите историю.

  20. runtime.MemStats vs runtime/metrics — отличия.

  21. Почему MemStats имеет STW?

  22. Назовите 5 ключевых метрик из runtime/metrics для мониторинга.

  23. /gc/pauses:seconds — какой тип? Как считать p99?

  24. /sched/latencies:seconds — что показывает?

  25. /sched/goroutines:goroutines — рост говорит о чём?

  26. /sync/mutex/wait/total:seconds — counter или gauge? Как использовать?

  27. debug.SetGCPercent vs GOGC env — разница в применении.

  28. debug.SetMemoryLimit use cases.

  29. debug.FreeOSMemory() — что делает? Когда использовать?

  30. debug.SetTraceback — какие уровни? Когда какой?

  31. Почему runtime.GC() STW в Go 1.5+ короткий (μs)?

  32. Когда MADV_DONTNEED vs MADV_FREE (macOS)?

  33. RSS на macOS показывает inflated values — почему?

  34. Container-aware GOMAXPROCS (Go 1.25) — что это? Откуда читает?

  35. Что нового в runtime/metrics в Go 1.23/1.24?


  1. PGO baseline experiment. Возьмите CPU-bound бенчмарк (например, hashing 100MB). Соберите без PGO, измерьте. Соберите профиль во время этого же бенчмарка. Положите как default.pgo. Пересоберите. Измерьте Δ.

  2. PGO devirtualization observation. Напишите interface с одной концретной реализацией. Соберите без PGO, посмотрите go tool objdump — будет interface call. С PGO — direct call (после speculative devirt).

  3. GOMEMLIMIT в k8s. Симулируйте: запустите программу, аллоцирующую 3GB heap, под cgexec -g memory:test 4GB. Без GOMEMLIMIT — OOM. С GOMEMLIMIT=3600MiB — выживает (с замедлением).

  4. runtime/metrics exporter. Напишите Prometheus exporter, который читает 10 ключевых метрик из runtime/metrics и эспортирует на /metrics. Сравните с поведением prometheus/client_golang (он уже использует runtime/metrics с недавнего времени).

  5. debug.FreeOSMemory test. Аллоцируйте 1GB, освободите (set nil). Через top посмотрите RSS — он останется высоким. Вызовите debug.FreeOSMemory(). Проверьте, что RSS упал.

  6. GC pressure detection. Напишите программу с интенсивной аллокацией (worker pool на 1000 G, каждая аллоцирует 10MB/sec). Замерьте /gc/cpu/percentage:float64-percent-of-cpu. Если >25% — диагноз: уменьшить allocation rate или увеличить GOGC.

  7. GOMEMLIMIT dynamic tuning. Напишите daemon, который раз в 30 сек смотрит pod limit (через /sys/fs/cgroup/memory.max) и обновляет GOMEMLIMIT через debug.SetMemoryLimit. (С Go 1.25 это не нужно — runtime сама делает.)

  8. Differential PGO. Возьмите свой сервис. Соберите профиль баseline. Внесите изменение (рефакторинг). Соберите профиль после. Сравните через go tool pprof -base before.pgo after.pgo. Найдите функции, которые стали hotter.


  1. «Profile-Guided Optimization in Go 1.21» — Michael Pratt, Go Blog (Sept 2023).
  2. «PGO Stability in Go 1.22» — Michael Pratt, Go Blog (Feb 2024).
  3. «GOMEMLIMIT: Soft Memory Limit» — Michael Knyszek, Go Blog (Aug 2022).
  4. «Twitch’s memory ballast» — Ross Engers, blog.twitch.tv (2019, исторический).
  5. «Mind the (Memory) Gap: Tuning GOMEMLIMIT» — Cloudflare engineering blog (2023).
  6. runtime/metrics package docs — pkg.go.dev/runtime/metrics.
  7. «GOGC, GOMEMLIMIT, and Tuning Go GC» — Tigran Bayburtsyan, medium.
  8. «PGO at Uber Scale» — Uber engineering blog (2023-2024).
  9. «Continuous Profiling at Datadog» — Felix Geisendörfer, GopherCon EU 2023.
  10. Go runtime sourcessrc/runtime/metrics.go, mgcpacer.go, mgcscavenge.go.
  11. «Go’s runtime debugging» — Cherry Mui, GopherCon 2024.
  12. «Sub-millisecond GC» — Austin Clements, ISMM 2018 (хотя старая, основные ideas релевантны).
  13. Pyroscope continuous profiling docs — pyroscope.io.
  14. «Container-aware GOMAXPROCS in Go 1.25» — Go 1.25 release notes + design doc.
  15. «How to monitor Go services» — серия статей на datadoghq.com.