36. net/http/pprof и expvar

Философия наблюдаемости и диагностики из коробки

В инженерной культуре Bell Labs всегда господствовало убеждение: программа, внутреннее состояние которой невозможно легко исследовать во время работы, подобна закрытому черному ящику. Когда система развернута на сотнях серверов под реальной нагрузкой, традиционные отладчики вроде GDB становятся бесполезными — они останавливают процесс и вызывают каскадный сбой всей распределенной архитектуры.

Пакеты net/http/pprof и expvar предоставляют встроенный механизм наблюдаемости (observability) и отладки production-систем. Они позволяют извлекать метрики состояния рантайма, профилировать CPU, память и блокировки без перезапуска приложения или внедрения внешних агентов. Интеграция этих инструментов в стандартную библиотеку отражает философию Go: диагностика должна быть доступна из коробки, но разработчик несет полную ответственность за защиту этих эндпоинтов от несанкционированного доступа.

Для инженера уровня Senior понимание pprof и expvar — это не просто умение открыть http://localhost:6060, а способность программно собирать дампы, настраивать частоту сэмплирования под нагрузку и интерпретировать результаты с учетом Mechanical Sympathy (влияние профилирования на кэш-линии, GC и планировщик).

Under the hood. net/http/pprof: Сэмплирование и рантайм-инструментация

pprof не мониторит каждую инструкцию CPU или каждую аллокацию. Это привело бы к падению производительности на порядки. Вместо этого используется вероятностное сэмплирование и инструментация рантайма.

CPU Profiling

При запросе /debug/pprof/profile?seconds=30 рантайм настраивает системный таймер (setitimer на Linux, CreateTimerQueueTimer на Windows) для генерации сигнала SIGPROF ~100 раз в секунду. Каждый сигнал:

  1. Приостанавливает выполнение текущей горутины.
  2. Снимает стек вызовов (runtime.Callers).
  3. Записывает хеш стека в кольцевой буфер.
  4. Возвращает управление.

Частота 100 Гц выбрана как баланс между детализацией и overhead (~5-10% CPU). При высокой нагрузке ядро ОС может коалесцировать сигналы, но рантайм Go компенсирует это математической экстраполяцией в профайлере.

Heap и Memory Profiling

Работает через runtime.MemProfileRate. По умолчанию равен 512 * 1024 (512 КБ). Это означает, что профилировщик отбирает в среднем одну аллокацию на каждые 512 КБ выделенной памяти (выборка пропорциональна объему, а не просто порядковому номеру аллокации). При запросе /debug/pprof/heap рантайм агрегирует записанные стеки и формирует protobuf-дамп. Установка runtime.MemProfileRate = 1 включает запись каждой отдельной аллокации (100% точность), но увеличивает накладные расходы на mallocgc.

Goroutine и Block Profiles

goroutine профиль собирает состояние всех активных горутин из runtime.allgs. Это операция с остановкой мира (STW) на микросекунды, так как требуется безопасное чтение стеков. block и mutex профили используют атомарные счетчики в примитивах sync, записывая длительность блокировки только если она превысила порог runtime.SetBlockProfileRate.

flowchart TD
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    A["HTTP запрос к /debug/pprof/heap"]:::entry --> B["Рантайм собирает аллокации<br>runtime.MemProfile"]
    B --> C["Агрегация по стекам<br>вызовов"]
    C --> D["Генерация pprof<br>в память"]
    D --> E["Отправка клиенту"]

    F["Код вызывает expvar.Publish"]:::accent --> G["Регистрация в map<br>под sync.Mutex"]
    G --> H["HTTP запрос к /debug/vars"]
    H --> I["Итерация и вызов<br>Var.String"]
    I --> J["Формирование JSON"]
  

expvar: Глобальный реестр и ограничения формата

expvar предоставляет простой способ экспортировать скалярные метрики в формате JSON. Внутренне это структура expvar.Map, защищенная sync.Mutex, содержащая map[string]expvar.Var.

При вызове expvar.Publish("my_counter", counter) значение регистрируется в глобальном реестре. Эндпоинт /debug/vars итерирует карту, вызывает метод String() у каждого значения и собирает JSON-ответ.

Критическое ограничение: expvar не поддерживает гистограммы, квантили, сложную агрегацию или теги. Он предназначен только для монотонных счетчиков (expvar.Int) и гаужей. Метод String() аллоцирует новую строку на каждый запрос /debug/vars, что при высокой частоте опроса мониторингом создает фоновое давление на GC.

Mechanical Sympathy: Цена профилирования и аллокации

Профилирование — это не бесплатная операция. Понимание её стоимости необходимо для безопасного использования в продакшене.

  1. CPU Overhead: SIGPROF вызывает контекстные переключения и прерывает конвейер CPU. При нагрузке >50k RPS профилировщик может добавить 10-15% latency из-за сброса pipeline и кэш-промахов.
  2. STW при Goroutine Dumps: /debug/pprof/goroutine останавливает все горутины для безопасного обхода стеков. При 100k+ горутин это занимает десятки миллисекунд, что видно как всплеск p99 latency.
  3. Аллокации expvar: Каждый запрос /debug/vars генерирует новый JSON. Если Prometheus или Grafana опрашивает эндпоинт каждые 5 секунд, аллокации накапливаются. Решается через прокси-кэш или переход на prometheus/client_golang, который использует zero-аллокационные текстовые протоколы.

Идиомы production-безопасности

Никогда не выставляйте /debug/ в публичную сеть. Используйте базовую аутентификацию, IP-фильтрацию или отдельный админский порт за firewall.

Go
func setupDebugServer(addr string) error {
    mux := http.NewServeMux()

    // Явная регистрация pprof (без импорта _ "net/http/pprof")
    pprof.Handler("profile").ServeHTTP // и т.д. для каждого эндпоинта
    mux.HandleFunc("/debug/pprof/", pprof.Index)
    mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
    mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
    mux.HandleFunc("/debug/vars", expvar.Handler().ServeHTTP)

    // Middleware для базовой аутентификации
    authMux := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user, pass, ok := r.BasicAuth()
        if !ok || user != "admin" || pass != os.Getenv("DEBUG_PASS") {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        mux.ServeHTTP(w, r)
    })

    srv := &http.Server{Addr: addr, Handler: authMux, ReadHeaderTimeout: 5 * time.Second}
    return srv.ListenAndServe()
}

Ловушки и вопросы с собеседований

Сценарий Проблема Решение
/debug/pprof/profile висит 30с По умолчанию собирает 30 секунд. Блокирует HTTP-соединение. Используйте ?seconds=5 для быстрых проверок или запускайте через go tool pprof http://host/debug/pprof/profile локально.
expvar дублирует ключи expvar.Publish паникует при повторной регистрации. expvar.Map добавляет без проверки. Всегда регистрируйте метрики в init() или используйте sync.Once. Для динамических ключей создавайте кастомный expvar.Func.
Горутинный стек не показывает асинхронность pprof показывает только синхронные вызовы. Каналы и таймеры не видны напрямую. Используйте pprof/trace для анализа планировщика или GODEBUG=schedtrace=1000 для логов планировщика.
runtime.MemProfileRate = 0 Отключает профилирование памяти. /debug/pprof/heap вернет пустой дамп. Установите значение > 0. По умолчанию 512KB оптимально. Для точного анализа снижайте до 1 только локально.
pprof в production без auth Любой может скачать дамп кучи, содержащий пароли, токены, приватные данные. Обязательно оборачивайте в auth middleware или используйте build tag -tags noprof для релизных сборок.

Сравнение с экосистемами других языков

Язык / Инструмент Механизм Особенности в сравнении с Go
Java JMX, Java Flight Recorder JMX тяжелый, требует агентов, потребляет много памяти. JFR мощный, но лицензирован для production. Go pprof легковесный и бесплатный.
Python cProfile, tracemalloc, sys.setprofile Работают на уровне интерпретатора, замедляют код на 30-50%. tracemalloc требует включения до старта. Go профилирует нативный код с минимальным overhead.
C++ gperftools, perf, Valgrind Требуют внешних утилит, ручной настройки флагов компиляции и линковки. Go встраивает всё в бинарник, не требуя пересборки.
Node.js clinic.js, --inspect, v8-profiler Зависит от V8. Профилирование JS и native кода разделено. Go единый бинарник, профилировщик видит весь стек (Go + Cgo).
Go net/http/pprof, expvar Нулевая конфигурация, HTTP-интерфейс, безопасное сэмплирование, встроенная интеграция с рантаймом. Идеален для быстрого старта, но требует защиты в prod.

Итог

  1. net/http/pprof использует вероятностное сэмплирование (~100 Гц) для CPU и MemProfileRate для кучи, минимизируя overhead.
  2. expvar — простой реестр скалярных метрик с sync.Mutex. Не поддерживает гистограммы и создает аллокации при /debug/vars.
  3. Никогда не выставляйте /debug/ публично. Используйте auth, IP-фильтры или отдельный админский порт.
  4. Горутинные дампы вызывают микросекундные STW. Не запускайте /debug/pprof/goroutine часто под высокой нагрузкой.
  5. Для production-мониторинга предпочитайте Prometheus/OpenTelemetry. pprof и expvar идеальны для отладки инцидентов и dev-среды.
  6. Программный доступ к профилям осуществляется через pprof.Lookup("тип").WriteTo(), что удобно для автоматического сбора дампов при ошибках.

Понимание инструментов диагностики и экспорта метрик логически подводит нас к вопросу генерации случайных чисел. Как Go обеспечивает криптографическую стойкость, почему стандартный генератор не является потокобезопасным и как правильно выбирать источник случайности? В следующей статье: 37. crypto/rand и math/rand.