Ваше приложение на Go начало тормозить. Первая мысль? Наверное, база данных медленно отвечает. Вторая? Может, сеть лагает. Мы начинаем строить догадки, добавлять кэши, оптимизировать запросы, переписывать SQL-конструкции, дергать девопсов... и часто бьем мимо цели. Мы тратим часы, а то и дни, на оптимизацию того, что и так работало нормально, в то время как настоящая проблема прячется в совершенно неожиданном месте нашего собственного кода. Знакомая боль, не правда ли?

Чтобы перейти от предположений к точному анализу, в стандартной библиотеке Go предусмотрен мощный инструмент — пакет pprof. Это не просто утилита, а полноценная система для профилирования, которая заменяет догадки на эмпирические данные. pprof функционирует как диагностический инструмент, с высокой точностью указывая на узкие места в приложении: какая функция потребляет избыточное процессорное время, где происходят утечки памяти, или почему количество горутин растет без ограничений. Он предоставляет объективную картину состояния программы. Данная статья является практическим руководством, которое поможет вам научиться интерпретировать данные профилирования и стать инженером, решающим проблемы на основе фактов.

Если вам интересен процесс и вы хотите следить за дальнейшими материалами, буду признателен за подписку на мой телеграм-канал. Там я публикую полезныe материалы по разработке, разборы сложных концепций, советы как быть продуктивным и конечно же отборные мемы: https://t.me/nullPointerDotEXE.

Две модальности сбора данных: net/http/pprof и runtime/pprof

Ключ к эффективному использованию pprof лежит в понимании двух различных подходов к сбору данных. Хотя оба используют один и тот же внутренний механизм рантайма Go, их назначение и сценарии применения кардинально отличаются.

net/http/pprof: Непрерывный мониторинг для сервисов

Он предназначен для долгоживущих приложений: веб-сервисов, API-шлюзов, фоновых воркеров. Его активация тривиальна: достаточно импортировать пакет _"net/http/pprof". И он автоматически регистрирует набор HTTP-обработчиков по пути /debug/pprof/.

Этот пакет предоставляет пассивный HTTP-интерфейс к данным профилирования, которые рантайм Go собирает постоянно. Вы можете в любой момент времени подключиться к работающему приложению (на staging или даже production(но лучше не надо)) и запросить его состояние: профиль CPU за последние N секунд, текущее состояние кучи, трассировку всех горутин и т.д. Это основной инструмент для диагностики проблем в реальных условиях эксплуатации, так как он позволяет наблюдать за системой в ее естественной среде, под реальной нагрузкой, без необходимости модифицировать код или перезапускать сервис.

Давайте создадим простой веб-сервер, в котором на каждый запрос будет утекать 1 МБ памяти.

package main

import (
	"fmt"
	"log"
	"net/http"
	_ "net/http/pprof" // Активация обработчиков pprof
)

// Глобальная переменная — самый частый источник утечек.
// Сборщик мусора (GC) не может очистить эту память, пока на нее есть ссылка.
var leakyData [][]byte

func handleLeakyRequest(w http.ResponseWriter, r *http.Request) {
	// С каждым запросом утекает 1 мегабайт памяти.
	data := make([]byte, 1024*1024)
	// Мы добавляем наш 1МБ в глобальный срез.
	leakyData = append(leakyData, data)

	w.WriteHeader(http.StatusOK)
	fmt.Fprintf(w, "Added 1MB to memory. Current leaky size: %d MB\n", len(leakyData))
}

func main() {
	http.HandleFunc("/leak", handleLeakyRequest)

	log.Println("Starting server on :8080")
	log.Println("Pprof endpoints available at http://localhost:8080/debug/pprof/")

	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatalf("Failed to start server: %v", err)
	}
}

После запуска этого сервера мы можем несколько раз обратиться к http://localhost:8080/leak, чтобы симулировать утечку. Теперь наше приложение готово к диагностике.

Как анализировать собранные данные: браузер vs. консольная утилита

После активации net/http/pprof у нас есть два основных способа получить и проанализировать данные о производительности.

Способ 1: Быстрый осмотр в браузере

Пример открытия http://localhost:8080/debug/pprof
Пример открытия http://localhost:8080/debug/pprof

Это самый простой способ. Вы можете просто открыть в браузере один из эндпоинтов, которые зарегистрировал pprof. Например, http://localhost:8080/debug/pprof/heap. Вы увидите текстовое представление данных, которое удобно для быстрой оценки, но не для глубокого анализа.

Основные эндпоинты, которые вы будете использовать:

  • /debug/pprof/heap: Снимок объектов в куче (для поиска утечек памяти).

  • /debug/pprof/profile: Профиль CPU (собирает данные в течение N секунд, по умолчанию 30).

  • /debug/pprof/goroutine: Трассировка стеков всех текущих горутин (для поиска дедлоков).

  • /debug/pprof/allocs: Снимок всех прошлых аллокаций памяти.

  • /debug/pprof/block: Стектрейсы горутин, заблокированных на примитивах синхронизации.

Способ 2: Глубокий анализ с помощью go tool pprof

Для детального и интерактивного анализа предназначен стандартный инструмент go tool pprof.

Важно понимать: эта утилита не является отдельным способом профилирования. Она подключается к тем же HTTP-эндпоинтам, которые мы активировали в нашем приложении, но предоставляет мощный интерактивный интерфейс для работы с данными. Это основной инструмент для диагностики проблем в реальных условиях эксплуатации. Для детального и интерактивного анализа предназначен стандартный инструмент go tool pprof.

Анализ утечки памяти с помощью go tool pprof

Давайте используем консольную утилиту для анализа утечки памяти в нашем примере

1. Подключение к профилю

Откройте терминал и выполните команду. Она скачает профиль кучи с нашего работающего сервера и запустит интерактивную сессию:

go tool pprof http://localhost:8080/debug/pprof/heap

Вы увидите примерно следующее:

Fetching profile over HTTP from http://localhost:8080/debug/pprof/heap
Saved profile in C:\Users\user\pprof\pprof.main.exe.alloc_objects.alloc_space.inuse_objects.inuse_space.005.pb.gz
File: main.exe
Build ID: C:\Users\user\AppData\Local\Temp\go-build2954703233\b001\exe\main.exe2025-06-16 08:42:02.0746201 +0300 MSK
Type: inuse_space
Time: 2025-06-16 15:42:18 MSK
Entering interactive mode (type "help" for commands, "o" for options)

2. Основные команды для анализа

В интерактивном режиме доступны десятки команд (help для полного списка), но для большинства задач достаточно трех:

  • top: Показывает функции, которые потребляют больше всего ресурса (памяти, CPU).

  • list <имя функции>: Показывает исходный код функции с аннотациями по потреблению ресурсов для каждой строки.

  • web: Генерирует и открывает визуальный граф вызовов (требуется установленный Graphviz).

3. Поиск источника утечки

Введем команду top, чтобы увидеть главных потребителей памяти:

Showing nodes accounting for 4.47MB, 100% of 4.47MB total
Showing top 10 nodes out of 14
      flat  flat%   sum%        cum   cum%
    3.47MB 77.59% 77.59%     3.47MB 77.59%  main.handleLeakyRequest
       1MB 22.41%   100%        1MB 22.41%  runtime.allocm
  • flat: Память, выделенная непосредственно в этой функции.

  • cum (cumulative): Память, выделенная этой функцией и всеми функциями, которые она вызвала.

Вывод top однозначно указывает на main.handleLeakyRequest как на источник проблемы — на нее приходится 77.59% выделенной памяти (почти 3.47MB в моем случае).

Теперь посмотрим на код этой функции с помощью list:

Total: 4.47MB
ROUTINE ======================== main.handleLeakyRequest in C:\Users\user\Desktop\pprof\main.go
    3.47MB     3.47MB (flat, cum) 77.59% of Total
         .          .     14:func handleLeakyRequest(w http.ResponseWriter, r *http.Request) {
         .          .     15:   // С каждым запросом "утекает" 1 мегабайт памяти.
    3.47MB     3.47MB     16:   data := make([]byte, 1024*1024)
         .          .     17:   // Мы добавляем наш 1МБ в глобальный срез.
         .          .     18:   leakyData = append(leakyData, data)
         .          .     19:
         .          .     20:   w.WriteHeader(http.StatusOK)
         .          .     21:   fmt.Fprintf(w, "Added 1MB to memory. Current leaky size: %d MB\n", len(leakyData))

pprof точно подсвечивает строку data := make([]byte, 1024*1024), ответственную за все МБ выделенной памяти. Анализ кода показывает, что эта память сохраняется в глобальной переменной leakyData, что и является причиной утечки.

Пример генерации графа командой web
Пример генерации графа командой web

Мы рассмотрели, как профилировать долгоживущие сервисы с помощью net/http/pprof. Но что, если у вашего приложения нет HTTP-сервера? Это могут быть:

  • CLI-утилиты: Инструменты командной строки, которые выполняют задачу и завершаются.

  • Скрипты обработки данных: Программы, которые читают файл, обрабатывают его и сохраняют результат.

  • Воркеры без API: Фоновые процессы, которые получают задачи из очереди (например, RabbitMQ или Kafka), а не по HTTP.

Для таких случаев предназначен пакет runtime/pprof. В отличие от net/http/pprof, который пассивно собирает данные и ждет подключения, runtime/pprof дает вам прямой контроль над процессом профилирования. Вы сами в коде решаете, когда начать сбор данных, когда его закончить и в какой файл сохранить результат.

Пример

Представим, что у нас есть утилита, которая выполняет некие сложные вычисления. Мы хотим понять, какая именно часть кода потребляет больше всего процессорного времени

package main

import (
	"log"
	"os"
	"runtime/pprof"
)

func fib(n int) int {
	if n <= 1 {
		return n
	}

	prev, curr := 0, 1
	for i := 2; i <= n; i++ {
		prev, curr = curr, prev+curr
	}
	return curr
}

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()

	log.Println("Начинаем считать...")
	// Нагружаем CPU
	result := fib(35)
	log.Printf("Результат fib(35): %d\n", result)
}
}

Анализ профиля

После запуска этой программы в каталоге появится файл cpu.pprof, содержащий данные профилирования процессора. Его можно проанализировать с помощью команды:

go tool pprof cpu.pprof

В интерактивной оболочке pprof можно использовать те же команды, что я показывал вышел. Вывод будет аналогичным, как и в прошлых примерах.

Заключение

Мы рассмотрели два ключевых подхода к профилированию в Go: непрерывный мониторинг сервисов с помощью net/http/pprof и точечный анализ для утилит через runtime/pprof. Мы увидели, как команды top, list и web превращают туманные подозрения в конкретные строки кода, ответственные за проблему.

Но pprof — это не просто инструмент. Это переход от культуры предположений («кажется, тормозит база») к культуре фактов. В следующий раз, когда производительность упадет, вы не будете в панике искать виноватых. Вы спокойно подключитесь к профилировщику, соберете данные и придете к коллегам не с проблемой, а с доказательствами и, возможно, уже готовым решением.

Да, мы научились находить проблемы производительности внутри одного приложения. Но современные системы редко состоят из одного монолита. Обычно это десятки микросервисов, где один пользовательский запрос порождает целую цепочку вызовов между сервисами.

Здесь возможности pprof заканчиваются: он может показать, что приложение большую часть времени ждёт ответа… но от кого?

Ответ даёт следующий уровень диагностики — распределённая трассировка. Инструменты вроде Jaeger, Zipkin и стандарт OpenTelemetry позволяют отследить полный путь запроса через все сервисы. Вы получаете наглядную картину с разбивкой по времени и сервисам.

Тема трассировки объемная и если вы хотите углубиться, то вот отличная статья : https://habr.com/ru/articles/710644/

Эти два подхода не конкурируют, а идеально дополняют друг друга, формируя полный инструментарий для анализа производительности:

  1. Горизонтальный анализ (трассировка): С помощью распределенной трассировки вы находите, какой сервис в вашей системе является бутылочным горлышком.

  2. Вертикальный анализ (pprof): Определив проблемный сервис, вы используете pprof, чтобы провести глубокий анализ и найти, какая функция внутри него вызывает деградацию.

Таким образом, pprof остаётся ключевым инструментом для детального анализа кода, а трассировка — для понимания архитектурных узких мест всей системы.

По традиции жду ваше мнение в комментариях.

Комментарии (1)


  1. oleganatolievich
    17.06.2025 19:30

    Так примеры синтетические. Утечка заранее известна. Такое себе.

    В реальных утечках зачастую непонятно даже по top в pprof куда память течет.