Как читать профиль pprof в Go
pprof входит в стандартную поставку Go, но разобраться в его выводе с нуля непросто: профилей шесть, у каждого своя логика, а команды отличаются от учебников. Здесь каждый тип профиля разобран по шагам: как собрать, как открыть, как читать строки вывода и что обычно находят.
Профилирование решает споры о медленном коде
Спор «здесь медленно» или «здесь нормально» в Go решается буквально: профиль показывает, где именно уходит время, память или потоки. Это дешевле, чем читать код и гадать, и точнее, чем интуиция: очень часто «медленное место» оказывается не тем, где все думают.
Инструмент встроен в стандартную библиотеку, поэтому ничего ставить не нужно. В бенчмарках профиль снимается флагом, в работающем сервисе — несколькими строками кода или готовым эндпоинтом net/http/pprof. Дальше файл профиля открывается командой go tool pprof: она работает и в терминале, и в браузере через -http.
Важная деталь, на которой спотыкаются чаще всего: не все профили включены по умолчанию. Для block и mutex нужно вручную вызвать SetBlockProfileRate и SetMutexProfileFraction в коде, иначе профиль будет пустым, и человек решит, что инструмент не работает. Ниже эти настройки указаны для каждого типа.
Ещё одно различие, которое стоит понимать сразу: бенчмарк и профиль живого сервиса отвечают на разные вопросы. Бенчмарк измеряет вашу функцию в идеальных условиях — без конкуренции, без кэша и без шума. Профиль сервиса показывает реальную картину, где время уходит на посторонние вещи: сериализацию, логирование, ожидание базы. Поэтому «медленный бенчмарк» и «медленный сервис» могут иметь совершенно разные причины, и профилировать нужно то, что тормозит у пользователя.
Какой профиль вам нужен
Профилей в Go несколько, и они отвечают на разные вопросы. Выберите тип — ниже появятся команды и разбор вывода.
Показывает, какие функции потребляют CPU. Ответ на вопрос «почему сервис ест ядра».
Шаг первый: собрать профиль
go test -bench=. -benchtime=30s -cpuprofile=cpu.out ./pkg/...В сервисе профиль включают в коде: pprof.StartCPUProfile(f) при старте и pprof.StopCPUProfile() при остановке. Снимать нужно 30 секунд реальной нагрузки — на заглушке профиль покажет пустоту.
Шаг второй: посмотреть результат
go tool pprof -top -cum cpu.out
go tool pprof -http=:8080 cpu.out # графический интерфейсШаг третий: прочитать вывод
Формат вывода pprof top — пример, иллюстрирующий разбор. В реальном профиле значения будут другими.
| flat | flat% | cum | cum% | функция |
|---|---|---|---|---|
| 1.21s | 31.42% | 1.21s | 31.42% | regexp.(*machine).match |
| 0.84s | 21.83% | 1.02s | 26.49% | main.parseOrder |
| 0.51s | 13.25% | 0.62s | 16.10% | encoding/json.Decode |
| 0.02s | 0.52% | 0.48s | 12.47% | main.handleOrder |
Как читать
- Смотрите колонку cum (кумулятивно): она включает время вызванных функций. Именно cum показывает, сколько времени стоит ваш обработчик целиком.
- flat — время внутри самой функции. Большой flat означает, что внутри тяжёлая работа; большой cum при маленьком flat — что функция просто много вызывает.
- Сортируйте по cum (-top -cum), а не по flat: иначе вы увидите stdlib, а не свой код.
- В графическом виде (go tool pprof -http) выбирайте граф и ищите широкие блоки — это дорогие участки.
Что обычно находят
- Регулярные выражения в горячем пути: regexp медленнее простого сравнения и strings.Contains.
- JSON-декодирование целых больших документов, когда нужны два поля.
- Лишние аллокации внутри цикла, которые видны в графе как вызовы runtime.makeslice.
Подводные камни
- вниманиеПрофиль показывает только то время, пока профайлер был включён: короткий замер в бенчмарке не отражает поведение сервиса.
- вниманиеПрофиль без символов (сборка с -ldflags="-s -w") превращается в набор адресов — пересоберите бинарник с символами.
- вниманиеПрофилирование меняет timings: включайте его в стенде, а не в проде с большой нагрузкой.
-top для верхушки списка и -list или -peek для конкретной функции. Оптимизировать по профилю, а не по ощущениям: иначе вы почти гарантированно выберете не то место.Все типы профилей коротко
Подробный разбор каждого типа — в интерактивном блоке выше. Здесь краткая шпаргалка: что профиль показывает и как его снять.
| Профиль | Что показывает | Как снять |
|---|---|---|
| CPU | Показывает, какие функции потребляют CPU. Ответ на вопрос «почему сервис ест ядра». | go test -bench=. -benchtime=30s -cpuprofile=cpu.out ./pkg/... |
| Heap | Показывает живые объекты в куче. Ответ на вопрос «что удерживает память». | go test -bench=. -benchmem -memprofile=heap.out ./pkg/... |
| Allocs | Все аллокации за всё время, а не только живые объекты. Ловит мусор в горячем пути. | go test -bench=. -benchmem -memprofile=mem.out ./pkg/... |
| Goroutine | Снимок всех живых горутин с их стеками. Главный инструмент в поиске утечек горутин. | go tool pprof -top goroutine.out |
| Block | Время, проведённое в блокировках на каналах и мьютексах. Показывает contention. | // в коде сервиса, до старта: |
| Mutex | Статистика конкуренции за мьютексы: доля времени в ожидании и список самых горячих. | runtime.SetMutexProfileFraction(100) // сэмплирование 1 из 100 конфликтов |
Частые вопросы о профилировании в Go
С чего начать, если сервис тормозит?
С CPU-профиля, но только если есть нагрузка, похожая на боевую. Профиль без нагрузки покажет пустоту или фоновые задачи. Снимите 30 секунд профиля под нагрузкой, откройте go tool pprof -top -cum и посмотрите, какая функция стоит на первом месте по cum. Если узкое место в памяти — смотрите heap, если сервис упирается в горутины — goroutine-профиль.
Почему профиль block или mutex пустой?
Потому что их нужно включить в коде: runtime.SetBlockProfileRate и runtime.SetMutexProfileFraction. По умолчанию они выключены, чтобы не создавать накладных расходов в продакшене. Это самая частая причина разочарования: профайлер запустили, а данных нет.
Что означают flat и cum?
flat — время или память, потраченные внутри самой функции. cum — кумулятивно, включая всё, что она вызвала. Для поиска узкого места смотрите cum: он показывает полную стоимость вызова вместе с поддеревом. Если flat большой, а cum почти равен flat, проблема внутри функции; если flat мал, а cum велик, функция просто много вызывает.
Как искать утечку памяти?
Смотрите heap-профиль в режиме inuse_space: он показывает живые объекты. Если конкретная структура занимает растущую долю профиля, смотрите на её ссылки: скорее всего, её кладут в глобальную карту или в замыкание, откуда она не выходит. Помогает сравнение двух снимков профиля с интервалом в минуту: то, что растёт, и есть утечка.
Где изучить дальше
- Урок про бенчмарки и профилированиеКак измерить производительность в Go и не оптимизировать наугад.
- Курс PrometheusМетрики сервиса: что собирать постоянно, а что — по профилю.
- Калькулятор аллокацийСчитаем байты до того, как пойдём искать их в профиле.
- Светофор качества кодаЭвристики простоты кода до того, как он станет медленным.