pprof: CPU, heap, goroutine

О лекции

pprof снимает профили CPU, кучи, блокировок и горутин. Разговор про «медленно» без цифр и без профиля обычно ничем не кончается.

Рантайм и инструменты Go golang собеседование

pprof — набор сэмплов: где тратится CPU, какие объекты держат кучу, где застряли горутины, кто держит mutex. Это не логи и не trace распределённых спанов, хотя их сочетают.

Наблюдаемость начинается с метрик: RPS, P99, CPU, RSS. Поиск «почему» часто заканчивается профилем. Снимать лучше с инстанса под боевой нагрузкой, не с ноутбука в простое. Открытый pprof в интернет — утечка стеков и возможность DoS съёмкой CPU. В проде — внутренняя сеть, авторизация, или съём по требованию.

Аналогия: разные анализы у врача. Температура (метрика) сказала «плохо». Дальше снимок костей не заменяет анализ крови. CPU-профиль молчит, пока сервис ждёт Postgres: процессор спит, болен не он.

Какие профили

CPU profile — сэмплы стека по таймеру: где процессор. Не показывает ожидание сети. Heap — аллокации и inuse: кто держит байты сейчас (inuse_space) против кто много выделял за жизнь (alloc_space). Для «растёт RSS» смотрят inuse. Goroutine — стеки всех G: сколько висит в receive, select, syscall. Mutex/block — споры за замки.

import _ "net/http/pprof"

func main() {
	go func() {
		_ = http.ListenAndServe("localhost:6060", nil)
	}()
	// go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
	// go tool pprof http://localhost:6060/debug/pprof/heap
	// go tool pprof http://localhost:6060/debug/pprof/goroutine
}

Подключить pprof к служебному HTTP и снять CPU и heap.

Пример. P99 вырос, CPU в покое, RSS стабилен. CPU-профиль почти пустой. Смотрят goroutine и block: тысячи G в chan receive или в Lock. Или traces в БД: ждут запрос, а не крутят JSON.

Как читать

В pprof полезны top, list, flame graph. Cum — с детьми, flat — сама функция. Оптимизировать библиотечную runtime.mallocgc бессмысленно: смотрят, кто её вызывает из вашего кода. Для CPU «медленно потому что JSON» видно. «Медленно потому что Postgres» — нет, пока не смотрите traces и explain.

Goroutine profile с десятками тысяч G в одном receive — утечка или слишком широкий fan-out. Heap с жирной строкой в пакете кэша — забыли TTL. Сравнение двух heap (до и после) ловит рост. Не путайте alloc_objects (шум мелких аллокаций) с inuse_space (кто держит память сейчас).

Цепочка расследования

  1. Метрика: P99 вырос, CPU или RSS.
  2. Гипотеза: CPU в хендлере / аллокации / горутины / замок.
  3. Профиль, который эту гипотезу проверяет.
  4. Фикс и снова метрика — без этого «покрутили pprof» ничего не доказывает.
Типичная ошибка

Держать pprof на :80 без защиты. Смотреть только CPU, когда сервис ждёт БД. Судить о куче по alloc_objects без inuse_space. Снимать профиль с простоя и удивляться, что «всё в runtime».

На собеседовании

Цепочка: метрика (P99) → нужный профиль → гипотеза → фикс → снова метрика. Heap для RSS — inuse. Зависшие горутины — goroutine и block. Логи без профиля на вопросе про скорость — слабый ответ.