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, не «дефолтные» настройки.
Содержание
Заголовок раздела «Содержание»- Краткое введение (для разогрева)
- Глубочайшее погружение
- 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
- Подводные камни
- Реальные production-кейсы
- Вопросы на собесе Middle 3
- Practice
- Источники
1. Краткое введение (для разогрева)
Заголовок раздела «1. Краткое введение (для разогрева)»Go 1.19—1.22 принесли три tier-1 фичи, которые радикально меняют production-ops Go:
-
PGO (Profile-Guided Optimization, 1.21 GA) — компилятор использует runtime-профили для лучшего inlining/devirtualization. Типичный выигрыш: 2-7% throughput.
-
GOMEMLIMIT (1.19) — soft memory limit. Заменил «memory ballast» pattern. Особенно важен для k8s pods с memory limit’ом.
-
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% → проблема).
2. Глубочайшее погружение
Заголовок раздела «2. Глубочайшее погружение»2.1. PGO — концепция и pipeline
Заголовок раздела «2.1. PGO — концепция и pipeline»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 работает так:
- Профиль читается в
cmd/compile/internal/pgo. - Парсится pprof, извлекаются edge weights (call graph).
- Inliner получает hot/cold annotation для каждого call site.
- Hot функции получают повышенный budget (до 2000 единиц вместо 80).
- Devirtualization pass смотрит, для каких interface call sites профиль показал dominant concrete type.
- SSA pass’ы используют hot/cold для оптимизации block layout (горячие блоки рядом — better cache).
2.2. PGO — что улучшается
Заголовок раздела «2.2. PGO — что улучшается»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.
2.3. PGO — стабильность профилей (1.22+)
Заголовок раздела «2.3. PGO — стабильность профилей (1.22+)»Go 1.22 принёс «profile stability» — улучшение, что профиль от старого билда применим к новому.
До 1.22: после рефакторинга / переименования функций — PGO считал «несоответствие» большим, эффект пропадал.
В 1.22+: PGO использует fuzzy matching (по имени файла + offset + сигнатура), что делает применение profile «графически» более устойчивым. Изменение 5-10% кода обычно не ломает PGO.
Best practice: профили валидны примерно 1-2 недели в production-цикле. Раз в спринт делайте refresh.
2.4. PGO — агрегация и differential
Заголовок раздела «2.4. PGO — агрегация и differential»Aggregation (если у вас несколько pod’ов одного сервиса):
# Collect 30s profile from each of N podsfor pod in pod1 pod2 pod3 ... podN; do kubectl exec $pod -- curl localhost:8080/debug/pprof/profile?seconds=30 > $pod.pprofdone
# Merge всехgo tool pprof -proto -output=merged.pgo pod1.pprof pod2.pprof ... podN.pprofgo tool pprof -proto объединяет, суммируя counts/values.
Differential profiling — сравнение профиля до/после:
go tool pprof -base before.pgo -output diff.svg after.pgoПокажет, какие функции стали быстрее (или медленнее) после PGO. Полезно для регрессии.
2.5. PGO — CI/CD integration
Заголовок раздела «2.5. PGO — CI/CD integration»Простейший CI flow:
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:
- Continuous profiling tool (Pyroscope, Datadog, Cloud Profiler) собирает профили постоянно.
- CI каждый release pull’ит последний агрегированный профиль за 24h.
- Билдит с этим профилем.
- После деплоя — новые профили начинают накапливаться.
Uber’s approach:
- Used Pyroscope для continuous profiling.
- Раз в сутки aggregation script собирает per-service profile.
- CI pipeline всегда тянет последний.
2.6. GOMEMLIMIT — что это и зачем
Заголовок раздела «2.6. GOMEMLIMIT — что это и зачем»До 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).
2.7. GOMEMLIMIT — поведение и trade-off
Заголовок раздела «2.7. GOMEMLIMIT — поведение и trade-off»Когда heap приближается к лимиту:
- GC triggered more often — agressive cycles.
- Mark assist — мутирующие горутины помогают marking (CPU penalty).
GOGC=off+ GOMEMLIMIT — экстремальный режим: GC только при достижении лимита.
Trade-off: CPU ↔ memory.
- Низкий limit → больше GC циклов → больше CPU.
- Высокий limit → меньше GC → больше memory peak.
Best practice для k8s:
resources: limits: memory: 4Gi requests: memory: 4Gienv: - 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 будет шумным).
2.8. Memory ballast pattern (исторический)
Заголовок раздела «2.8. Memory ballast pattern (исторический)»До 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.
2.9. runtime/metrics API
Заголовок раздела «2.9. runtime/metrics API»Старый 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.
2.10. Ключевые метрики
Заголовок раздела «2.10. Ключевые метрики»Memory:
| Метрика | Что показывает |
|---|---|
/memory/classes/total:bytes | Total memory (heap + stacks + runtime). |
/memory/classes/heap/objects:bytes | Live heap objects. |
/memory/classes/heap/free:bytes | Free (allocated but unused) heap. |
/memory/classes/heap/released:bytes | Released to OS (MADV_DONTNEED). |
/memory/classes/heap/unused:bytes | Reserved but never touched. |
/memory/classes/heap/stacks:bytes | Memory used for goroutine stacks. |
/memory/classes/os-stacks:bytes | OS thread stacks. |
GC:
| Метрика | Что показывает |
|---|---|
/gc/heap/allocs:bytes (cumulative) | Total bytes allocated (lifetime). |
/gc/heap/frees:bytes | Total bytes freed. |
/gc/heap/goal:bytes | Current heap target (GC goal). |
/gc/pauses:seconds (histogram) | STW pause distribution. |
/gc/cycles/automatic:gc-cycles | Number of automatic GC cycles. |
/gc/cycles/forced:gc-cycles | Forced GC (runtime.GC()). |
/gc/cpu/percentage:float64-percent-of-cpu | % CPU spent in GC (since 1.22). |
Scheduler:
| Метрика | Что показывает |
|---|---|
/sched/goroutines:goroutines | Current 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):
/memory/classes/total:bytes— общая память./sched/goroutines:goroutines— рост → leak./gc/cpu/percentage:float64-percent-of-cpu— GC overhead./gc/pauses:secondsp99 → STW health./sched/latencies:secondsp99 → schedule latency (CPU contention)./sync/mutex/wait/total:seconds(rate) → contention./memory/classes/heap/objects:bytes→ live heap./memory/classes/heap/released:bytes→ возврат памяти OS./gc/heap/goal:bytes→ GC target (для оценки headroom до GOMEMLIMIT)./gc/cycles/automatic:gc-cycles(rate) — частота GC.
2.11. debug package — SetGCPercent, SetMemoryLimit, FreeOSMemory
Заголовок раздела «2.11. debug package — SetGCPercent, SetMemoryLimit, FreeOSMemory»Пакет runtime/debug — runtime tuning API.
debug.SetGCPercent(percent int) int — установить GOGC в runtime. Возвращает старое значение.
old := debug.SetGCPercent(50) // более частый GCdefer debug.SetGCPercent(old)-1 — отключить GC (как GOGC=off).
debug.SetMemoryLimit(limit int64) int64 — установить GOMEMLIMIT в runtime.
debug.SetMemoryLimit(4 << 30) // 4 GiBdebug.SetMemoryLimit(-1) // disable limitUse 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.
3. Подводные камни
Заголовок раздела «3. Подводные камни»-
PGO профиль из другого arch — не работает. Профиль AMD64 непригоден для ARM64 build.
-
PGO профиль слишком старый (изменился >50% кода) — может ухудшить perf. Освежайте.
-
default.pgoдолжен лежать в main-пакете (не в корне репо). Если main вcmd/server/, файл должен бытьcmd/server/default.pgo. -
-pgo=auto(default с 1.21) только еслиdefault.pgoсуществует. Если нет — silent skip. Лучше явно-pgo=path/to/file.pprof. -
GOMEMLIMIT не учитывает cgo память. Если ваш код через cgo держит 2GB — это сверх GOMEMLIMIT.
-
GOMEMLIMIT слишком близко к pod limit → OOMKilled при peak. Запас 5-10%.
-
GOGC=offбез GOMEMLIMIT — программа никогда не делает GC → out of memory. -
debug.SetMemoryLimit(-1)отключает, но не возвращает default. Default —math.MaxInt64. -
debug.FreeOSMemory()STW (короткий, но всё же). Не вызывайте в hot path. -
runtime/metricshistogram значения — это count в bucket, не absolute values. Нужно вычислять p50/p99 самим. -
runtime/metricsметрика:bytes— total, а:gc-cycles— count. Внимательнее с unit’ами. -
/gc/cycles/automatic:gc-cycles— counter, не gauge. Нужно вычислять rate в Prometheus. -
Profile с пустыми samples (если приложение idle) бесполезен для PGO. Собирайте под нагрузкой.
-
PGO замедляет компиляцию (в 1.5-2x). Кэшируйте
default.pgoв build cache. -
MADV_DONTNEED(Linux) vsMADV_FREE— Go использует DONTNEED по умолчанию. На macOSmadvise(MADV_FREE)(page может быть переиспользована, но кажется что выделена). RSS на macOS показывает «inflated» values — это false alarm.
4. Реальные production-кейсы
Заголовок раздела «4. Реальные production-кейсы»4.1. Cloudflare: PGO на 1.20 → 1.21 миграция
Заголовок раздела «4.1. Cloudflare: PGO на 1.20 → 1.21 миграция»- 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%).
4.2. Uber: 30+ микросервисов
Заголовок раздела «4.2. Uber: 30+ микросервисов»- 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) не получили улучшения. Профиль не репрезентативен.
4.3. Авито: GOMEMLIMIT для k8s
Заголовок раздела «4.3. Авито: GOMEMLIMIT для k8s»- Сервис каталога: 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 — выдерживает.
4.4. Тинькофф: переход с ballast на GOMEMLIMIT
Заголовок раздела «4.4. Тинькофф: переход с ballast на GOMEMLIMIT»- В 2020 году использовали ballast 8GB на сервисах с 16GB memory.
- При обновлении на 1.19 удалили ballast, поставили GOMEMLIMIT=14GiB.
- Поведение идентично (GC реже), но без визуальной «утечки» в монитринге.
4.5. Яндекс: runtime/metrics в Prometheus
Заголовок раздела «4.5. Яндекс: runtime/metrics в Prometheus»- Раньше использовали
runtime.MemStatspolling раз в 10 сек → STW добавляла jitter. - Перешли на
runtime/metricsчерез свой exporter. - p99 latency снизилась на 200µs (за счёт убирания STW при polling).
- Полный набор метрик: 30+ показателей, дашборд для каждого сервиса.
4.6. Сбер: differential PGO для регрессии
Заголовок раздела «4.6. Сбер: differential PGO для регрессии»- Перед каждым релизом собирали профиль на baseline + новой версии.
- Сравнивали через
go tool pprof -base. - Поймали регрессию: новая фича добавила interface call в hot path → -3% throughput. Пересмотрели до prod.
4.7. Open source: bbolt (etcd-io)
Заголовок раздела «4.7. Open source: bbolt (etcd-io)»- Применили PGO к etcd v3.5+.
- Внутренние benchmarks: +4-5% Put/Get throughput.
- На больших кластерах: latency p99 снизилась на 15%.
4.8. Кейс: «PGO замедлил программу»
Заголовок раздела «4.8. Кейс: «PGO замедлил программу»»- Команда внедрила 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 (экспертный уровень)»-
Что такое PGO в Go? С какой версии GA?
-
Опишите PGO workflow от collect profile до build.
-
Где должен лежать
default.pgoфайл? -
Что такое
-pgo=auto,-pgo=path,-pgo=off? -
Какие 4 типа оптимизаций улучшаются с PGO?
-
Speculative devirtualization — что это? Пример.
-
Как меняется inline budget с PGO?
-
PGO profile stability в 1.22 — что было до и что стало?
-
Как агрегировать PGO профили из N pods? Какая команда?
-
Differential profiling — для чего? Команда?
-
Как часто обновлять PGO профили? Best practice.
-
Что такое GOMEMLIMIT? С какой версии?
-
Что входит в GOMEMLIMIT (что limit’ится)?
-
Что НЕ входит (что не учитывается)?
-
GOMEMLIMIT vs GOGC — как работают вместе?
-
GOGC=off+ GOMEMLIMIT — когда такая конфигурация полезна? -
Best practice для k8s: какое значение GOMEMLIMIT относительно pod limit?
-
Memory ballast pattern — что это? Почему deprecated?
-
Twitch’s ballast в 2019 — расскажите историю.
-
runtime.MemStatsvsruntime/metrics— отличия. -
Почему
MemStatsимеет STW? -
Назовите 5 ключевых метрик из
runtime/metricsдля мониторинга. -
/gc/pauses:seconds— какой тип? Как считать p99? -
/sched/latencies:seconds— что показывает? -
/sched/goroutines:goroutines— рост говорит о чём? -
/sync/mutex/wait/total:seconds— counter или gauge? Как использовать? -
debug.SetGCPercentvsGOGCenv — разница в применении. -
debug.SetMemoryLimituse cases. -
debug.FreeOSMemory()— что делает? Когда использовать? -
debug.SetTraceback— какие уровни? Когда какой? -
Почему
runtime.GC()STW в Go 1.5+ короткий (μs)? -
Когда
MADV_DONTNEEDvsMADV_FREE(macOS)? -
RSS на macOS показывает inflated values — почему?
-
Container-aware GOMAXPROCS (Go 1.25) — что это? Откуда читает?
-
Что нового в
runtime/metricsв Go 1.23/1.24?
6. Practice
Заголовок раздела «6. Practice»-
PGO baseline experiment. Возьмите CPU-bound бенчмарк (например, hashing 100MB). Соберите без PGO, измерьте. Соберите профиль во время этого же бенчмарка. Положите как
default.pgo. Пересоберите. Измерьте Δ. -
PGO devirtualization observation. Напишите interface с одной концретной реализацией. Соберите без PGO, посмотрите
go tool objdump— будет interface call. С PGO — direct call (после speculative devirt). -
GOMEMLIMIT в k8s. Симулируйте: запустите программу, аллоцирующую 3GB heap, под
cgexec -g memory:test 4GB. Без GOMEMLIMIT — OOM. С GOMEMLIMIT=3600MiB — выживает (с замедлением). -
runtime/metrics exporter. Напишите Prometheus exporter, который читает 10 ключевых метрик из runtime/metrics и эспортирует на /metrics. Сравните с поведением
prometheus/client_golang(он уже использует runtime/metrics с недавнего времени). -
debug.FreeOSMemory test. Аллоцируйте 1GB, освободите (set nil). Через
topпосмотрите RSS — он останется высоким. Вызовитеdebug.FreeOSMemory(). Проверьте, что RSS упал. -
GC pressure detection. Напишите программу с интенсивной аллокацией (worker pool на 1000 G, каждая аллоцирует 10MB/sec). Замерьте
/gc/cpu/percentage:float64-percent-of-cpu. Если >25% — диагноз: уменьшить allocation rate или увеличить GOGC. -
GOMEMLIMIT dynamic tuning. Напишите daemon, который раз в 30 сек смотрит pod limit (через
/sys/fs/cgroup/memory.max) и обновляет GOMEMLIMIT черезdebug.SetMemoryLimit. (С Go 1.25 это не нужно — runtime сама делает.) -
Differential PGO. Возьмите свой сервис. Соберите профиль баseline. Внесите изменение (рефакторинг). Соберите профиль после. Сравните через
go tool pprof -base before.pgo after.pgo. Найдите функции, которые стали hotter.
7. Источники
Заголовок раздела «7. Источники»- «Profile-Guided Optimization in Go 1.21» — Michael Pratt, Go Blog (Sept 2023).
- «PGO Stability in Go 1.22» — Michael Pratt, Go Blog (Feb 2024).
- «GOMEMLIMIT: Soft Memory Limit» — Michael Knyszek, Go Blog (Aug 2022).
- «Twitch’s memory ballast» — Ross Engers, blog.twitch.tv (2019, исторический).
- «Mind the (Memory) Gap: Tuning GOMEMLIMIT» — Cloudflare engineering blog (2023).
runtime/metricspackage docs — pkg.go.dev/runtime/metrics.- «GOGC, GOMEMLIMIT, and Tuning Go GC» — Tigran Bayburtsyan, medium.
- «PGO at Uber Scale» — Uber engineering blog (2023-2024).
- «Continuous Profiling at Datadog» — Felix Geisendörfer, GopherCon EU 2023.
- Go runtime sources —
src/runtime/metrics.go,mgcpacer.go,mgcscavenge.go. - «Go’s runtime debugging» — Cherry Mui, GopherCon 2024.
- «Sub-millisecond GC» — Austin Clements, ISMM 2018 (хотя старая, основные ideas релевантны).
- Pyroscope continuous profiling docs — pyroscope.io.
- «Container-aware GOMAXPROCS in Go 1.25» — Go 1.25 release notes + design doc.
- «How to monitor Go services» — серия статей на datadoghq.com.