open atlas
↑ К треку
Go с нуля до senior GO · 06 · 03

pprof от и до: профили CPU, heap, goroutine и mutex — от живого сервиса до утечки

net/http/pprof превращает прод в самоописывающую лабораторию: CPU на 100 Гц, heap inuse/alloc, goroutine, mutex и block. Флеймграф читается по ширине, diff снимков кучи через -base загоняет утечку в угол, а continuous profiling страхует от непредсказанных инцидентов.

GO Senior ◷ 19 min
Уровень
ОсновыJuniorMiddleSenior

Биллинговый сервис каждые четыре дня перезапускал cron, владеть которым никто не хотел. Память росла на ~200 МБ в день; автор кода уволился; перезапуск был «временным» восемь месяцев назад. Инженер, который в итоге убил этот cron, сделал это двумя curl с разницей в сутки: curl -o monday.pb localhost:6060/debug/pprof/heap, то же во вторник, затем go tool pprof -base monday.pb tuesday.pb. Diff вычел всё стабильное — буферы, кеши, пулы соединений, весь шум, делающий сырой heap-профиль нечитаемым, — и оставил ровно одну растущую грань: 190 МБ/день inuse_space, аллоцированных в audit.(*Recorder).Append и удерживаемых пакетным слайсом, в который фича «audit trail» писала, а читать не читал никто. Два снимка, одно вычитание — и восемь месяцев тайны исчезли. Профиль всё это время лежал на порту 6060, в двух флагах от любого, кто спросит. pprof — не кризисный инструмент, а постоянный прибор; кризис — лишь момент, когда о нём вспоминают.

После этого урока вы будете знать, какой профиль отвечает на какой вопрос, как двумя curl-командами закрыть восьмимесячную загадку и почему профайлер должен работать до инцидента, а не во время него.

Инструментируем прод: net/http/pprof по уму

import _ "net/http/pprof" регистрирует хендлеры под /debug/pprof/ — на http.DefaultServeMux, и вот тут кусается. Если основной сервер использует дефолтный mux, ваши профилировочные эндпоинты теперь публичны: смотрящий в интернет /debug/pprof/ сливает имена символов и пути исходников и позволяет любому запускать 30-секундные захваты CPU. Продакшен-паттерн — отдельный внутренний листенер:

import (
	"net/http"
	_ "net/http/pprof" // регистрируется на http.DefaultServeMux
	"runtime"
)

func main() {
	runtime.SetMutexProfileFraction(100) // семплировать 1/100 событий contention
	runtime.SetBlockProfileRate(10_000)  // семплировать блокировки ≥10 мкс (в нс)

	go func() {
		// только внутренний порт: сеть кластера / localhost, никогда не LB
		http.ListenAndServe("127.0.0.1:6060", nil) // отдаёт DefaultServeMux
	}()
	// основной API-сервер на своём mux и своём порту …
}

// Захват с рабочей машины (в k8s — port-forward):
//   go tool pprof -http=:8080 "localhost:6060/debug/pprof/profile?seconds=30"
//   go tool pprof -http=:8080 "localhost:6060/debug/pprof/heap"
//   curl -o now.pb localhost:6060/debug/pprof/heap   # снимок для будущего diff

Вечное возражение — оверхед, и честные числа его снимают: CPU-профайлер семплирует на 100 Гц и обычно стоит единицы процентов во время захвата — и ничего в простое. Heap-профиль семплируется в момент аллокации (по умолчанию один семпл на ~512 КБ выделенного) и включён всегда; чтение почти бесплатно. Профили mutex и block выключены по умолчанию (fraction/rate = 0) и стоят ровно столько, сколько просит ваша частота семплирования. Держать pprof доступным на внутреннем порту — стандартная практика в любой Go-компании, которую будили пейджером хотя бы дважды.

Меню профилей: какой на какой вопрос отвечает

  • CPU (/debug/pprof/profile?seconds=30): куда уходят такты? Рантайм прерывает каждый исполняющийся поток 100 раз в секунду и записывает стек; профиль — статистическая перепись из 3 000 семплов за 30 с. Он видит только время на CPU — сервис, медленный потому, что ждёт (локи, I/O, GC assist), может показать почти пустой CPU-профиль.
  • Heap (/debug/pprof/heap): два взгляда на одни данные, и неверный выбор стоит потерянного дня. inuse_space — байты, живые сейчас, привязанные к точке аллокации: вопрос «что заполняет память?» — утечки, кеши, удержание. alloc_space — накопленные байты за всё время: вопрос «что создаёт давление на GC?» — churn, упаковка fmt из прошлых уроков. Точка может доминировать в alloc_space с нулевым inuse (аллоцировал-и-бросил) и наоборот (один гигантский удерживаемый слайс).
  • Goroutine (/debug/pprof/goroutine): все горутины, сгруппированные по одинаковому стеку, — перепись утечек из юнита о конкурентности.
  • Mutex и block: где конкурируемые локи и ожидания каналов/select сжигают настенное время. Они отвечают на загадку пустого CPU-профиля: сервис, медленный при видимом простое. Оба требуют включения семплирования (код выше) — пустой mutex-профиль обычно значит, что его не включали, а не что contention нет.

Вместе эти четыре профиля — дерево решений: горит CPU — читайте CPU-профиль; растёт память — сравнивайте снимки inuse_space; латентность высокая, а CPU похоже спит — включайте и читайте mutex/block. Без правильного профиля вы гадаете не на том слое.

Викторина

Память сервиса стабильно растёт. Вы открываете heap-профиль, и наверху по alloc_space — хелпер JSON-кодирования с 4 ТБ накопленных аллокаций. Это ваша утечка?

Чтение флеймграфа: ширина — правда, высота — шум

go tool pprof -http=:8080 profile.pb поднимает интерактивный UI; основное чтение происходит во флеймграфе. Правила: ширина = инклюзивная стоимость (функция плюс всё, что она вызвала), поэтому деньги — в самых широких прямоугольниках; высота — лишь глубина вызовов: высокий узкий пик — это глубокий стек, стоящий копейки, и погоня за ним — классический крюк новичка. Когда открываете флеймграф впервые, удержите инстинкт гнаться за самой высокой башней — ищите вместо неё самое широкое плато. Ищите широкие плато у верхней кромки: блок без детей, занимающий ширину, — это self time, настоящие циклы, memcpy, работа на уровне строк кода — туда и приземляется оптимизация. Два системных хода лучше разглядывания: вид top, отсортированный по flat (своё) против cum (инклюзивное), говорит, дорога ли функция сама или лишь председательствует над дорогими детьми; а «focus» (клик или -focus=regex) перерисовывает граф по одному поддереву, превращая профиль сервиса с 60 потоками в читаемую историю. Повторяющиеся Go-фигуры: широкая башня runtime.gcBgMarkWorker — давление GC (чините аллокации, а не GC); широкий runtime.mallocgc под вашими функциями — то же сообщение с доставкой на дом; плато runtime.futex/runtime.semacquire указывают на contention локов — подтверждайте mutex-профилем, а не CPU.

Рабочий процесс утечки: diff, затем continuous profiling

Ход с двумя снимками из пролога — канонический workflow утечки, и он должен стать механическим рефлексом: снять базу (curl -o base.pb …/heap), подождать роста (час, день — пока сигнал не задавит шум), снять снова и запустить go tool pprof -base base.pb current.pb. Diff вычитает стабильную кучу — пулы, кеши, буферы устоявшегося режима — и остаётся рост, привязанный к точке аллокации. Дальше вопрос всегда один — «кто это удерживает?» — и ответ обычно из списка: append-only структура уровня пакета (пролог), map-кеш без вытеснения, утечки горутин с захваченными буферами (сверьтесь с goroutine-профилем) или тонкое удержание через подслайс большого массива. Continuous profiling (непрерывное профилирование) — этот workflow, превращённый в продукт: агент (Grafana Pyroscope, Parca, профайлеры облачных вендоров) снимает профили раз в несколько минут, хранит их как временной ряд и позволяет делать diff через границу деплоя или спрашивать «что изменилось в 14:32?». Разговор об оверхеде тот же: семплирующие профайлеры на низкой частоте стоят ~1–2%, а выигрыш в том, что профиль до инцидента существует — curl по требованию этого не даст никогда.

Викторина

p99 вырос втрое, но 30-секундный CPU-профиль почти пуст — широких кадров нет, семплов сильно меньше, чем 30 с × ядра. Куда смотреть дальше?

Расставь шаги по порядку

Расставьте шаги диагностики утечки памяти через pprof в правильном порядке:

  1. 1 Снять базовый снапшот кучи — curl -o base.pb localhost:6060/debug/pprof/heap — пока рост ещё не перекрыл шум
  2. 2 Подождать достаточно долго (часы или сутки), чтобы сигнал утечки превысил churn и буферы в постоянном состоянии
  3. 3 Снять второй снапшот той же командой в current.pb
  4. 4 Выполнить diff командой go tool pprof -base base.pb current.pb, чтобы вычесть стабильную кучу и выделить рост
  5. 5 Читать inuse_space, чтобы найти аллокационный сайт, ответственный за рост, затем выяснить, кто удерживает память (глобальный слайс, map без вытеснения или горутина с захваченным буфером)
Вспомните перед уходом
  1. 01
    Сопоставьте каждый тип профиля pprof с продакшен-вопросом, на который он отвечает, и назовите два выключенных по умолчанию.
  2. 02
    Опишите workflow утечки через два снимка и почему continuous profiling превосходит версию по требованию.
Итог

pprof — постоянный прибор, а не кризисный инструмент: импортируйте net/http/pprof, отдавайте его на отдельном внутреннем листенере (грабли с дефолтным mux — это публичный профайлер) и включайте семплирование mutex/block на старте, потому что эти два профиля молчат, пока их не включат. Каждый профиль отвечает ровно на один вопрос: CPU семплирует стеки на процессоре на 100 Гц и не видит ожиданий; heap делится на inuse_space (что удерживается — метрика утечек) и alloc_space (что когда-либо выделялось — метрика давления на GC, где прячется churn); goroutine пересчитывает припаркованных; mutex и block меряют простои настенным временем. Флеймграф читается по ширине: инклюзивная стоимость — в ширине блока, своё время — в бездетных плато; системные ходы — flat против cum и focus; башни GC и ширина mallocgc означают «чините аллокации», плато futex — «читайте mutex-профиль». Workflow утечки — два снимка и вычитание: pprof -base стирает стабильную кучу и называет растущую точку аллокации, как с восьмимесячным перезапускным cron биллинга. Continuous profiling крутит эту петлю постоянно за 1–2% оверхеда, сохраняя профиль, который захват по требованию не даст никогда, — из времени до поломки. И диагностическая загадка, которую стоит выучить наизусть: латентность растёт, CPU-профиль пуст — сервис ждёт, и ответ лежит в профилях mutex, block и goroutine.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.