Skip to Content
How-toПрофилирование (pprof)

Как профилировать сервис

Рецепт использования pprof для поиска CPU-bottleneck’ов, утечек памяти и зависших goroutine. Профайлинг — это инструмент диагностики, включай его на сервисе осознанно и никогда не выставляй endpoint наружу.

Содержание

Зачем профилировать

Профайлинг применяется, когда уже есть измеримый симптом:

  • 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=true env-флаге на конкретной реплике (для длительной диагностики в 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/heapMemory snapshot (allocated bytes / objects)
/debug/pprof/allocsВсе аллокации с начала процесса
/debug/pprof/profile?seconds=30CPU sample 30 сек
/debug/pprof/goroutineВсе goroutine stacks
/debug/pprof/blockContention (нужен SetBlockProfileRate)
/debug/pprof/mutexMutex-contention (нужен SetMutexProfileFraction)
/debug/pprof/threadcreateOS threads
/debug/pprof/trace?seconds=5Go execution trace (для go tool trace)

Сбор profile

В prod-pod’е port-forward’ишь локально:

kubectl -n kazmaps-prod port-forward deploy/review 6060:6060

Потом собираешь с 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:

curl -s http://localhost:6060/debug/pprof/heap > heap-$(date +%s).pb.gz

Анализ в 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:

go tool pprof -http=:8000 profile.pb.gz

Открывает UI с:

  • Graph — callgraph.
  • Flame Graph — визуализация времени по стеку.
  • Source — код с per-line samples.
  • Top — табличный вид.

Flame graph читается так: ширина = время (CPU) / память (heap), высота = глубина стека. Ищи широкие «плато» на верхних этажах — это hot path.

CPU bottleneck — flow

  1. Поймай момент нагрузки. Smoke-тест / нагрузочный тест / pod под реальным прод-трафиком. Метрика http_request_duration_seconds должна показывать повышенный p99.

  2. Сними CPU profile:

    go tool pprof -http=:8000 http://localhost:6060/debug/pprof/profile?seconds=30
  3. top -cum → найди функцию с cum% > 5.

  4. list <func> → посмотри, какая строка жрёт samples.

  5. Fix. Типовые случаи:

    • json.Unmarshal на hot path — закэшируй, используй готовые схемы, не парси одно и то же дважды.
    • regexp.MustCompile внутри handler’а — вынеси в var re = ....
    • time.Now() в тугом цикле — батчь.
    • Аллокации в цикле — pre-allocate slices, sync.Pool.
  6. Проверь. Снова сняли profile, top -cum должен показывать уменьшенные проценты на той же функции.

Memory leak — flow

Симптом: go_memstats_alloc_bytes или RSS растёт линейно со временем без плато.

  1. Baseline. Сразу после warm-up сними heap:

    curl -s http://localhost:6060/debug/pprof/heap > heap-baseline.pb.gz
  2. Wait — минимум 1 час нормальной работы.

  3. Current. Сними второй снимок:

    curl -s http://localhost:6060/debug/pprof/heap > heap-current.pb.gz
  4. Diff:

    go tool pprof -http=:8000 -base=heap-baseline.pb.gz heap-current.pb.gz
  5. top -cum в diff-режиме → функции с растущими аллокациями.

  6. Типовые причины leak’а:

    • Глобальная map без лимита и без очистки.
    • Goroutine, которая держит ссылку на большой буфер (см. goroutine flow ниже).
    • Незакрытые *sql.Rows, http.Response.Body, файлы.
    • strings.Builder/bytes.Buffer как поля в long-lived struct, куда постоянно пишут.

Goroutine leak — flow

Симптом: go_goroutines растёт и не возвращается к baseline.

  1. Сними text-dump:

    curl -s "http://localhost:6060/debug/pprof/goroutine?debug=2" > g.txt
  2. Сгруппируй по stack-trace’ам (есть одинаковые stack’и — одна причина). В dump’е каждая группа помечена количеством:

    goroutine 4321 [semacquire, 23 minutes]: sync.runtime_Semacquire(...)
  3. Типовые паттерны leak’ов:

    • [chan receive, N minutes] — горутина ждёт сообщения из канала, который никто не закрывает. Обычно — забыт close(ch) или отменённый ctx, который не наблюдается.
    • [select, N minutes] без ctx.Done() в ветке — горутина не может выйти по cancel.
    • [IO wait] — сетевой вызов без timeout’а.
    • [semacquire, N minutes] — взят mutex, который никто не отпускает.
  4. Фикси в коде:

    • Добавь case <-ctx.Done(): return в select.
    • Используй http.Client с таймаутом, а не http.DefaultClient.
    • context.WithTimeout для любых внешних вызовов.
    • defer rows.Close(), defer resp.Body.Close().

См. также ../troubleshooting/test-hangs — 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.

Связанные разделы

Last updated on