Модуль: Runtime и память · Уровень: Senior+
TL;DR#
pprof — это система статистического профилирования Go. Большинство профилей (CPU, heap, mutex, block) собираются через сэмплирование, поэтому стоят дёшево и пригодны для продакшена. Профиль можно собрать программно (runtime/pprof) или через HTTP-эндпоинты (net/http/pprof), а затем анализировать в go tool pprof (top/list/web/peek) или в continuous-profiling-системах (Pyroscope, Parca, Grafana Cloud). Ключ к правильному чтению — понимать flat vs cum и разницу между inuse/alloc для heap.
Теория#
Какие профили бывают#
Go runtime ведёт несколько предопределённых профилей. Их можно посмотреть в коде через runtime/pprof.Profiles() и по именам через pprof.Lookup(name).
| Профиль | Что измеряет | Тип | Как собирается |
|---|---|---|---|
cpu (profile) | На чём тратится процессорное время | Сэмплинг по таймеру | SIGPROF ~100 Гц |
heap | Живая и накопленная аллокация в куче | Сэмплинг по объёму | каждые ~512 КБ аллокаций |
allocs | То же, что heap, но дефолтный вид — alloc_space | Сэмплинг по объёму | как heap |
goroutine | Стеки всех горутин на момент снимка | Полный снимок (stop-the-world) | по запросу |
block | Где горутины блокируются (chan, mutex, select) | Сэмплинг событий блокировки | по rate |
mutex | Контеншн на sync.Mutex/RWMutex | Сэмплинг событий разблокировки | по fraction |
threadcreate | Где создавались OS-треды | Снимок | по запросу |
Важно: heap и allocs — это один и тот же источник данных (профиль кучи), различаются лишь дефолтным sample_index, который выбирает инструмент при показе.
runtime/pprof: программный сбор#
Используется, когда нужен контролируемый сбор (CLI-утилита, бенчмарк, разовая диагностика) без открытия HTTP.
package main
import (
"os"
"runtime"
"runtime/pprof"
)
func main() {
// CPU-профиль: пишется потоково всё время между Start и Stop.
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
doWork()
// Heap-профиль: это снимок текущего состояния, не поток.
hf, _ := os.Create("heap.prof")
runtime.GC() // важно: получить актуальную картину живых объектов
pprof.WriteHeapProfile(hf)
hf.Close()
// Любой именованный профиль:
gf, _ := os.Create("goroutine.prof")
pprof.Lookup("goroutine").WriteTo(gf, 0) // debug=0 -> бинарный формат
gf.Close()
}WriteTo(w, debug): debug=0 — бинарный protobuf (для go tool pprof), debug=1 — человекочитаемый текст с агрегированными стеками, debug=2 (только goroutine) — полные стеки всех горутин, как при панике (очень полезно для дедлоков).
net/http/pprof: эндпоинты в проде#
import (
"net/http"
_ "net/http/pprof" // регистрирует /debug/pprof/* в http.DefaultServeMux
)
func main() {
// ОТДЕЛЬНЫЙ серверный mux на внутреннем порту, не публичный!
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// ... основной сервис
}Что регистрирует анонимный импорт _ "net/http/pprof" (через init()):
/debug/pprof/— индекс/debug/pprof/profile?seconds=30— CPU-профиль за N секунд/debug/pprof/heap,/allocs,/goroutine,/block,/mutex,/threadcreate/debug/pprof/trace?seconds=5— execution trace (не pprof, а runtime/trace)/debug/pprof/cmdline,/symbol
Подводный камень: если вы используете кастомный http.ServeMux, эндпоинты НЕ зарегистрируются автоматически (init пишет в DefaultServeMux). Тогда вешайте руками:
mux := http.NewServeMux()
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)go tool pprof: анализ#
# Сбор и сразу анализ по URL (CPU, 30 секунд):
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Heap по URL:
go tool pprof http://localhost:6060/debug/pprof/heap
# Из файла + бинарь (нужен для символизации в старых версиях):
go tool pprof ./mybin cpu.prof
# Веб-UI с флейм-графом (открывает браузер):
go tool pprof -http=:8080 cpu.profОсновные команды интерактивного режима:
(pprof) top # топ-10 функций по flat
(pprof) top20 -cum # топ-20 по cumulative
(pprof) list regexp # построчно показать исходник функции с весами
(pprof) peek funcName # callers/callees функции (граф вызовов вокруг неё)
(pprof) web # SVG граф вызовов в браузере
(pprof) traces # показать примеры стеков-сэмплов
(pprof) tree # текстовое дерево вызовов
(pprof) disasm regexp # дизассемблер с весами (для микрооптимизаций)flat vs cum — главное при чтении#
- flat — время/память, потраченные непосредственно в теле самой функции (без вызываемых ею функций).
- cum (cumulative) — функция плюс всё, что она вызвала вниз по стеку.
Эвристика:
- Высокий flat = горячая точка прямо здесь (тут и оптимизируем).
- Высокий cum при низком flat = функция сама дешёвая, но дерево под ней дорогое (это «маршрутизатор» — ищем глубже).
main/http.serveпочти всегда имеют cum=100%, flat≈0 — это нормально.
Индексы heap-профиля#
Heap-профиль содержит 4 метрики (sample_index). Выбор кардинально меняет интерпретацию:
| sample_index | Что показывает |
|---|---|
inuse_space | Байты живых (не собранных GC) объектов — дефолт для heap, ищем утечки/высокий RSS |
inuse_objects | Кол-во живых объектов — ищем фрагментацию/много мелких объектов |
alloc_space | Все аллоцированные байты за время жизни процесса — дефолт для allocs, ищем GC-давление |
alloc_objects | Кол-во аллокаций суммарно — ищем источник нагрузки на аллокатор |
# Сколько живой памяти сейчас:
go tool pprof -sample_index=inuse_space http://localhost:6060/debug/pprof/heap
# Кто больше всех аллоцирует (давит на GC) за всё время:
go tool pprof -sample_index=alloc_space http://localhost:6060/debug/pprof/heapПравило: высокий inuse — проблема живой памяти/утечка; высокий alloc при низком inuse — GC-давление от короткоживущих объектов (кандидаты на sync.Pool или escape-устранение).
Сравнение профилей (diff)#
Незаменимо для расследования регрессий и утечек во времени:
# base.prof снят раньше, cur.prof позже:
go tool pprof -http=:8080 -diff_base=base.prof cur.prof
# Покажет дельту — что выросло между двумя снимками.Sampling rate — управление точностью/стоимостью#
// CPU: частота сэмплов SIGPROF в герцах. По умолчанию 100.
runtime.SetCPUProfileRate(250) // вызывать ДО StartCPUProfile, осторожно >500
// Heap: один сэмпл в среднем на каждые N байт аллокаций. По умолчанию 512*1024.
// Установить 1 = профилировать каждую аллокацию (дорого, но точно).
runtime.MemProfileRate = 1 // ставить в самом начале main/init, до аллокаций
// Block: фиксировать одно событие блокировки на каждые N наносекунд
// заблокированного времени. 0 = выключено (дефолт).
runtime.SetBlockProfileRate(10000) // 1 сэмпл / 10мкс блокировки
// Mutex: профилируется 1/N событий контеншена. 0 = выключено (дефолт).
runtime.SetMutexProfileFraction(5) // ~каждое 5-е событиеНюанс: MemProfileRate нужно выставлять как можно раньше (изменение после старта аллокаций сделает картину неконсистентной). SetMutexProfileFraction(0) возвращает текущее значение, не меняя его — удобно прочитать.
Continuous profiling#
Разовый pprof ловит момент. Для продакшена нужны непрерывные профили во времени, чтобы коррелировать с инцидентами:
- Grafana Pyroscope — агент тянет
/debug/pprof/*или встраивается SDKpyroscope.Start(...), хранит time-series профили, есть diff между временными окнами. - Parca (eBPF + pprof) — может профилировать без инструментирования (system-wide), pull-модель по тем же эндпоинтам.
- Grafana Cloud Profiles / Datadog Continuous Profiler — managed-варианты.
Все они опираются на стандартный pprof-формат, так что встраивание net/http/pprof уже половина дела.
Подводные камни / gotchas#
- Heap — это снимок, CPU — поток. Перед
WriteHeapProfileделайтеruntime.GC(), иначе вinuse_spaceпопадёт мусор, ещё не собранный GC. - Block и mutex выключены по умолчанию. Если профиль пустой — вы забыли
SetBlockProfileRate/SetMutexProfileFraction. - Сэмплированные числа — оценки, не точные значения. pprof экстраполирует: один heap-сэмпл «весит» как ~512КБ. Для мелких/редких аллокаций цифры шумные; уменьшайте
MemProfileRateдля точности. /debug/pprofнельзя выставлять в публичный интернет — это утечка стеков, имён функций и потенциальный DoS (?seconds=3600). Только internal-порт/mTLS/auth.- CPU-профиль на коротких/IO-bound сервисах почти пуст — SIGPROF срабатывает только когда поток реально на CPU. Для ожиданий используйте block-профиль или trace.
- Кастомный mux игнорирует
_ "net/http/pprof"— эндпоинты регистрируются только вDefaultServeMux. -alloc_spacevs утечка. Большой alloc_space != утечка. Утечка видна в растущемinuse_spaceмежду двумя снимками (diff_base).- Высокий
SetMutexProfileFraction/SetCPUProfileRateсам добавляет оверхед и искажает профиль (наблюдатель влияет на наблюдаемое). - Inlining «съедает» функции. Заинлайненные мелкие функции могут не появиться отдельной строкой; смотрите
listи помните про-gcflags=-lпри отладке.
Вопросы на собеседовании#
В: В чём разница между flat и cum, и как по ним искать узкое место?
О: flat — ресурс, потраченный в теле самой функции; cum — функция плюс весь её поддерев вызовов. Высокий flat — горячая точка, оптимизируем тут. Высокий cum при низком flat — функция-диспетчер, реальная работа глубже, идём по стеку вниз через peek/list. Корневые функции (main, http handler) всегда cum≈100%, и это не повод для паники.
В: Чем inuse_space отличается от alloc_space и когда какой смотреть?
О: inuse_space — байты живых объектов на момент снимка (после последнего GC); по нему ищут утечки и высокий RSS. alloc_space — кумулятивно все аллоцированные байты за жизнь процесса; по нему ищут GC-давление от короткоживущих объектов. Утечка = растущий inuse между снимками; GC-давление = большой alloc при стабильном inuse.
В: Что именно делает анонимный импорт _ "net/http/pprof"?
О: Его init() регистрирует хендлеры (Index, Profile, Cmdline, Symbol, Trace) в http.DefaultServeMux под /debug/pprof/. Если приложение слушает на DefaultServeMux — эндпоинты доступны сразу. Если используется кастомный mux — нужно регистрировать хендлеры из пакета net/http/pprof вручную.
В: Почему block- и mutex-профили часто оказываются пустыми?
О: Они выключены по умолчанию (rate/fraction = 0) из-за оверхеда. Нужно явно вызвать runtime.SetBlockProfileRate(n) и runtime.SetMutexProfileFraction(n). Block считает по наносекундам ожидания, mutex — по доле событий контеншена.
В: Как безопасно собирать профили в продакшене?
О: CPU/heap/block/mutex — сэмплированные и дешёвые, их можно держать включёнными. Эндпоинт /debug/pprof выносим на внутренний порт (localhost:6060) за auth/mTLS, никогда не в публичный интернет. Ограничиваем seconds. Для постоянного наблюдения — continuous profiling (Pyroscope/Parca) с умеренным rate. Heap-снимки безопасны, но не забываем, что goroutine с debug=2 делает stop-the-world.
В: Почему CPU-профиль веб-сервиса показал почти ноль, хотя latency высокая?
О: CPU-профиль через SIGPROF фиксирует только время на CPU. Если сервис IO-bound (ждёт БД/сеть/мьютексы), время уходит на блокировки, а не на процессор. Нужно смотреть block-профиль, mutex-профиль или execution trace (go tool trace), которые видят ожидания и планировщик.
В: Что показывает peek и чем отличается от list?
О: peek funcName показывает непосредственных вызывающих (callers) и вызываемых (callees) функции с их весами — локальный фрагмент графа вызовов; полезно понять, кто и сколько раз приводит к горячей функции. list funcName показывает исходный код функции построчно с flat/cum на каждой строке — для точной локализации горячей строки.
В: Что значит MemProfileRate = 1 и зачем менять дефолт?
О: По умолчанию профилируется в среднем одна аллокация на 512 КБ. =1 означает профилировать каждую аллокацию — максимальная точность ценой большого оверхеда и памяти; полезно в бенчмарке/локальной диагностике редких аллокаций. В проде дефолт оставляют, иначе наблюдатель искажает поведение.
В: Как доказать наличие утечки памяти через pprof?
О: Снять два heap-снимка с интервалом (base.prof, затем cur.prof), сравнить go tool pprof -diff_base=base.prof -sample_index=inuse_space cur.prof. Постоянно растущий inuse_space у конкретного стека под нагрузкой = утечка (или неограниченный кэш). Параллельно проверить goroutine-профиль на рост числа горутин (типичная причина — заблокированные горутины, держащие память).
На что копают на senior+#
- Понимание, что все основные профили статистические, и умение объяснить математику экстраполяции сэмплов (heap rate, CPU Hz) и связанный bias.
- Способность связать профиль с моделью памяти Go: escape analysis, разница stack/heap, почему
alloc_objectsкоррелирует с GC-частотой, какsync.Poolснижает alloc_space. - Корреляция pprof + execution trace + GC-метрики (
gctrace): когда одного pprof недостаточно и нужен trace для тайминга и планировщика. - Continuous profiling в проде: pull vs push, безопасность эндпоинтов, overhead-бюджет, diff по временным окнам для расследования регрессий, корреляция с дашбордами latency.
- Тонкости символизации, инлайнинга и того, как
-gcflagsвлияет на читаемость профиля; чтениеdisasmдля микрооптимизаций горячего пути.