pprof: профилирование в проде
Зачем знать на Middle 1. pprof — это «рентген» Go-программы. На джуне вы могли сказать «у нас сервис тормозит». На мидле от вас ждут: подключение
net/http/pprofза 1 минуту, снятие CPU/heap/goroutine/block/mutex профилей, чтение flame graph, поиск hot path, сравнение двух heap snapshots для leak hunting, диагностика goroutine leak, понимание trade-off оverhead профилирования. Без pprof вы не сможете осмысленно оптимизировать код — вы будете гадать. С pprof — точно увидите, где сжигается CPU и куда уходит память.
Содержание
Заголовок раздела «Содержание»- Базовая концепция: что такое pprof и зачем
- Под капотом: типы профилей, sampling, форматы
- Gotchas: overhead, корректность данных, артефакты
- Production-практики: безопасный pprof, continuous profiling, кейсы
- Вопросы на собесе (25–30)
- Practice
- Источники
1. Базовая концепция: что такое pprof и зачем
Заголовок раздела «1. Базовая концепция: что такое pprof и зачем»1.1. pprof в одном предложении
Заголовок раздела «1.1. pprof в одном предложении»pprof — это инструмент для сбора и визуализации профилей производительности (CPU, memory, blocking, mutex, goroutine state). Состоит из двух частей:
- runtime/pprof и net/http/pprof — сбор данных в Go-программе.
- go tool pprof — анализ собранных профилей.
Зачем нужен:
- Найти, где сжигается CPU (hot functions).
- Найти, куда уходит память (allocation sites + live objects).
- Найти leak горутин (что зависло в
_Gwaiting). - Найти contention на mutex’ах и channels.
1.2. Когда какой профиль использовать
Заголовок раздела «1.2. Когда какой профиль использовать»| Симптом | Профиль |
|---|---|
| CPU 100%, не понимаю почему | CPU profile (/debug/pprof/profile) |
| Память растёт, OOM | Heap profile (/debug/pprof/heap) |
NumGoroutine тысячи, растёт | Goroutine profile (/debug/pprof/goroutine) |
| Латенс высокий, CPU простаивает | Block profile (/debug/pprof/block) |
| Lock contention подозревается | Mutex profile (/debug/pprof/mutex) |
| Хочу видеть scheduler и all events | Trace (/debug/pprof/trace) — но это уже не pprof |
1.3. Минимальный setup
Заголовок раздела «1.3. Минимальный setup»package main
import ( "log" "net/http" _ "net/http/pprof" // !!! важно: blank import регистрирует handlers !!!)
func main() { // Отдельный port для админ-handlers (не на main API): go func() { log.Println(http.ListenAndServe(":6060", nil)) }()
// ... ваше приложение ... runApp()}_ "net/http/pprof" регистрирует endpoints на http.DefaultServeMux:
/debug/pprof/— index./debug/pprof/cmdline— command line./debug/pprof/profile?seconds=30— CPU./debug/pprof/heap— heap./debug/pprof/goroutine— goroutines./debug/pprof/block— block./debug/pprof/mutex— mutex./debug/pprof/threadcreate— thread creation./debug/pprof/trace?seconds=5— execution trace./debug/pprof/symbol— symbol resolution.
1.4. ASCII-схема: workflow
Заголовок раздела «1.4. ASCII-схема: workflow»┌──────────────────┐ HTTP ┌──────────────┐ file ┌─────────────┐│ Go application │ ─────────► │ collected │ ─────────► │ go tool ││ (net/http/pprof)│ │ profile.pb │ │ pprof │└──────────────────┘ └──────────────┘ └─────────────┘ │ │ │ sampling каждые ~10 ms │ open in │ (для CPU) │ browser / │ │ interactive ▼ ▼┌──────────────────┐ ┌────────────────┐│ stack traces + │ │ flame graph ││ счётчики событий │ │ top N │└──────────────────┘ │ source list │ │ peek callees │ └────────────────┘2. Под капотом: типы профилей, sampling, форматы
Заголовок раздела «2. Под капотом: типы профилей, sampling, форматы»2.1. CPU profile
Заголовок раздела «2.1. CPU profile»Как собирается
Заголовок раздела «Как собирается»runtime.SetCPUProfileRate(100) (или по умолчанию 100 Hz) запускает таймер. Каждые ~10 мс через SIGPROF останавливается текущий поток, runtime снимает stack trace текущей горутины, агрегирует:
Time ──────┬───────┬───────┬───────┬───────┬───────┬───────┬─────► │ │ │ │ │ │ │ SIGPROF SIGPROF SIGPROF SIGPROF SIGPROF SIGPROF SIGPROF capture capture capture capture capture capture capture stack stack stack stack stack stack stackКаждый sample = stack trace + 1 «попадание». В конце суммарные попадания на каждой функции = доля CPU времени.
Это statistical sampling. Точность ~1% при достаточном количестве samples (1000+).
Сбор и анализ
Заголовок раздела «Сбор и анализ»# Снять 30-секундный профиль:curl -o cpu.pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Интерактивно:go tool pprof cpu.pprof(pprof) top(pprof) top -cum # сортировка по cumulative(pprof) list myFunc # построчно(pprof) web # SVG в браузер(pprof) peek myFunc # кто вызывает / кого вызывает(pprof) traces # все samples
# Веб-интерфейс (рекомендуется):go tool pprof -http=:8080 cpu.pprof# Откроется http://localhost:8080/ui/# Меню: top, graph, flame graph, peek, source, disassemblyflat vs cum
Заголовок раздела «flat vs cum»(pprof) topShowing nodes accounting for 4.2s, 99.5% of 4.22s total flat flat% sum% cum cum% 2.5s 59.24% 59.24% 2.5s 59.24% myapp.Hash 1.0s 23.70% 82.94% 3.5s 82.94% myapp.processBatch 0.7s 16.59% 99.53% 0.7s 16.59% runtime.mallocgc- flat — время непосредственно в этой функции (без вызываемых).
- cum — время в этой функции плюс во всём, что она вызывает.
Пример:
processBatchflat 1.0s, cum 3.5s → 1.0s «свой» код + 2.5s в вызываемомHash.
В flame graph горизонтальная ширина = cum time. Самые широкие функции вверху ≈ корни проблемы.
Что искать
Заголовок раздела «Что искать»- Функции с большим flat — сам код функции жжёт CPU.
- Функции с большим cum, маленьким flat — оркестратор, проверьте вызываемое.
runtime.mallocgcмного → много аллокаций (см. heap profile).runtime.gcAssistAllocмного → mark assist (см. GC tuning).syscall.Syscallмного → возможно, file/network bound.
2.2. Heap profile
Заголовок раздела «2.2. Heap profile»Как собирается
Заголовок раздела «Как собирается»В отличие от CPU, heap profile НЕ периодический. Runtime включает sampling на каждой N-ой аллокации (default — каждые 512 KB).
runtime.MemProfileRate = 512 * 1024 (можно изменить).
Каждый sample = stack trace + размер allocation. В отличие от CPU, есть 4 разных метрики:
| Метрика | Что показывает |
|---|---|
inuse_space (default) | Память, которую сейчас держат живые объекты. |
inuse_objects | Количество живых объектов. |
alloc_space | Память, аллоцированная за всё время (даже если уже освобождена). |
alloc_objects | Количество аллокаций (исторически). |
Когда какую метрику использовать
Заголовок раздела «Когда какую метрику использовать»- Memory leak? →
inuse_spaceилиinuse_objects(что сейчас живо). - Куда GC время уходит? →
alloc_spaceилиalloc_objects(что аллоцируется в hot path). - OOM? →
inuse_space(что сейчас держит память). - Слишком много GC? →
alloc_objects(частота аллокаций).
# Просто heap:curl http://localhost:6060/debug/pprof/heap > heap.pprofgo tool pprof heap.pprof
# По alloc:go tool pprof -alloc_space heap.pprof# или внутри:(pprof) alloc_space(pprof) topHeap дает точку аллокации, не точку утечки
Заголовок раздела «Heap дает точку аллокации, не точку утечки»Это важно. Если bufferpool.Get() создаёт буфер, потом он попадает в connection.buf, который держится 10 минут — heap profile покажет, что аллокация была в bufferpool.Get(). Но «утекает» он через connection.
Поэтому для leak hunting иногда нужен diff snapshots (см. далее).
Сравнение heap profiles (-base)
Заголовок раздела «Сравнение heap profiles (-base)»# Снять snapshots:curl http://localhost:6060/debug/pprof/heap > t1.pprofsleep 600curl http://localhost:6060/debug/pprof/heap > t2.pprof
# Сравнить (t2 - t1):go tool pprof -base t1.pprof -http=:8080 t2.pprofВ UI top N покажет, что появилось между snapshots. Это лучший способ найти leak.
2.3. Goroutine profile
Заголовок раздела «2.3. Goroutine profile»Что показывает
Заголовок раздела «Что показывает»/debug/pprof/goroutine — список всех живых горутин с их stack traces. Каждая горутина = 1 sample (не sampling, а полный список).
Текстовый формат (быстрее всего читать)
Заголовок раздела «Текстовый формат (быстрее всего читать)»curl 'http://localhost:6060/debug/pprof/goroutine?debug=1'# или debug=2 — ещё подробнееПример вывода:
goroutine profile: total 18421500 @ 0x103b2c4 0x10453e4 0x140abc8# 0x140abc7 net/http.(*conn).serve+0x4e7
200 @ 0x103b2c4 0x10453e4 0x130bcd9# 0x130bcd8 main.worker+0x118
142 @ 0x103b2c4 0x10453e4 0x140abc8# 0x140abc7 crypto/tls.(*Conn).Read+0x4e7Если 1500 горутин в одной точке — это либо нормальная нагрузка (web server), либо leak.
Stack types
Заголовок раздела «Stack types»goroutine 1 [running]:goroutine 2 [chan receive]:goroutine 3 [select]:goroutine 4 [IO wait]:goroutine 5 [semacquire]:goroutine 6 [sync.Cond.Wait]:goroutine 7 [sleep]:Чаще всего leak видно в:
chan receiveбез отправителя.chan sendбез получателя.select (no cases)(бесконечный select?).semacquire(sync.WaitGroup или mutex).
2.4. Block profile
Заголовок раздела «2.4. Block profile»Что показывает
Заголовок раздела «Что показывает»Сколько времени горутины проводят в блокировке на синхронизационных примитивах:
chan send/chan receive.sync.Mutex.Lock(но точнее ловит mutex profile).sync.WaitGroup.Wait.sync.Cond.Wait.time.Sleep(да, тоже учитывается).select.
Включение
Заголовок раздела «Включение»По умолчанию выключен (overhead). Включаем:
runtime.SetBlockProfileRate(rate)// rate в наносекундах. 1 = каждая блокировка регистрируется.// 0 = выключено.// > 0 = sampling: только блокировки длиной > rate ns// плюс длинные с вероятностью пропорциональной rate.Типичная prod-настройка:
runtime.SetBlockProfileRate(int(1 * time.Millisecond))// Игнорируем блокировки < 1 ms.curl http://localhost:6060/debug/pprof/block > block.pprofgo tool pprof -http=:8080 block.pprofМетрика: delay. Сколько времени горутины ждали.
Когда полезен
Заголовок раздела «Когда полезен»- API сервис: cpu простаивает, latency высокий → проверьте block profile.
- «Зависает» при большой нагрузке → contention на channel/mutex.
2.5. Mutex profile
Заголовок раздела «2.5. Mutex profile»Что показывает
Заголовок раздела «Что показывает»Узкоспециализированный block profile только для sync.Mutex и sync.RWMutex. Показывает:
- На каких mutex горутины ждали unlock.
- В каких функциях держался lock (от unlock-site).
Включение
Заголовок раздела «Включение»runtime.SetMutexProfileFraction(rate)// rate=0 — выключено// rate=1 — каждое contention регистрируется// rate=N>1 — каждое N-ое (sampling)Prod: runtime.SetMutexProfileFraction(100) (1% sampling).
curl http://localhost:6060/debug/pprof/mutex > mutex.pprofgo tool pprof -http=:8080 mutex.pprof2.6. Trace (не путать с pprof)
Заголовок раздела «2.6. Trace (не путать с pprof)»Trace — это не sampling, а запись всех scheduler/GC/syscall событий за период. Открывается отдельным инструментом.
curl http://localhost:6060/debug/pprof/trace?seconds=5 > trace.outgo tool trace trace.outПодробно в файле 12-benchmarks-trace.md.
2.7. ThreadCreate profile
Заголовок раздела «2.7. ThreadCreate profile»Показывает, где создавались OS-потоки. Используется редко. Полезно, если runtime.NumThread() растёт (CGO-вызовы, LockOSThread без Unlock).
2.8. Формат файла
Заголовок раздела «2.8. Формат файла»pprof файлы — это protobuf, схема profile.proto (gperftools heritage). Можно открыть на сервере, на лэптопе, в Cloud Profiler — все понимают.
Содержимое:
- Samples — массив (stack_trace, value).
- Locations — функция + line.
- Functions — имя.
- Mappings — segment info.
В Go 1.21+ профайлы содержат больше debug info, поэтому даже без бинарника можно прочитать функции.
3. Gotchas
Заголовок раздела «3. Gotchas»3.1. Heap profile показывает точку аллокации, не «текущего держателя»
Заголовок раздела «3.1. Heap profile показывает точку аллокации, не «текущего держателя»»func newBuffer() []byte { return make([]byte, 1<<20) // heap profile увидит ЗДЕСЬ}
func handle(req *Request) { req.Buf = newBuffer() // утечка реально здесь, если req не освобождается}В heap profile вы увидите newBuffer → runtime.makeslice, но не handle. Чтобы понять «где это держится» — нужен pprof -base сравнение или ручной анализ кода.
3.2. CPU profile врёт на короткой выборке
Заголовок раздела «3.2. CPU profile врёт на короткой выборке»Sampling 100 Hz = 100 samples в секунду. На 5-секундный профиль = 500 samples. Если ваша функция занимает 1% времени → ожидается 5 hits. Это статистически почти ничто.
Хорошая практика:
- Профилировать ≥ 30 секунд под нагрузкой.
- Лучше 60 секунд.
- Если функция важна, проверьте на нескольких профилях.
3.3. Wall time vs CPU time
Заголовок раздела «3.3. Wall time vs CPU time»go tool pprof показывает CPU time (время, когда поток реально работал). Если ваша функция спала в I/O — её не будет в CPU profile. Используйте block profile или trace.
3.4. Goroutine profile может быть огромным
Заголовок раздела «3.4. Goroutine profile может быть огромным»curl http://prod:6060/debug/pprof/goroutine?debug=2 > goroutines.txt# 200 MB файла, если у вас миллион горутин.Используйте debug=1 для краткого вывода, или анализируйте через pprof без debug.
3.5. Inlined функции в pprof
Заголовок раздела «3.5. Inlined функции в pprof»Go inlines маленькие функции (gcflags -l отключает). В pprof они могут показаться как часть caller’а. С -gcflags="-l" можно отключить inlining, но это меняет производительность.
В Go 1.20+ pprof умеет «разворачивать» inlined frames в UI.
3.6. pprof на http.DefaultServeMux — security
Заголовок раздела «3.6. pprof на http.DefaultServeMux — security»import _ "net/http/pprof"http.ListenAndServe(":8080", nil) // !!! pprof доступен по :8080Если ваш main API на этом же порту — pprof открыт миру. Через /debug/pprof/profile?seconds=300 атакующий может завесить CPU.
Правильно: отдельный admin port, не доступный из интернета.
go http.ListenAndServe("127.0.0.1:6060", nil) // только localhostИли через middleware с auth.
3.7. block profile — не учитывает GOMAXPROCS=1
Заголовок раздела «3.7. block profile — не учитывает GOMAXPROCS=1»Если у вас один P, любая блокировка автоматически отдаёт ему work. Block profile может не показать contention, который в multi-core окружении был бы виден.
3.8. -seconds работает не везде
Заголовок раздела «3.8. -seconds работает не везде»curl http://localhost:6060/debug/pprof/heap?seconds=30# Что это?С seconds=30 для heap pprof возвращает diff между snapshot в t=0 и t=30. Это удобно для leak hunting.
Для CPU seconds — обязательно (длительность профилирования).
Для goroutine и block — игнорируется (моментальный снимок).
3.9. -alloc_space включает уже освобождённую память
Заголовок раздела «3.9. -alloc_space включает уже освобождённую память»go tool pprof -alloc_space heap.pprofВы увидите аллокации, которые произошли за всё время с прошлого reset. Это не «живая» память. Используйте, если вас интересует частота аллокаций (для GC тюнинга), а не утечки.
3.10. Профили с разных бинарников несовместимы
Заголовок раздела «3.10. Профили с разных бинарников несовместимы»go tool pprof требует, чтобы профиль соответствовал бинарнику (для resolution символов). Если вы сменили версию, символы могут сдвинуться.
В Go 1.20+ профили содержат debug info прямо в файле — резолвить можно без бинарника. Но для disassembly и source listing всё равно нужен бинарник + исходники.
4. Production-практики
Заголовок раздела «4. Production-практики»4.1. Безопасный pprof в проде
Заголовок раздела «4.1. Безопасный pprof в проде»package admin
import ( "context" "log" "net/http" "net/http/pprof" "time")
func StartPProfServer(addr string, authToken string) { mux := http.NewServeMux() mux.HandleFunc("/debug/pprof/", auth(authToken, pprof.Index)) mux.HandleFunc("/debug/pprof/cmdline", auth(authToken, pprof.Cmdline)) mux.HandleFunc("/debug/pprof/profile", auth(authToken, pprof.Profile)) mux.HandleFunc("/debug/pprof/symbol", auth(authToken, pprof.Symbol)) mux.HandleFunc("/debug/pprof/trace", auth(authToken, pprof.Trace))
server := &http.Server{ Addr: addr, Handler: mux, ReadTimeout: 5 * time.Second, WriteTimeout: 320 * time.Second, // > max trace duration } log.Println(server.ListenAndServe())}
func auth(token string, h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("X-Pprof-Token") != token { http.Error(w, "unauthorized", 401) return } h(w, r) }}Или просто привязать к localhost и подключаться через kubectl port-forward:
kubectl port-forward pod/myapp-xxx 6060:6060curl http://localhost:6060/debug/pprof/heap > heap.pprof4.2. Continuous profiling
Заголовок раздела «4.2. Continuous profiling»Для долгоживущих сервисов лучше постоянно собирать профили (по 30 сек каждые 5 минут) и хранить, чтобы видеть тренды.
Инструменты:
- Grafana Pyroscope (open-source, eBPF + pprof, SDK для Go).
- Datadog Continuous Profiler (managed).
- Google Cloud Profiler (managed, GCP).
- AWS CodeGuru Profiler.
Минимальная самописная версия:
package profiler
import ( "bytes" "context" "fmt" "runtime/pprof" "time")
func StartContinuousCPU(ctx context.Context, every time.Duration, dur time.Duration, sink func(name string, data []byte)) { go func() { t := time.NewTicker(every) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: var buf bytes.Buffer pprof.StartCPUProfile(&buf) time.Sleep(dur) pprof.StopCPUProfile() name := fmt.Sprintf("cpu-%d.pprof", time.Now().Unix()) sink(name, buf.Bytes()) } } }()}Куда сохранять:
- S3/GCS bucket с TTL.
- Loki/Pyroscope.
- Локальный диск + cron rotate.
4.3. Overhead
Заголовок раздела «4.3. Overhead»| Профиль | Overhead | Когда включать |
|---|---|---|
| CPU profile | 1–5% (sampling 100 Hz) | По требованию (3-х минутный snapshot) |
| Heap profile | < 1% (sampling per 512 KB alloc) | Всегда, дешёво |
| Goroutine | < 1% (только при сборе) | По требованию |
| Block profile | 0% (rate=0), может расти до 5–10% с rate=1 | Осторожно, по требованию |
| Mutex profile | < 1% с fraction=100 (1%) | Можно постоянно |
| Trace | 10–20%! | Только на коротких интервалах |
Trace дорогой потому, что записывает все scheduler/GC события (миллионы в секунду).
4.4. Reading flame graph
Заголовок раздела «4.4. Reading flame graph» ┌──────────────────────────────────────────────────┐ │ main │ ├──────────────────────────────────────────────────┤ │ handler │ gc │ ├────────────────────────────────┼──────────────────┤ │ parse │ marshal │ gcMark │ ├─────────────┼──────────────────┼──────────────────┤ │ Atoi │ encoding/json │ ... │ └─────────────┴──────────────────┴──────────────────┘ width = % of CPU timeПравила чтения:
- Ширина = время. Узкая функция = редкая. Широкая = много CPU.
- Высота = глубина стека. Низкая = root. Высокая = глубоко вложенная.
- Цвет — обычно random, для различения. Не несёт смысла.
- Плато — ровная top-функция, занимающая много ширины — это листовой hot path, оптимизируйте её.
- Пирамида — высокая башня — это глубокая call chain. Если в основании много времени — оркестратор, ищите выше.
В UI:
- Клик на функцию = zoom in.
- Search bar = найти все вхождения функции.
4.5. Кейс 1: CPU 100% на единственном handler
Заголовок раздела «4.5. Кейс 1: CPU 100% на единственном handler»Симптом: один HTTP endpoint жгёт CPU.
curl http://localhost:6060/debug/pprof/profile?seconds=30 > cpu.pprofgo tool pprof -http=:8080 cpu.pprofFlame graph показывает: 70% времени в regexp.MatchString. Смотрим source:
func handle(w http.ResponseWriter, r *http.Request) { if regexp.MustCompile(`^[a-z0-9]+$`).MatchString(r.URL.Path) { // ... }}Bug: MustCompile в каждом запросе компилирует регулярку!
Fix:
var pathRe = regexp.MustCompile(`^[a-z0-9]+$`) // once
func handle(w http.ResponseWriter, r *http.Request) { if pathRe.MatchString(r.URL.Path) { // ... }}После фикса: CPU 100% → 8%. Это самый частый pprof-инсайт.
4.6. Кейс 2: memory leak
Заголовок раздела «4.6. Кейс 2: memory leak»Сервис ест память, не отдаёт.
curl http://localhost:6060/debug/pprof/heap > t0.pprof# подождать 30 минутcurl http://localhost:6060/debug/pprof/heap > t1.pprofgo tool pprof -base t0.pprof -http=:8080 t1.pprofTop diff:
flat flat% sum% cum cum%500MB 89.7% 89.7% 500MB 89.7% main.(*Cache).SetСмотрим код:
func (c *Cache) Set(key string, value []byte) { c.mu.Lock() defer c.mu.Unlock() c.data[key] = value // !!! без eviction !!!}Решение: LRU или TTL eviction.
4.7. Кейс 3: goroutine leak
Заголовок раздела «4.7. Кейс 3: goroutine leak»curl 'http://localhost:6060/debug/pprof/goroutine?debug=1' | head -20goroutine profile: total 5032150000 @ 0x103b2c4 0x10453e4 0x140abc8# 0x140abc7 main.processRequest+0x4e7 /app/main.go:4250k горутин в main.processRequest+0x4e7. Смотрим:
func processRequest(req Request) { ch := make(chan Result) go fetch(ch, req) // ! нет select с context.Done() ! result := <-ch // line 42, висим вечно если fetch не вернул process(result)}Если fetch иногда не отвечает и не закрывает chan — leak.
Fix:
func processRequest(ctx context.Context, req Request) error { ch := make(chan Result, 1) // буфер, чтобы fetch не висел go fetch(ch, req) select { case result := <-ch: process(result) return nil case <-ctx.Done(): return ctx.Err() }}4.8. Кейс 4: lock contention
Заголовок раздела «4.8. Кейс 4: lock contention»CPU простаивает, p99 latency растёт.
// Включить mutex profile:runtime.SetMutexProfileFraction(100)curl http://localhost:6060/debug/pprof/mutex > mutex.pprofgo tool pprof -http=:8080 mutex.pprofflat flat% cum cum%200ms 50.0% 200ms 50.0% main.(*Counter).AddCounter под global mutex:
type Counter struct { mu sync.Mutex n int64}
func (c *Counter) Add(delta int64) { c.mu.Lock() defer c.mu.Unlock() c.n += delta}При 1000 RPS это узкое место.
Fix:
import "sync/atomic"
type Counter struct { n atomic.Int64}
func (c *Counter) Add(delta int64) { c.n.Add(delta)}Latency упала с 50 ms до 1 ms.
4.9. Profile snapshot routine
Заголовок раздела «4.9. Profile snapshot routine»Полезно для отчётов / postmortem:
package profiler
import ( "fmt" "os" "runtime/pprof" "time")
func DumpAll(dir string) error { ts := time.Now().Format("20060102-150405")
// Heap f, err := os.Create(fmt.Sprintf("%s/heap-%s.pprof", dir, ts)) if err != nil { return err } pprof.Lookup("heap").WriteTo(f, 0) f.Close()
// Goroutine f, err = os.Create(fmt.Sprintf("%s/goroutine-%s.pprof", dir, ts)) if err != nil { return err } pprof.Lookup("goroutine").WriteTo(f, 0) f.Close()
// CPU 30 sec f, err = os.Create(fmt.Sprintf("%s/cpu-%s.pprof", dir, ts)) if err != nil { return err } pprof.StartCPUProfile(f) time.Sleep(30 * time.Second) pprof.StopCPUProfile() f.Close()
return nil}При SIGTERM это можно автоматически перед выходом.
4.10. Сравнение версий: A/B
Заголовок раздела «4.10. Сравнение версий: A/B»Деплоите новую версию, хотите убедиться, что она быстрее старой.
# На v1.0:curl http://v1:6060/debug/pprof/profile?seconds=60 > v1.pprof
# На v2.0:curl http://v2:6060/debug/pprof/profile?seconds=60 > v2.pprof
# Diff:go tool pprof -base v1.pprof -http=:8080 v2.pprofПокажет, что прибавилось в v2 (положительные diff’ы) и исчезло (отрицательные).
5. Вопросы на собесе
Заголовок раздела «5. Вопросы на собесе»- Что такое pprof? Какие виды профилей бывают?
- Как подключить pprof к HTTP-серверу?
- В чём опасность подключения pprof на public port?
- Что такое sampling в CPU profile? Какая частота по умолчанию? (100 Hz.)
- Чем отличается inuse_space от alloc_space в heap profile?
- Как сравнить два heap profile? (
pprof -base.) - Что показывает goroutine profile? Как его получить?
- Когда нужен block profile, и как его включить? (
runtime.SetBlockProfileRate.) - Когда нужен mutex profile, и как его включить? (
runtime.SetMutexProfileFraction.) - Чем отличаются flat и cum в pprof top?
- Что такое flame graph и как его читать?
- Какой overhead у CPU профилирования? (~1–5%.)
- Какой overhead у trace? (10–20%.)
- Что показывает heap profile — точку аллокации или текущего держателя?
- Почему может расти memory, но heap profile показывает мало? (RSS != HeapAlloc, см. файл по GC.)
- Как diagnosticнуть memory leak через pprof?
- Как diagnosticнуть goroutine leak?
- Какие endpoints у net/http/pprof?
- Чем -seconds=30 в URL отличается для CPU vs heap? (CPU duration vs heap diff.)
- Какие функции в pprof обычно сигнализируют о много allocations? (
runtime.mallocgc.) - Какая функция показывает GC mark assist? (
runtime.gcAssistAlloc.) - Что делать, если в pprof много времени в
runtime.findrunnable? (Idle workers, normal.) - Как уменьшить overhead в pprof? (Sampling rate, off block profile.)
- Как профилировать short-lived процесс? (
pprof.StartCPUProfileв main + defer Stop.) - Что такое continuous profiling? Какие инструменты?
- Как pprof определяет «hot» функцию — по CPU или wall time? (CPU.)
- Inlined function в pprof — что с ней?
- Можно ли смотреть pprof локально на лэптопе, если бинарник от другой архитектуры? (Сложно, нужен matching binary.)
- Какой output format у pprof? (Protobuf, .pb.gz.)
- Чем pprof отличается от trace?
6. Practice
Заголовок раздела «6. Practice»6.1. Минимальный сервер с pprof
Заголовок раздела «6.1. Минимальный сервер с pprof»package main
import ( "fmt" "math/rand" "net/http" _ "net/http/pprof" "time")
func compute() int { s := 0 for i := 0; i < 1e7; i++ { s += rand.Int() % 100 } return s}
func main() { go http.ListenAndServe(":6060", nil)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { result := compute() time.Sleep(10 * time.Millisecond) fmt.Fprintf(w, "%d", result) })
http.ListenAndServe(":8080", nil)}# Сгенерировать нагрузку:hey -z 30s -c 100 http://localhost:8080/
# Параллельно профилировать:curl http://localhost:6060/debug/pprof/profile?seconds=30 > cpu.pprofgo tool pprof -http=:8081 cpu.pprof6.2. Heap profile leak
Заголовок раздела «6.2. Heap profile leak»package main
import ( "fmt" "net/http" _ "net/http/pprof" "sync" "time")
type Cache struct { mu sync.Mutex data map[string][]byte}
func (c *Cache) Set(k string, v []byte) { c.mu.Lock() defer c.mu.Unlock() c.data[k] = v}
var cache = &Cache{data: make(map[string][]byte)}
func main() { go http.ListenAndServe(":6060", nil)
go func() { i := 0 for { cache.Set(fmt.Sprint(i), make([]byte, 1<<10)) i++ time.Sleep(time.Millisecond) } }()
select {}}# Через 30 сек:curl http://localhost:6060/debug/pprof/heap > t1.pprof# Через 3 минуты:curl http://localhost:6060/debug/pprof/heap > t2.pprofgo tool pprof -base t1.pprof -http=:8080 t2.pprof# Увидите, что main.(*Cache).Set вырос.6.3. Goroutine leak
Заголовок раздела «6.3. Goroutine leak»package main
import ( "fmt" "net/http" _ "net/http/pprof" "time")
func leak() { ch := make(chan struct{}) <-ch // навсегда}
func main() { go http.ListenAndServe(":6060", nil)
for i := 0; i < 10; i++ { go leak() }
for { time.Sleep(time.Second) fmt.Println("leaks made") }}curl 'http://localhost:6060/debug/pprof/goroutine?debug=1'# Увидите N горутин в main.leak.func1.6.4. Mutex contention
Заголовок раздела «6.4. Mutex contention»package main
import ( "net/http" _ "net/http/pprof" "runtime" "sync")
var ( mu sync.Mutex counter int)
func work() { for i := 0; i < 1e6; i++ { mu.Lock() counter++ mu.Unlock() }}
func main() { runtime.SetMutexProfileFraction(1) // включить mutex profile
go http.ListenAndServe(":6060", nil)
var wg sync.WaitGroup for i := 0; i < runtime.GOMAXPROCS(0); i++ { wg.Add(1) go func() { defer wg.Done(); work() }() } wg.Wait()}curl http://localhost:6060/debug/pprof/mutex > mutex.pprofgo tool pprof -http=:8080 mutex.pprof# Увидите main.work держит main.mu.6.5. Использование runtime/pprof напрямую
Заголовок раздела «6.5. Использование runtime/pprof напрямую»package main
import ( "log" "os" "runtime/pprof")
func cpuWork() { s := 0 for i := 0; i < 1e9; i++ { s += i } _ = s}
func main() { f, err := os.Create("cpu.pprof") if err != nil { log.Fatal(err) } defer f.Close()
if err := pprof.StartCPUProfile(f); err != nil { log.Fatal(err) } defer pprof.StopCPUProfile()
cpuWork()}go run main.gogo tool pprof -http=:8080 cpu.pprof6.6. Continuous profiling stub
Заголовок раздела «6.6. Continuous profiling stub»package main
import ( "bytes" "fmt" "io" "net/http" _ "net/http/pprof" "os" "runtime/pprof" "time")
func uploadProfile(name string, r io.Reader) { // sink: S3, GCS, Pyroscope... // для примера — в файл: f, _ := os.Create(name) defer f.Close() io.Copy(f, r)}
func continuousCPU(period time.Duration, dur time.Duration) { for { var buf bytes.Buffer pprof.StartCPUProfile(&buf) time.Sleep(dur) pprof.StopCPUProfile() name := fmt.Sprintf("cpu-%d.pprof", time.Now().Unix()) uploadProfile(name, &buf) time.Sleep(period - dur) }}
func main() { go http.ListenAndServe(":6060", nil) go continuousCPU(5*time.Minute, 30*time.Second) select {}}7. Источники
Заголовок раздела «7. Источники»- net/http/pprof package: https://pkg.go.dev/net/http/pprof
- runtime/pprof package: https://pkg.go.dev/runtime/pprof
- Profiling Go Programs (official blog): https://go.dev/blog/pprof
- pprof tool docs: https://github.com/google/pprof/blob/main/doc/README.md
- JBD — Profiling Go applications: https://rakyll.org/profiler-labels/
- Brendan Gregg — Flame graphs: https://www.brendangregg.com/flamegraphs.html
- Damian Gryski — High Performance Go Workshop: https://dave.cheney.net/high-performance-go-workshop/dotgo-paris.html
- Grafana Pyroscope (continuous profiling): https://grafana.com/oss/pyroscope/
- Datadog Continuous Profiler for Go: https://docs.datadoghq.com/profiler/enabling/go/
- GopherCon 2019: 7 common mistakes in Go and when to avoid them (Steve Francia)
- Diagnostics guide: https://go.dev/doc/diagnostics
- runtime package — SetBlockProfileRate / SetMutexProfileFraction: https://pkg.go.dev/runtime