Как профилировать сервис¶
Рецепт использования pprof для поиска CPU-bottleneck'ов, утечек
памяти и зависших goroutine. Профайлинг — это инструмент диагностики,
включай его на сервисе осознанно и никогда не выставляй endpoint
наружу.
Содержание¶
- Зачем профилировать
- Setup
- Endpoints
- Сбор profile
- Анализ в pprof
- Browser UI
- CPU bottleneck — flow
- Memory leak — flow
- Goroutine leak — flow
- Continuous profiling
- Benchmark-first
- Security
- Anti-patterns
- Связанные разделы
Зачем профилировать¶
Профайлинг применяется, когда уже есть измеримый симптом:
- Perf-регрессия в
http_request_duration_secondsпосле деплоя. - Memory растёт линейно со временем (
go_memstats_alloc_bytes). - Goroutine count растёт без возврата к baseline (
go_goroutines). - CPU throttling в k8s при нормальной нагрузке.
Без симптома pprof не нужен — это не «регулярная гигиена», а
инструмент расследования.
Setup¶
Подключай net/http/pprof только на отдельный internal port,
никогда не на основной HTTP-сервер:
// cmd/server/main.go
import (
"net/http"
_ "net/http/pprof" // регистрирует handler'ы в http.DefaultServeMux
)
go func() {
srv := &http.Server{
Addr: "127.0.0.1:6060",
Handler: http.DefaultServeMux,
}
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Error("pprof server", "err", err)
}
}()
Правила:
- Только
127.0.0.1:6060на pod. Доступ из вне pod'а — черезkubectl port-forwardили sidecar debug-контейнер. - Никогда не монтировать
/debug/pprofна основной router, даже под auth. Случайный misconfig открытого internal-port'а приводит к утечке stack-trace'ов и memory-layout'ов. - Альтернативно — включать pprof только при
DEBUG_PPROF=trueenv-флаге на конкретной реплике (для длительной диагностики в prod).
Для block/mutex-профилей включи sampling на старте:
runtime.SetBlockProfileRate(1) // все события contention
runtime.SetMutexProfileFraction(1) // все события mutex-contention
В prod — не всегда. SetBlockProfileRate(1) даёт overhead; в prod
используй сэмплинг: SetBlockProfileRate(10000) (одна из 10k).
Endpoints¶
Под http://127.0.0.1:6060/debug/pprof/:
| Endpoint | Что |
|---|---|
/debug/pprof/ |
Index |
/debug/pprof/heap |
Memory snapshot (allocated bytes / objects) |
/debug/pprof/allocs |
Все аллокации с начала процесса |
/debug/pprof/profile?seconds=30 |
CPU sample 30 сек |
/debug/pprof/goroutine |
Все goroutine stacks |
/debug/pprof/block |
Contention (нужен SetBlockProfileRate) |
/debug/pprof/mutex |
Mutex-contention (нужен SetMutexProfileFraction) |
/debug/pprof/threadcreate |
OS threads |
/debug/pprof/trace?seconds=5 |
Go execution trace (для go tool trace) |
Сбор profile¶
В prod-pod'е port-forward'ишь локально:
Потом собираешь с localhost:6060:
# CPU profile 30 секунд под нагрузкой
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Memory heap snapshot
go tool pprof http://localhost:6060/debug/pprof/heap
# Goroutines — текстовый дамп в файл
curl -s http://localhost:6060/debug/pprof/goroutine?debug=2 > goroutines.txt
Сохрани файл для post-mortem:
Анализ в pprof¶
В REPL-режиме (go tool pprof <file-or-url>):
(pprof) top # top-N функций по flat time
(pprof) top -cum # по cumulative
(pprof) list Create # исходник функции Create с samples per-line
(pprof) web # callgraph в браузере (требует graphviz)
(pprof) peek Create # кто вызывает Create
(pprof) disasm Create # assembly (редко нужно)
Полезное:
top -cum— функции, внутри которых (включая вызванные) проведено больше всего времени. Начинай отсюда.list <func>— посмотри, на какой строке сидит hot spot. Часто это конкретныйjson.Unmarshal,regexp.Compileв цикле,time.Now()в горячем пути.
Browser UI¶
Для interactive-explore:
Открывает UI с:
- Graph — callgraph.
- Flame Graph — визуализация времени по стеку.
- Source — код с per-line samples.
- Top — табличный вид.
Flame graph читается так: ширина = время (CPU) / память (heap), высота = глубина стека. Ищи широкие «плато» на верхних этажах — это hot path.
CPU bottleneck — flow¶
- Поймай момент нагрузки. Smoke-тест / нагрузочный тест / pod
под реальным прод-трафиком. Метрика
http_request_duration_secondsдолжна показывать повышенный p99. - Сними CPU profile:
top -cum→ найди функцию сcum%> 5.list <func>→ посмотри, какая строка жрёт samples.- Fix. Типовые случаи:
json.Unmarshalна hot path — закэшируй, используй готовые схемы, не парси одно и то же дважды.regexp.MustCompileвнутри handler'а — вынеси вvar re = ....time.Now()в тугом цикле — батчь.- Аллокации в цикле — pre-allocate slices,
sync.Pool. - Проверь. Снова сняли profile,
top -cumдолжен показывать уменьшенные проценты на той же функции.
Memory leak — flow¶
Симптом: go_memstats_alloc_bytes или RSS растёт линейно со временем
без плато.
- Baseline. Сразу после warm-up сними heap:
- Wait — минимум 1 час нормальной работы.
- Current. Сними второй снимок:
- Diff:
top -cumв diff-режиме → функции с растущими аллокациями.- Типовые причины leak'а:
- Глобальная map без лимита и без очистки.
- Goroutine, которая держит ссылку на большой буфер (см. goroutine flow ниже).
- Незакрытые
*sql.Rows,http.Response.Body, файлы. strings.Builder/bytes.Bufferкак поля в long-lived struct, куда постоянно пишут.
Goroutine leak — flow¶
Симптом: go_goroutines растёт и не возвращается к baseline.
- Сними text-dump:
- Сгруппируй по stack-trace'ам (есть одинаковые stack'и — одна причина). В dump'е каждая группа помечена количеством:
- Типовые паттерны leak'ов:
[chan receive, N minutes]— горутина ждёт сообщения из канала, который никто не закрывает. Обычно — забытclose(ch)или отменённыйctx, который не наблюдается.[select, N minutes]безctx.Done()в ветке — горутина не может выйти по cancel.[IO wait]— сетевой вызов без timeout'а.-
[semacquire, N minutes]— взят mutex, который никто не отпускает. -
Фикси в коде:
- Добавь
case <-ctx.Done(): returnв select. - Используй
http.Clientс таймаутом, а неhttp.DefaultClient. context.WithTimeoutдля любых внешних вызовов.defer rows.Close(),defer resp.Body.Close().
См. также ../troubleshooting/test-hangs.md
— goroutine dump так же применим к зависшим тестам.
Continuous profiling¶
Для prod — Pyroscope / Grafana Phlare. Агент поверх pprof-endpoint'а непрерывно сэмплирует профили и складывает в хранилище.
Полезно, когда регрессия воспроизводится редко: у тебя всегда есть profile за любой момент последнего периода retention'а, post-mortem не требует воспроизведения.
Setup (типовой):
- Sidecar / DaemonSet с Pyroscope-агентом.
- Агент периодически обращается в
/debug/pprof/profile?seconds=10и/debug/pprof/heap. - UI в Grafana → Explore → Pyroscope datasource.
Benchmark-first¶
Не жди prod-инцидента. Для hot path подсемьи пиши benchmark'и:
func BenchmarkReviewCreate(b *testing.B) {
svc := newTestReviewService(b)
for i := 0; i < b.N; i++ {
_, _ = svc.Create(ctx, testCmd)
}
}
Запуск с profile:
go test -bench=BenchmarkReviewCreate -cpuprofile=cpu.pb.gz -memprofile=mem.pb.gz ./internal/service
go tool pprof -http=:8000 cpu.pb.gz
Так профилировать намного дешевле, чем на prod: воспроизведение идеальное, без сетевых шумов, можно сравнивать «до/после» оптимизации.
Security¶
Профилировочные endpoint'ы раскрывают:
- Stack trace'ы с именами функций и переменных (частично).
- Allocated objects — в heap-снимке видны, каких структур сколько.
- Package structure — легко понять, какие библиотеки и версии используются (attack surface reconnaissance).
Поэтому:
- Pprof всегда на
127.0.0.1или на отдельном internal-port'е. NetworkPolicyв k8s — доступ к port 6060 только из SRE-namespace или черезkubectl port-forward.- Gateway никогда не роутит
/debug/*наружу. - Если нужен удалённый доступ — через jump-host / sidecar, не через публичный endpoint.
Anti-patterns¶
- Pprof в prod без предупреждения команды. 30-секундный CPU profile даёт ~2-5% overhead; на edge-нагрузке это превращается в latency spike. Согласовывай окно.
- Long-running CPU profile (> 60 сек). Сэмплы рассеиваются, профиль размазывается, полезный сигнал теряется.
- Profile на одной реплике без контекста. Если из 10 pod'ов профилируешь один, убедись, что он репрезентативен (по roadshow нагрузки, по affinity).
- Premature optimization по profile. Функция занимает 2% CPU — оптимизировать её бессмысленно. Работай по правилу: оптимизируй только то, что > 5% и что имеет измеримое влияние на SLO.
- Pprof endpoint без auth на публичном интерфейсе. Prod-инцидент
одним ошибочным PR → утечка всего адресного пространства процесса
на
/debug/pprof/heap?debug=2для любого в интернете. - Сравнение heap-снимков без warm-up. Первый снимок сразу после старта показывает инициализацию, не leak.
Связанные разделы¶
../conventions/observability.md— метрики (go_goroutines,go_memstats_*), которые подскажут когда профилировать.../troubleshooting/test-hangs.md— goroutine dump для зависшего теста.../conventions/security.md— запрет на публичные debug-endpoint'ы.