Production Profiling в Go
Зачем знать: Production-системы сталкиваются с проблемами, которые невозможно воспроизвести локально: memory regression после релиза, tail latency p99, CPU spikes, goroutine leaks. Profiling в production — это не роскошь, а необходимость для Middle 2+ инженера. Continuous profiling позволяет видеть деградацию в реальном времени, а не получать ticket “сервис тормозит”. Знание pprof, flame graphs, trace tool и eBPF-based профайлеров отличает senior engineer от middle.
Содержание
Заголовок раздела «Содержание»- Базовая концепция (повторение)
- Production-практики
- Gotchas (10+)
- Реальные кейсы
- Вопросы (30)
- Practice (5-8)
- Источники
1. Базовая концепция (повторение)
Заголовок раздела «1. Базовая концепция (повторение)»Что такое pprof
Заголовок раздела «Что такое pprof»pprof — это инструмент профилирования из Google, встроенный в Go стандартную библиотеку. Он использует sample-based profiling: периодически снимает стек-трейсы и агрегирует их по времени/объёму.
Виды профилей в Go:
| Профиль | Что измеряет | Когда использовать |
|---|---|---|
cpu | CPU time per function | Высокая загрузка CPU, hot loops |
heap | Memory allocations (live + cumulative) | Memory regressions, OOM |
goroutine | Все живые goroutines + стеки | Goroutine leaks, deadlocks |
block | Время блокировки на synchronization | Lock contention, channels |
mutex | Contention на mutex | Lock contention, sync.Mutex hotspots |
threadcreate | OS thread creations | Threading anomalies |
allocs | Cumulative allocations с момента старта | Total allocation rate |
Базовое использование
Заголовок раздела «Базовое использование»import ( _ "net/http/pprof" "net/http")
func main() { go func() { // Профайлер endpoint http.ListenAndServe("localhost:6060", nil) }() // ... основной код}После запуска доступны endpoints:
http://localhost:6060/debug/pprof/— indexhttp://localhost:6060/debug/pprof/heap— snapshot heaphttp://localhost:6060/debug/pprof/profile?seconds=30— CPU profile 30 секундhttp://localhost:6060/debug/pprof/goroutine— goroutine dumphttp://localhost:6060/debug/pprof/trace?seconds=10— execution trace
Анализ через pprof CLI
Заголовок раздела «Анализ через pprof CLI»# Скачать и открыть CPU профильgo tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Heap snapshotgo tool pprof http://localhost:6060/debug/pprof/heap
# Web visualizationgo tool pprof -http=:8080 cpu.pprofВнутри pprof CLI:
top— top функции по costtop -cum— cumulative cost (включая callees)list FuncName— source-level annotationweb— открыть call graph в браузереpeek FuncName— посмотреть callers и calleestraces— все собранные стек-трейсы
2. Production-практики
Заголовок раздела «2. Production-практики»2.1. pprof в production: что включать
Заголовок раздела «2.1. pprof в production: что включать»Главное правило: profiling endpoints не должны быть public.
Что включать:
Заголовок раздела «Что включать:»✅ heap profile — overhead ~1%, безопасно держать включённым всегда. ✅ goroutine profile — практически бесплатно, очень полезно для leak detection. ✅ CPU profile on-demand — overhead ~5% при активном profiling, иначе 0. ✅ allocs profile — то же что heap, но cumulative.
Что включать с осторожностью:
Заголовок раздела «Что включать с осторожностью:»⚠️ block profile — требует runtime.SetBlockProfileRate(N). При rate=1 overhead значителен. Использовать только при investigation.
⚠️ mutex profile — требует runtime.SetMutexProfileFraction(N). При rate=1 высокий overhead.
// Только для активного исследования contention:runtime.SetBlockProfileRate(1000) // 1 sample per 1000ns blockedruntime.SetMutexProfileFraction(100) // 1 из 100 contention events2.2. Безопасный admin port
Заголовок раздела «2.2. Безопасный admin port»Анти-паттерн:
http.ListenAndServe(":8080", nil) // pprof доступен ВСЕМProduction-паттерн:
// Public API на одном портуgo func() { publicMux := http.NewServeMux() publicMux.HandleFunc("/api/", apiHandler) http.ListenAndServe(":8080", publicMux)}()
// Admin (pprof, metrics, health) на другомgo func() { adminMux := http.NewServeMux() adminMux.HandleFunc("/debug/pprof/", pprof.Index) adminMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) adminMux.HandleFunc("/debug/pprof/profile", pprof.Profile) adminMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) adminMux.HandleFunc("/debug/pprof/trace", pprof.Trace) adminMux.Handle("/metrics", promhttp.Handler())
// Bind на localhost или internal interface http.ListenAndServe("127.0.0.1:6060", adminMux)}()В Kubernetes admin port не expose-ить через Service, а только kubectl port-forward:
kubectl port-forward pod/myapp-xyz 6060:6060go tool pprof -http=:8080 http://localhost:6060/debug/pprof/heap2.3. Authentication
Заголовок раздела «2.3. Authentication»Если admin port всё же доступен по сети (например, internal network), добавить auth:
func basicAuth(handler http.Handler, user, pass string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { u, p, ok := r.BasicAuth() if !ok || u != user || subtle.ConstantTimeCompare([]byte(p), []byte(pass)) != 1 { w.Header().Set("WWW-Authenticate", `Basic realm="admin"`) http.Error(w, "Unauthorized", http.StatusUnauthorized) return } handler.ServeHTTP(w, r) })}
adminServer := &http.Server{ Addr: ":6060", Handler: basicAuth(adminMux, os.Getenv("ADMIN_USER"), os.Getenv("ADMIN_PASS")),}Для serious enterprise — mTLS:
tlsConfig := &tls.Config{ ClientCAs: caPool, ClientAuth: tls.RequireAndVerifyClientCert,}adminServer.TLSConfig = tlsConfigadminServer.ListenAndServeTLS(certFile, keyFile)2.4. Continuous Profiling
Заголовок раздела «2.4. Continuous Profiling»Континуальное профилирование — это сбор профилей непрерывно (каждые 10-30 секунд) и хранение их в time-series базе. Это позволяет:
- Сравнивать профили “до релиза” и “после”
- Находить regression автоматически
- Видеть тренды (memory growth, CPU drift)
Pyroscope (Grafana)
Заголовок раздела «Pyroscope (Grafana)»Open source, теперь часть Grafana Labs.
import "github.com/grafana/pyroscope-go"
func main() { pyroscope.Start(pyroscope.Config{ ApplicationName: "myapp.production", ServerAddress: "http://pyroscope:4040", Logger: pyroscope.StandardLogger, ProfileTypes: []pyroscope.ProfileType{ pyroscope.ProfileCPU, pyroscope.ProfileAllocObjects, pyroscope.ProfileAllocSpace, pyroscope.ProfileInuseObjects, pyroscope.ProfileInuseSpace, }, Tags: map[string]string{ "region": os.Getenv("REGION"), "version": os.Getenv("VERSION"), }, }) // ... основной код}Pyroscope UI показывает:
- Flame graph по времени
- Diff между двумя временными окнами
- Sandwich view (focus on function)
Parca (open source)
Заголовок раздела «Parca (open source)»Альтернатива Pyroscope, использует eBPF для system-wide profiling (включая Go).
- Storage в S3-compatible
- Поддержка system-wide profiling (не только Go)
- parca-agent работает через eBPF без модификации Go binary
Datadog Continuous Profiler
Заголовок раздела «Datadog Continuous Profiler»Коммерческий, интегрирован с APM. Импорт:
import "gopkg.in/DataDog/dd-trace-go.v1/profiler"
profiler.Start( profiler.WithService("myapp"), profiler.WithEnv("prod"), profiler.WithVersion("v1.2.3"), profiler.WithProfileTypes( profiler.CPUProfile, profiler.HeapProfile, profiler.GoroutineProfile, profiler.MutexProfile, profiler.BlockProfile, ),)Polar Signals
Заголовок раздела «Polar Signals»Continuous profiling SaaS от создателей Parca. eBPF-based.
Sampling и retention
Заголовок раздела «Sampling и retention»Типичные настройки:
- Sampling rate: 10 секунд CPU profile каждую минуту (или непрерывно)
- Retention: 7-30 дней
- Cost: профиль ~1MB сжатый, на 1000 хостов × 10K профилей/день = ~10TB/месяц
2.5. Flame graphs
Заголовок раздела «2.5. Flame graphs»Flame graph придумал Brendan Gregg (Netflix). Это визуализация sample-based profiling:
- X-axis (горизонталь): ширина = доля времени (НЕ время по порядку!)
- Y-axis (вертикаль): глубина call stack (вниз = caller, вверх = callee)
- Цвет: обычно произвольный, для группировки
Reading rules:
- Самые широкие функции внизу = top-level entry points
- Самые широкие функции наверху = “hot leaves” — где реально тратится время
- Pyramid shape (узкая вершина) = функция вызывает много других
- Plateau (плоская верхушка) = функция сама делает работу (hot leaf)
Differential flame graphs
Заголовок раздела «Differential flame graphs»Сравнение двух профилей:
- Красный (+): медленнее в новой версии
- Синий (-): быстрее в новой версии
# pprof diffgo tool pprof -base=old.pprof new.pprof(pprof) webИнструменты
Заголовок раздела «Инструменты»- speedscope.app — отличный online viewer, можно загрузить .pprof
- flamegraph.com — Brendan Gregg’s
- Pyroscope / Parca / Grafana — встроены
go tool pprof -http=:8080— встроенный viewer
2.6. pprof CLI deep dive
Заголовок раздела «2.6. pprof CLI deep dive»Comparing snapshots
Заголовок раздела «Comparing snapshots»# Сделать snapshot до измененийcurl -o before.pprof http://localhost:6060/debug/pprof/heap
# ... выкат новой версии, дать прогреться
# Сделать послеcurl -o after.pprof http://localhost:6060/debug/pprof/heap
# Сравнитьgo tool pprof -base=before.pprof after.pprof(pprof) top(pprof) list NewFunction-base показывает delta между профилями, что критично для regression hunting.
Source-level annotation
Заголовок раздела «Source-level annotation»(pprof) list myPackage.MyFuncTotal: 30sROUTINE ======================== myPackage.MyFunc 2.5s 5.0s (flat, cum) 16.66% of Total . . 42:func MyFunc(items []Item) { . . 43: for _, item := range items { 500ms 1.0s 44: if expensiveCheck(item) { <-- 1s here . . 45: ... 2.0s 4.0s 46: result = process(item) <-- 4s here . . 47: ...flat = время именно в этой строке.
cum = cumulative с callees.
Peek для callers/callees
Заголовок раздела «Peek для callers/callees»(pprof) peek myFunc 5.00s 100% | caller1 2.00s 40% | caller2 ... myPackage.myFunc 3.00s 60% | childFunc1 1.00s 20% | childFunc2Показывает, кто вызывает функцию и кого она вызывает (с весами).
2.7. Heap profile интерпретация
Заголовок раздела «2.7. Heap profile интерпретация»Heap profile имеет 4 представления:
| Sample type | Описание |
|---|---|
inuse_space | Память, занятая live объектами (NOT GC’d) — default |
inuse_objects | Количество live объектов |
alloc_space | Общая память аллоцированная (включая уже GC’d) |
alloc_objects | Общее количество аллокаций |
go tool pprof -inuse_space heap.pprof # текущее использованиеgo tool pprof -alloc_space heap.pprof # cumulative allocationsgo tool pprof -alloc_objects heap.pprof # для allocation rateИнтерпретация:
inuse_spacehigh → memory leak или большой working setalloc_space>>inuse_space→ много аллокаций которые GC собирает (GC pressure!)alloc_objectshigh → много мелких аллокаций (GC pressure)
2.8. Goroutine leak detection
Заголовок раздела «2.8. Goroutine leak detection»В runtime
Заголовок раздела «В runtime»# Snapshot всех goroutinescurl -o goroutines.pprof http://localhost:6060/debug/pprof/goroutine?debug=2debug=2 даёт human-readable stacks вместо .pprof формата.
Признаки leak
Заголовок раздела «Признаки leak»В goroutine profile:
- 1000+ goroutines с одинаковым стеком = leak
- Goroutines заблокированы на
chan receive,select,sync.Mutex.Lock— кандидаты - Goroutines stuck на
net.Read— потенциально hanging connection
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/goroutineВ web UI можно увидеть call graph с количеством goroutines на каждом узле.
uber-go/goleak для тестов
Заголовок раздела «uber-go/goleak для тестов»import "go.uber.org/goleak"
func TestMain(m *testing.M) { goleak.VerifyTestMain(m)}
// или per-testfunc TestSomething(t *testing.T) { defer goleak.VerifyNone(t) // ... test logic}После теста goleak проверяет, что не осталось “лишних” goroutines.
2.9. Block и mutex profile
Заголовок раздела «2.9. Block и mutex profile»Block profile
Заголовок раздела «Block profile»Показывает где goroutines блокируются (channels, sync.Cond, time.Sleep, etc.).
runtime.SetBlockProfileRate(1) // record EVERY blocking event// илиruntime.SetBlockProfileRate(1000000) // 1 sample per 1ms blocked time (cheaper)⚠️ Gotcha: Параметр — это rate in nanoseconds. 1 = всё, что блокируется ≥1ns, 0 = выключено.
curl -o block.pprof http://localhost:6060/debug/pprof/blockgo tool pprof block.pprofMutex profile
Заголовок раздела «Mutex profile»runtime.SetMutexProfileFraction(100) // 1 из 100 contention eventsПараметр — fraction (не rate). 100 = 1%, 1 = все события.
Когда использовать:
- Подозрение на lock contention (рост latency без CPU)
- Высокий
goroutines blockedв metrics - Tail latency p99 в разы выше p50
⚠️ НЕ держать постоянно включёнными в production — overhead значительный.
2.10. eBPF для Go
Заголовок раздела «2.10. eBPF для Go»Что такое eBPF
Заголовок раздела «Что такое eBPF»eBPF (extended Berkeley Packet Filter) — это технология Linux ядра для запуска sandboxed программ внутри kernel. Используется для:
- Network monitoring (Cilium)
- Security (Falco)
- Profiling (Parca, Pixie, Polar Signals)
Преимущества для profiling:
- Не требует модификации Go binary
- Низкий overhead
- System-wide (видит и kernel, и user-space)
Open source observability platform (CNCF), купленный New Relic.
- Auto-instrumentation через eBPF
- Goroutine tracking
- HTTP/gRPC tracing без кода
- Latency analysis per endpoint
Установка в k8s:
px deployParca-agent
Заголовок раздела «Parca-agent»eBPF-based system-wide profiler. Запускается как DaemonSet в k8s, профилирует ВСЕ процессы на ноде.
# k8s DaemonSetkind: DaemonSetmetadata: name: parca-agentspec: template: spec: containers: - name: parca-agent image: ghcr.io/parca-dev/parca-agent:latest securityContext: privileged: true # для eBPFAsync stack unwinding
Заголовок раздела «Async stack unwinding»Главная проблема eBPF profiling для Go — stack unwinding. Go использует виртуальные стеки goroutines, и без debug info kernel не может пройти стек.
Решение:
- Frame pointers (Go 1.20+): включены по умолчанию на Linux amd64. Это даёт fast stack unwinding.
- DWARF unwinding: медленнее, но работает везде. Парсит
.eh_frameсекцию ELF.
Проверить frame pointers в binary:
go build -gcflags="-l" -ldflags="-X main.X=1" myapp.go # default with FPreadelf -S myapp | grep eh_frame # должен бытьBCC scripts для Go
Заголовок раздела «BCC scripts для Go»BCC (BPF Compiler Collection) — Python frontend для eBPF.
# bcc script для подсчёта goroutinesfrom bcc import BPF
bpf_text = """int trace_newproc(struct pt_regs *ctx) { bpf_trace_printk("New goroutine created!\\n"); return 0;}"""
b = BPF(text=bpf_text)b.attach_uprobe(name="myapp", sym="runtime.newproc", fn_name="trace_newproc")b.trace_print()2.11. go tool trace deep
Заголовок раздела «2.11. go tool trace deep»go tool trace показывает execution trace — точную хронологию событий runtime.
import "runtime/trace"
func main() { f, _ := os.Create("trace.out") defer f.Close() trace.Start(f) defer trace.Stop() // ... код}Или через HTTP:
curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5go tool trace trace.outОткрывается в браузере, доступны views:
- Goroutine analysis — все goroutines с их state changes
- GC events — когда был GC, сколько занял
- Network blocking profile — где блокировались на сети
- Sync blocking profile — блокировки на mutex/channels
- Scheduler latency profile — задержки шедулера
- User-defined regions — кастомные annotations
User regions
Заголовок раздела «User regions»import "runtime/trace"
ctx, task := trace.NewTask(ctx, "handleRequest")defer task.End()
trace.WithRegion(ctx, "parseInput", func() { parseInput(r)})
trace.WithRegion(ctx, "queryDB", func() { db.Query(...)})В trace viewer видны эти регионы — можно понять, какая часть запроса тормозит.
Flight Recorder (Go 1.23+)
Заголовок раздела «Flight Recorder (Go 1.23+)»import "runtime/trace"
fr := trace.NewFlightRecorder()fr.SetPeriod(10 * time.Second) // храним последние 10s событийfr.Start()defer fr.Stop()
// При event (например, slow query):if duration > threshold { f, _ := os.Create("incident.trace") fr.WriteTo(f) f.Close()}Flight Recorder хранит ring buffer trace-событий. Когда происходит инцидент — дампит последние N секунд. Идеально для редких anomaly.
2.12. Production debugging без restart
Заголовок раздела «2.12. Production debugging без restart»Delve attach
Заголовок раздела «Delve attach»# Attach к live процессуdlv attach $(pidof myapp)
(dlv) goroutines(dlv) goroutine 42(dlv) stack(dlv) locals⚠️ Delve останавливает процесс во время debug. Не использовать на live traffic без warning.
runtime.Stack
Заголовок раздела «runtime.Stack»// На admin endpointhttp.HandleFunc("/admin/stack", func(w http.ResponseWriter, r *http.Request) { buf := make([]byte, 1<<20) n := runtime.Stack(buf, true) // true = all goroutines w.Write(buf[:n])})Эквивалент /debug/pprof/goroutine?debug=2, но можно встроить кастомно.
Combined approach
Заголовок раздела «Combined approach»При инциденте:
curl /debug/pprof/heap > heap.pprof— текущая памятьcurl /debug/pprof/goroutine?debug=2 > goroutines.txt— все стекиcurl /debug/pprof/profile?seconds=30 > cpu.pprof— что грузит CPUcurl /debug/pprof/trace?seconds=5 > trace.out— runtime events
Анализировать локально:
go tool pprof -http=:8080 heap.pprofgo tool trace trace.out3. Gotchas (10+)
Заголовок раздела «3. Gotchas (10+)»3.1. ⚠️ pprof endpoint доступен публично
Заголовок раздела «3.1. ⚠️ pprof endpoint доступен публично»Самая частая ошибка. import _ "net/http/pprof" регистрирует handlers в http.DefaultServeMux. Если потом http.ListenAndServe(":8080", nil) — pprof доступен на public порту.
Исправление: всегда использовать explicit ServeMux.
3.2. ⚠️ CPU profile прерывает только goroutines в Go-коде
Заголовок раздела «3.2. ⚠️ CPU profile прерывает только goroutines в Go-коде»CPU profiling в Go использует SIGPROF который собирает sample у goroutine в момент сигнала. Но если goroutine в syscall (например, blocked на read из сети) — она не получает SIGPROF корректно. Поэтому network-heavy код может выглядеть “холодным” в CPU profile.
Использовать block profile или trace для blocking-bound кода.
3.3. ⚠️ Heap profile показывает ALLOCATIONS, не usage
Заголовок раздела «3.3. ⚠️ Heap profile показывает ALLOCATIONS, не usage»alloc_space — это total allocated since start. Если 1GB аллоцировалось, но GC всё собрал — inuse_space будет low, но alloc_space high. Это GC pressure, не leak.
3.4. ⚠️ Heap profile sampled, не точный
Заголовок раздела «3.4. ⚠️ Heap profile sampled, не точный»По умолчанию runtime.MemProfileRate = 512*1024 (1 sample per 512KB). Маленькие аллокации (по 64 байта) могут не попасть в профиль.
Понизить для большей точности (ценой overhead):
runtime.MemProfileRate = 1 // record ALL (DANGEROUS!)runtime.MemProfileRate = 4096 // 1 per 4KB (умеренно)3.5. ⚠️ Goroutine profile показывает только активные
Заголовок раздела «3.5. ⚠️ Goroutine profile показывает только активные»В goroutine профиле нет завершённых goroutines. Если leak уже произошёл и goroutine упала — profile её не покажет.
Для исторического анализа использовать metrics:
go func() { for range time.Tick(10 * time.Second) { count := runtime.NumGoroutine() metrics.Gauge("goroutines").Set(float64(count)) }}()3.6. ⚠️ Block profile дорого включать с rate=1
Заголовок раздела «3.6. ⚠️ Block profile дорого включать с rate=1»SetBlockProfileRate(1) записывает КАЖДОЕ событие блокировки. На busy server — это миллионы событий в секунду, overhead 20%+.
Использовать sampling: SetBlockProfileRate(10000) = 1 sample per 10μs.
3.7. ⚠️ Mutex profile fraction, не rate
Заголовок раздела «3.7. ⚠️ Mutex profile fraction, не rate»SetMutexProfileFraction(N) — это fraction, не rate. 1 = записывать ВСЕ contention events. 100 = 1%. Часто путают и пишут 1 думая “выключено”.
runtime.SetMutexProfileFraction(0) // выключеноruntime.SetMutexProfileFraction(100) // 1% событийruntime.SetMutexProfileFraction(1) // 100% событий (НЕ "выключено"!)3.8. ⚠️ pprof не учитывает off-heap память
Заголовок раздела «3.8. ⚠️ pprof не учитывает off-heap память»mmap, cgo allocations, syscall buffers — НЕ в heap profile. Если RSS процесса растёт, а heap не растёт — смотреть:
- cgo
- mmap
- thread stacks
- runtime overhead
Использовать runtime/metrics:
samples := []metrics.Sample{ {Name: "/memory/classes/total:bytes"}, {Name: "/memory/classes/heap/objects:bytes"}, {Name: "/memory/classes/heap/unused:bytes"}, {Name: "/memory/classes/os-stacks:bytes"},}metrics.Read(samples)3.9. ⚠️ pprof CLI и Go versions
Заголовок раздела «3.9. ⚠️ pprof CLI и Go versions».pprof файлы хранят symbol info. Если профилировать binary версии X, а анализировать go tool pprof версии Y — symbols могут не совпадать. Анализировать тем же go version, что и собран binary.
3.10. ⚠️ Pyroscope/Parca добавляют свой overhead
Заголовок раздела «3.10. ⚠️ Pyroscope/Parca добавляют свой overhead»Continuous profiling сам по себе профилируется в результатах. Обычно ~2-3% CPU. На critical-path сервисах можно увеличить sampling interval (30s вместо 10s).
3.11. ⚠️ Trace files огромные
Заголовок раздела «3.11. ⚠️ Trace files огромные»runtime/trace пишет ВСЁ. На busy server 1 секунда trace = 100MB+. Поэтому trace используется только короткими интервалами (1-10 секунд).
3.12. ⚠️ Flame graph не показывает порядок выполнения
Заголовок раздела «3.12. ⚠️ Flame graph не показывает порядок выполнения»X-axis — это proportion of time, не временная последовательность. Функции, которые выполнялись последовательно в разных стеках, могут стоять рядом или далеко в зависимости от sorting.
Для chronology — использовать go tool trace.
3.13. ⚠️ pprof “missing samples” на goroutines с короткой жизнью
Заголовок раздела «3.13. ⚠️ pprof “missing samples” на goroutines с короткой жизнью»Если goroutine живёт <10ms (рough sample interval 10ms для CPU), она может вообще не попасть в CPU profile.
3.14. ⚠️ Inlining скрывает функции из профиля
Заголовок раздела «3.14. ⚠️ Inlining скрывает функции из профиля»Компилятор inlines функции, и они могут не появиться в стеке профиля. Disable inlining для debug:
go build -gcflags="-l" myapp.go(Но это не для production!)
4. Реальные кейсы
Заголовок раздела «4. Реальные кейсы»4.1. Cloudflare: memory regression debug
Заголовок раздела «4.1. Cloudflare: memory regression debug»Cloudflare писали в блоге, как они отлавливают memory regression после релиза:
- До деплоя: snapshot heap profile
- После деплоя (30 мин прогрев): snapshot
go tool pprof -base=before.pprof after.pprof→top- Видят: новая функция
parseHeaderV2аллоцирует 300MB больше list parseHeaderV2→ находят[]byte(s)в hot path → fix черезunsafe.Slice
Lesson: continuous profiling с baseline снимает 80% memory regression в первые часы после релиза.
4.2. Discord: tail latency p99
Заголовок раздела «4.2. Discord: tail latency p99»Discord (на Go был один из первых сервисов, потом переписали на Rust) находил p99 latency spikes:
- Просто метрики показывают p99 = 200ms при p50 = 5ms
- CPU profile — ничего не показывает (CPU usage 30%)
- Block profile показывает: 80% времени p99 запросов — в
sync.RWMutex.Lock - Issue: один writer блокирует тысячи readers
- Решение: sharded map (consistent hashing)
Lesson: для latency outliers — block/mutex profile, не CPU.
4.3. Uber: CPU spike в hot path
Заголовок раздела «4.3. Uber: CPU spike в hot path»Uber engineering blog описывал случай:
- Релиз — CPU usage скачет с 40% до 90%
- CPU profile показывает: 50% времени в
runtime.mapaccess1_fast64 list myFunc→ видят горячий цикл сm[k]access- Issue: использовали
map[int64]struct{...}для большого set, GC pressure + cache misses - Решение:
roaring.Bitmapдля integer sets
Lesson: runtime.* функции в top профиля → копать в сторону маp/slice/GC overhead.
4.4. Twitch: goroutine leak in chat
Заголовок раздела «4.4. Twitch: goroutine leak in chat»Twitch писали о goroutine leak в чате:
- Алёрт:
goroutines > 100000(нормально 5000) goroutine?debug=2snapshot → 95K goroutines в одном стеке:chan receiveвsubscriptionHandler- Issue: при дисконнекте subscriber канал не закрывался, goroutine ждала вечно
- Решение: select с
<-ctx.Done()для exit на cancel
Lesson: регулярно мониторить runtime.NumGoroutine() как key metric.
4.5. Memory leak без alloc growth
Заголовок раздела «4.5. Memory leak без alloc growth»Реальный случай (не привязан к компании): app использовал ~2GB RSS стабильно, потом начал расти до 4GB за неделю.
- Heap profile
inuse_spaceстабильный 500MB - RSS растёт. Что?
runtime/metricsпоказывает/memory/classes/os-stacks:bytesрастёт- Issue: goroutine leak, stacks занимают 1.5GB
NumGoroutine()подтверждает: 500K goroutines
Lesson: RSS != heap. Смотреть полную картину памяти.
4.6. Flight Recorder в действии
Заголовок раздела «4.6. Flight Recorder в действии»С Go 1.23+ команда Anthropic (вымышленный пример, но реалистичный) использует Flight Recorder для p99.9:
- 99% запросов <10ms
- 0.1% запросов >1s — но воспроизвести нельзя
- Flight Recorder в background, при duration > 500ms → dump trace
- Анализ trace показывает: STW pause GC в момент медленных запросов
- Tuning: уменьшили
GOGCс 100 до 50, p99.9 упал на 60%
Lesson: Flight Recorder — для редких events, которые нельзя воспроизвести.
5. Вопросы (30)
Заголовок раздела «5. Вопросы (30)»- Какие виды pprof профилей существуют в Go и для чего каждый из них?
- В чём разница между
inuse_spaceиalloc_spaceв heap profile? - Какой overhead у CPU profiling в production?
- Почему нельзя выставлять public порт с
/debug/pprof/endpoints? - Как настроить отдельный admin port для pprof в production?
- Что такое
runtime.SetBlockProfileRateи какие значения параметра? - В чём отличие
SetMutexProfileFractionотSetBlockProfileRate? - Когда использовать
blockprofile, а когдаmutex? - Как сравнить два heap snapshot через pprof CLI?
- Что показывает differential flame graph?
- Как читать flame graph? Что значит ширина и высота?
- Что такое continuous profiling? Назовите 2-3 инструмента.
- Чем Pyroscope отличается от Parca?
- Что такое eBPF и зачем для Go profiling?
- Что такое frame pointers в Go и зачем они для eBPF?
- Как обнаружить goroutine leak?
- Что делает
uber-go/goleak? - Как анализировать
goroutine?debug=2output? - Что такое Flight Recorder в Go 1.23+ и для чего?
- В чём отличие
go tool traceот pprof? - Что такое user regions в trace и как их использовать?
- Какие view’ы доступны в
go tool trace? - Почему в CPU profile не видны функции, заблокированные на syscall?
- Как
runtime.MemProfileRateвлияет на heap profile? - Что такое off-heap память и как её измерить?
- Где смотреть OS stack memory если RSS растёт без alloc growth?
- Как использовать
runtime/metricsдля мониторинга памяти? - Опишите алгоритм debug memory regression после релиза.
- Опишите алгоритм debug tail latency p99.
- Безопасно ли использовать delve attach в production?
6. Practice (5-8)
Заголовок раздела «6. Practice (5-8)»6.1. Настроить production pprof endpoint
Заголовок раздела «6.1. Настроить production pprof endpoint»Создать HTTP сервер с:
- Public API на :8080
- Admin endpoint (pprof, /metrics, /health) на 127.0.0.1:6060
- Basic auth на admin endpoint
- Опционально mTLS
6.2. Continuous profiling setup
Заголовок раздела «6.2. Continuous profiling setup»Подключить Pyroscope (или Parca в docker-compose) и Go-приложение:
- Профилировать CPU и heap
- Запустить нагрузку (k6, vegeta)
- Сделать релиз с регрессией (добавить ненужную аллокацию)
- Увидеть в Pyroscope diff между “до” и “после”
6.3. Goroutine leak hunt
Заголовок раздела «6.3. Goroutine leak hunt»Написать приложение с искусственным goroutine leak (например, не закрывающийся channel). Найти через:
runtime.NumGoroutine()мониторингgoroutine?debug=2snapshotuber-go/goleakв тесте
6.4. Block profile investigation
Заголовок раздела «6.4. Block profile investigation»Сделать сервис с sync.Mutex contention (например, single mutex на global map). Включить block + mutex profile, найти hot spot, переделать на sync.Map или sharded map. Сравнить latency p50/p99 до и после.
6.5. Flight Recorder (Go 1.23+)
Заголовок раздела «6.5. Flight Recorder (Go 1.23+)»Реализовать handler с длинной операцией (~500ms) в 1 из 1000 запросов. Использовать Flight Recorder для capture trace при duration > 200ms. Проанализировать в go tool trace.
6.6. eBPF profiling с Parca
Заголовок раздела «6.6. eBPF profiling с Parca»Развернуть Parca-agent в minikube (или kind). Запустить Go-приложение с нагрузкой. Получить flame graph через Parca без модификации Go binary.
6.7. Heap diff analysis
Заголовок раздела «6.7. Heap diff analysis»Снять два heap snapshot (до/после операции). Через go tool pprof -base= найти топ-5 функций, увеличивших аллокации.
6.8. Trace user regions
Заголовок раздела «6.8. Trace user regions»Добавить trace.WithRegion в HTTP handler для каждой стадии: parse, db query, render. Запустить trace 5 секунд, проанализировать в go tool trace где основное время.
7. Источники
Заголовок раздела «7. Источники»- Go Diagnostics — официальная документация: https://go.dev/doc/diagnostics
- net/http/pprof package docs — https://pkg.go.dev/net/http/pprof
- Brendan Gregg, Flame Graphs — http://www.brendangregg.com/flamegraphs.html — оригинальная теория.
- Pyroscope documentation — https://grafana.com/oss/pyroscope/ — continuous profiling.
- Parca — https://www.parca.dev/ — open source continuous profiling.
- Pixie Labs — https://docs.px.dev/ — eBPF-based observability.
- Felix Geisendörfer blog — https://www.flixien.dev/ — Go profiler internals от автора многих pprof улучшений.
- Dave Cheney, “High Performance Go Workshop” — https://dave.cheney.net/high-performance-go-workshop — глубокое введение в Go performance.
- Cloudflare blog: “Go memory ballast” — https://blog.cloudflare.com/ — case studies.
- Russ Cox, “Profiling Go Programs” — https://go.dev/blog/pprof — оригинальный анонс pprof.
- uber-go/goleak — https://github.com/uber-go/goleak — testing для goroutine leaks.
- Go 1.23 Flight Recorder proposal — https://github.com/golang/go/issues/63185