15. Профилирование и отладка

«Нельзя оптимизировать или исправить то, что вы не в состоянии измерить».
— Брайан Керниган


Архитектура подсистемы профилирования (pprof)

Подсистема pprof интегрирована непосредственно в рантайм Go и обеспечивает измерение параметров выполнения сервиса с минимальным оверхедом (обычно <1-2%):

flowchart TD
    classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc

    subgraph RuntimeEngine["Ядро рантайма Go (Runtime Engine)"]
        SigProf["SIGPROF (100 Гц сигналы ОС)"]:::process
        MemHooks["Хуки аллокатора кучи (mcache / mcentral)"]:::process
        SyncHooks["Точки ожидания (sema / lock / chan)"]:::process
    end

    subgraph Endpoints["Транспорт профилей (net/http/pprof)"]
        HTTPProf["HTTP эндпоинты: /debug/pprof/*"]:::entry
        Protobuf["Формирование proto-буфера (profile.proto)"]:::accent
    end

    subgraph Tooling["Инструменты анализа разработчика"]
        CLI["go tool pprof (консольный режим top / list)"]:::process
        WebUI["pprof -http=:8080 (интерактивный Web UI)"]:::success
        FlameGraph["Встроенный интерактивный Flame Graph"]:::success
    end

    RuntimeEngine --> Endpoints --> Tooling
  

Карта типов профилей рантайма

flowchart LR
    classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc

    Root["Профили Go Runtime"]:::entry
    Root --> CPU["CPU Profile: время процессора"]:::process
    Root --> Heap["Heap Profile: inuse_space vs alloc_space"]:::accent
    Root --> Goroutine["Goroutine: полный срез всех стеков"]:::process
    Root --> MutexBlock["Mutex / Block: время ожидания блокировок"]:::warning
    Root --> GLeak["Goroutineleak (1.26-1.27): поиск навечно зависших горутин"]:::success
  

Архитектура черного ящика: runtime/trace.FlightRecorder

Начиная с Go 1.25, FlightRecorder сохраняет скользящее окно событий рантайма в циклический буфер памяти:

flowchart LR
    classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc

    Events["События Go Runtime (Syscall, GC, Scheduler)"]:::process --> RingBuffer["Кольцевой буфер в памяти (N МБ / последние 30 сек)"]:::entry
    RingBuffer -->|"Переполнение"| Discard["Автоматическая перезапись старых событий"]:::accent
    RingBuffer -->|"Событие сбоя / p99 спайк / запрос"| Dump["Мгновенный сброс в файл трейса (trace.out)"]:::success
  

  1. Что такое pprof?
  2. Как снять CPU profile?
  3. Как снять memory profile?
  4. Как снять profile горутин (goroutine profile)?
  5. Как снять profile блокировок (block profile)?
  6. Как снять profile мьютексов (mutex profile)?
  7. Что такое flame graph?
  8. Как найти узкое место (bottleneck)?
  9. Как найти утечку памяти (memory leak)?
  10. Как найти утечку горутин (goroutine leak)?
  11. Что такое трейсинг (tracing)?
  12. Как работает runtime/trace?
  13. Как посмотреть трассировку в go tool trace?
  14. Как отлаживать код с помощью delve?
  15. Как поставить breakpoint, смотреть переменные, стек в delve?
  16. Как работает GODEBUG?
  17. Как включить gctrace и что он показывает?
  18. Как работает schedtrace?
  19. Как включить allocfreetrace?
  20. Как отлаживать с помощью print vs log?
  21. Что такое экспортируемые переменные через expvar?
  22. Как работает net/http/pprof?
  23. Как безопасно открыть pprof endpoint в production?
  24. Как интерпретировать heap profile (inuse, alloc, alloc_space)?
  25. Как снять профиль "разницы" (diff профиль) для поиска утечек?
  26. Как работает trace в части визуализации G, M, P?
  27. Как найти долгие блокировки с помощью block profile?
  28. Как работает runtime.SetBlockProfileRate?
  29. Как работает runtime.SetMutexProfileFraction?
  30. Как отлаживать проблему "too many open files"?
  31. Как отлаживать "high CPU" в продакшене?
  32. Как strace и lsof помогают в отладке Go-приложений?
  33. Как профилировать использование сети (net profile)?
  34. Как настроить pprof для долгоживущих сервисов?
  35. Как работает continuous profiling (например, Pyroscope)?
  36. Что такое execution tracer и чем он отличается от CPU profiler?
  37. Как профилировать аллокации объектов определенного типа?
  38. Как работает debug/elf и debug/macho для анализа бинарных файлов?
  39. Как использовать go tool objdump для дизассемблирования?
  40. Что такое async preemption и как оно отражается в трейсах?
  41. Как найти "false sharing" с помощью профилировщика кэш-миссов?
  42. Как pprof работает с symbolization?
  43. Как настроить сбор профилей в условиях ограниченной памяти?
  44. Что такое "полноценный core dump" и как его проанализировать с delve core?
  45. Как использовать go tool compile -l для отключения инлайнинга при отладке?
  46. Как race detector помогает в отладке?
  47. Как go vet и staticcheck помогают находить потенциальные проблемы?
  48. Как отлаживать зависшие горутины с помощью panic и SIGQUIT?
  49. Как работает gops агент и как его использовать для удаленной отладки?
  50. Как интерпретировать вывод runtime.Stack(buf, true) в продакшене?
  51. Что такое FlightRecorder в Go 1.25?
  52. Что такое профилирование goroutineleak в Go 1.26–1.27?

Что такое pprof?

Краткий ответ

pprof — это встроенный в Go фреймворк для сбора и анализа профилей производительности (CPU, память, горутины, блокировки, мьютексы). Он предоставляет инструментарий для периодического снятия стек-трейсов и визуализации в виде графов, flame-графов и таблиц, что позволяет быстро находить горячие точки и утечки.

Развёрнутый ответ

pprof является частью экосистемы Go и состоит из:

  • Библиотек runtime/pprof (для не-HTTP приложений) и net/http/pprof (автоматическая регистрация эндпоинтов).
  • Протокола записи протобуф-файлов профиля, совместимого с визуализатором pprof (первоначально от Google, теперь github.com/google/pprof).
  • Интеграции с go tool pprof — интерактивным анализатором.

Основные профили:

  • CPU — где процессор тратит время (семплинг с частотой 100 Гц).
  • Heap (memory) — распределение памяти (inuse_space, alloc_space).
  • Goroutine — стек всех горутин.
  • Block — стек вызовов, где горутины блокируются на примитивах синхронизации.
  • Mutex — конкуренция за мьютексы.

Пример запуска CPU-профиля для standalone-приложения:

Go
f, _ := os.Create("cpu.pprof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()

Для HTTP-сервера достаточно импортировать _ "net/http/pprof", и профили будут доступны по /debug/pprof/.

Под капотом (internals)

  • CPU профилирование: runtime управляет сигналом SIGPROF (на Unix). Каждые 10 мс (100 Гц) таймер срабатывает, и обработчик сигнала получает текущий стек горутины, записывая его в буфер профилирования. Используется счетчик runtime.profiletimer и функция runtime.sigprof.
  • Heap: сборщик мусора имеет хуки для записи аллокаций; pprof читает внутренние метрики runtime.MemProfileRecord. При записи профиля runtime.MemProfile опрашивает кэшированные сэмплы аллокаций (mbuckets), хранящиеся в м-структурах (mProfCycle).
  • Goroutine: runtime.Stack вызывается для каждой горутины, собирая текущий стек.
  • Все профили сериализуются в protobuf (profile.proto), поддерживаемый общим инструментом.

Типичные ошибки кандидатов

  • Забыть StopCPUProfile / некорректное закрытие файла: CPU профиль окажется битым или пустым, потому что запишется только заголовок. Нужно обязательно вызвать pprof.StopCPUProfile() и только потом закрывать f.
  • Попытка анализа heap как alloc_space без разбора: путают inuse_space (текущая занятость) с alloc_space (все аллокации с начала), что ведёт к неверным выводам об утечках. Следует явно указать флаг -sample_index=alloc_space при анализе общего объёма аллокаций.
  • Включение профилировщика на продакшене без фильтрации: /debug/pprof/ открыт для всех, раскрывает стеки, что опасно. Нужно ограничивать доступ middleware или отдельным портом с аутентификацией.

Возможные дополнительные вопросы интервьюера

  • Как pprof взаимодействует с планировщиком? Сигнал SIGPROF доставляется потоку, который приостанавливается, и runtime через sigprof собирает pc/sp текущей G, не нарушая её состояние.
  • Можно ли поменять частоту семплирования CPU? Да, через runtime.SetCPUProfileRate(hz), но стандартные 100 Гц — хороший баланс точности и накладных расходов.
  • Чем формат pprof отличается от perf.data? pprof — protobuf с собственным протоколом; фокус на кросс-архитектурную переносимость и интеграцию с веб-UI.

Как снять CPU profile?

Краткий ответ

Через pprof.StartCPUProfile(w) и StopCPUProfile(), либо через эндпоинт /debug/pprof/profile?seconds=30. Профайлер сигнализирует ОС для периодических прерываний (100 Гц) и собирает стек-трейсы, идентифицируя функции, съедающие процессорное время.

Развёрнутый ответ

Для standalone программы:

Go
import "runtime/pprof"

func main() {
    f, err := os.Create("cpu.out")
    if err != nil { log.Fatal(err) }
    defer f.Close()
    if err := pprof.StartCPUProfile(f); err != nil {
        log.Fatal(err)
    }
    defer pprof.StopCPUProfile()
    // ... полезная работа
}

Для HTTP-сервера: импорт _ "net/http/pprof" и запрос GET /debug/pprof/profile?seconds=30 вернёт бинарный профиль. Анализируется go tool pprof -http=:8080 cpu.out.

Профиль содержит сэмплы, снятые с частотой 100 Гц. Это статистический профиль: он показывает, в каких функциях процессор проводит больше всего времени. Не путать с трассировкой.

Под капотом (internals)

runtime.StartCPUProfile создаёт горутину, вызывающую syscall.Setitimer(ITIMER_PROF, ...) или аналогичный таймер, который генерирует SIGPROF. Обработчик сигнала runtime.sigprof получает pc, sp, g, m, и записывает в циклический буфер cpuprof (структура cpuProfile). Горутина-профайлер runtime.cpuProfile читает буфер и кодирует в формат pprof. Частота по умолчанию — 100 Гц. Изменяется runtime.SetCPUProfileRate.

Типичные ошибки кандидатов

  • Не вызывается StopCPUProfile при панике: если программа завершается аварийно до вызова StopCPUProfile, профиль может быть не записан. Используйте defer сразу после запуска.
  • Профилирование слишком короткого интервала: профиль будет почти пустым; нужно не менее 10–30 секунд активной работы.
  • Неправильные ожидания от flat vs cum: кандидат смотрит только на flat (время в самой функции) и игнорирует cum (с учётом вызываемых), теряя общую картину.

Возможные дополнительные вопросы интервьюера

  • Как интерпретировать flat=0, но cum большой? Функция не выполняет работу сама, но вызывает другие затратные функции — кандидат на оптимизацию через кэширование или алгоритмические улучшения.
  • Можно ли профилировать только конкретную горутину? Нет, стандартный CPU профиль глобальный; но можно использовать runtime-трассировку с фильтрацией по G.
  • Влияет ли async preemption на точность CPU профиля? Да, с Go 1.14 стек может быть снят в точке некооперативной вытеснения, что улучшает покрытие tight-циклов.

Как снять memory profile?

Краткий ответ

Используя pprof.WriteHeapProfile(w) или эндпоинт /debug/pprof/heap. Профайлер собирает информацию о текущих (inuse) или всех (alloc) аллокациях по стекам вызовов, позволяя найти источники потребления памяти.

Развёрнутый ответ

Memory profile (heap profile) снимается:

Go
f, _ := os.Create("mem.out")
defer f.Close()
if err := pprof.WriteHeapProfile(f); err != nil {
    log.Fatal(err)
}

При анализе можно указать тип выборки: -sample_index=alloc_space (общий объём аллокаций) или -sample_index=inuse_space (текущий занятый объём). По умолчанию используется inuse_space.

Сборщик мусора регистрирует каждую аллокацию (сэмплируя 1 из runtime.MemProfileRate байт, по умолчанию 512 Кб) и сохраняет стек в профиль. Профиль включает срезы runtime.MemProfileRecord.

Под капотом (internals)

Структура runtime.mheap ведёт учёт через mheap_.specialprofilealloc. При выделении памяти больше порога next_sample (вычисляется на основе MemProfileRate) вызывается mProf_Malloc, которая сохраняет bucket с указателем на стек и счётчиками alloc_bytes, alloc_objects, free_bytes, free_objects. При сборке мусора mProf_Free обновляет free_*. При записи профиля runtime.MemProfile собирает активные bucket и вычисляет inuse = alloc - free.

Типичные ошибки кандидатов

  • Неправильное использование -sample_index: анализируя утечку, смотрят на alloc_space, где показывается кумулятивный объём, хотя виновник может уже быть освобождён; нужно использовать inuse_space для текущей утечки.
  • Игнорирование MemProfileRate: по умолчанию сэмплируется каждая 512 Кб аллокация, мелкие частые аллокации могут быть недопредставлены; для детального анализа можно уменьшить runtime.MemProfileRate = 1 (но осторожно — огромные накладные расходы).
  • Ожидание точности как у Valgrind: профиль статистический, показывает доминирующие стеки, а не каждый байт.

Возможные дополнительные вопросы интервьюера

  • Чем alloc_space отличается от inuse_space и когда что использовать? alloc_space — общий объём всех сделанных аллокаций (полезно для поиска горячих точек выделения), inuse_space — неудалённые объекты на момент снятия (поиск утечек).
  • Как runtime.MemProfileRate влияет на производительность? При =1 ловятся все аллокации, нагрузка на CPU и память профиля велика; стандартное значение — компромисс.
  • Можно ли получить профиль после OOM? Нет, но можно настроить http/pprof и снимать профиль периодически или по сигналу.

Как снять profile горутин (goroutine profile)?

Краткий ответ

Через pprof.Lookup("goroutine").WriteTo(w, 0) или HTTP-эндпоинт /debug/pprof/goroutine?debug=1/2. Он показывает полный стек всех текущих горутин, что помогает диагностировать зависания, утечки горутин и излишнее их создание.

Развёрнутый ответ

Программный вызов:

Go
func printGoroutineStacks() {
    p := pprof.Lookup("goroutine")
    p.WriteTo(os.Stdout, 1) // текстовый формат
}

Опция debug=2 даёт детальный вид со стеками в формате pprof. В отличие от CPU, это не профиль времени, а моментальный снимок состояния всех G. Анализатор показывает количество горутин с одинаковым стеком и строку запуска.

Под капотом (internals)

При вызове WriteTo runtime итерирует список всех горутин из runtime.allgs (защищённый allglock). Для каждой активной G вызывается runtime.Stack (или runtime.callers для отладки=0), формирующий срез pc. Флаг debug=0 генерирует протобуф с агрегированными стеками (как в других профилях).

Типичные ошибки кандидатов

  • Не учитывать системные горутины: в профиле видны GC-воркеры, sysmon и другие, что может сбить с толку. Необходимо фильтровать по известным паттернам.
  • Ошибка интерпретации «растущего числа горутин»: не всегда утечка, возможно, сервис обрабатывает много конкурентных запросов, и горутины завершаются. Нужно проверять накопление во времени.
  • Просто считать горутины без анализа стека блокировки: важно смотреть, что они делают (ожидание канала, мьютекса, сон).

Возможные дополнительные вопросы интервьюера

  • Как отличить живую горутину от зависшей? По состоянию (waiting, runnable) и времени ожидания: debug=2 выводит строку с goroutine X [chan receive, N minutes].
  • Можно ли убить конкретную горутину? Нет, Go не предоставляет API для принудительного завершения отдельной G (только через context или select).
  • Как связано с runtime.NumGoroutine()? Берёт число из того же счётчика runtime.sched.ngsys + n

Как снять profile блокировок (block profile)?

Краткий ответ

Включается вызовом runtime.SetBlockProfileRate(nanoseconds) и снимается через pprof.Lookup("block").WriteTo. Профиль показывает, где горутины блокируются дольше заданного порога, на операциях мьютексов, каналов, сетевых вызовов, time.Sleep и др.

Развёрнутый ответ

Block profile по умолчанию отключён. Для активации:

Go
runtime.SetBlockProfileRate(1) // профилировать все блокировки длительнее 1 нс

Затем после работы программы записывается:

Go
f, _ := os.Create("block.out")
pprof.Lookup("block").WriteTo(f, 0)

Профиль содержит стек, время блокировки и количество. Анализатор go tool pprof может построить граф задержек. Полезен для поиска контеншна на каналах и мьютексах (но для мьютексов лучше специализированный mutex profile).

Под капотом (internals)

В runtime каждая операция блокировки (chan, semacquire, блокировка мьютекса и т.д.) при сохранении события вызывает runtime.blockevent(cycles, skip). Если cycles >= blockprofilerate, происходит сохранение стека в block профиль (аналогично memprofile). Хранится в глобальном хэше bucketshash. Переменная blockprofilerate задаётся SetBlockProfileRate.

Типичные ошибки кандидатов

  • Установка нулевой границы без понимания накладных расходов: SetBlockProfileRate(1) включается на каждый блокировочный вызов, что может замедлить программу на 5-10%. Лучше ставить разумный порог (микросекунды).
  • Путаница с mutex profile: block профиль покрывает все блокировки, включая каналы; для глубокого анализа именно мьютексов используют SetMutexProfileFraction.
  • Забывают выключить перед продакшеном: постоянный сбор block профиля ведёт к ненужной аллокации и замедлению.

Возможные дополнительные вопросы интервьюера

  • Почему block profile не показывает долгие системные вызовы? Потому что syscall не является блокировкой на уровне рантайма; для этого используют трассировку или pprof CPU.
  • Как отфильтровать только блокировки каналов? В полученном профиле по имени функции (runtime.chanrecv, runtime.chansend).
  • Влияет ли GOMAXPROCS на block profile? Косвенно: при меньшем числе P больше горутин будут ожидать, дольше блокировки.

Как снять profile мьютексов (mutex profile)?

Краткий ответ

Включается runtime.SetMutexProfileFraction(rate) и снимается через pprof.Lookup("mutex").WriteTo. Показывает конкуренцию за мьютексы: сколько горутин ожидали и как долго. Позволяет найти горячие точки синхронизации.

Развёрнутый ответ

Мутексный профилировщик фиксирует события разблокировки, если в очереди ожидания были другие горутины. Коэффициент rate задаёт долю событий для записи (например, 1 — каждое, 1000 — ~1 из 1000). Пример:

Go
runtime.SetMutexProfileFraction(1)
// ... работа ...
f, _ := os.Create("mutex.out")
pprof.Lookup("mutex").WriteTo(f, 0)

Профиль хранит два значения: contentions (количество ожиданий) и delay (наносекунды ожидания). Анализатор по умолчанию показывает delay.

Под капотом (internals)

Внутри sync.Mutex при вызове Unlock, если есть ожидающие, выполняется runtime.semrelease, но перед этим вызывается runtime.mutexevent(cycles, skip), который при mutexprofilerate>0 пишет в mutex профиль. Структура профиля аналогична block profile, но события только от sync.Mutex и sync.RWMutex.

Типичные ошибки кандидатов

  • Установка SetMutexProfileFraction на слишком большой rate: rate>0 включает профилирование; если rate=0, профилирование выключено. Нужно ставить 1 или небольшое число.
  • Предположение, что профиль показывает все вызовы Lock: профиль фиксирует только случаи конкуренции, когда другой поток ждал разблокировки.
  • Игнорирование совокупного времени ожидания: важно смотреть не только на отдельные задержки, но и на суммарный delay по стеку.

Возможные дополнительные вопросы интервьюера

  • Чем mutex profile отличается от block profile в аспекте мьютексов? Block profile показывает общее время блокировки, включая случай, когда мьютекс свободен (но тогда блокировки почти нет); mutex profile — только случаи ожидания из-за занятости, даёт более точную картину contention.
  • Как интерпретировать большой delay, но мало contentions? Отдельные операции долго держат мьютекс, возможно, под ним выполняются тяжелые вычисления.
  • Можно ли профилировать RWMutex? Да, учитывается как блокировка на чтение/запись.

Что такое flame graph?

Краткий ответ

Flame graph — это визуализация иерархических данных профилирования, где по горизонтали отображается доля времени/памяти в стеке вызовов, а по вертикали — глубина стека. Прямоугольники соответствуют функциям, их ширина — объём ресурса. Позволяет быстро выявить самые затратные пути выполнения.

Развёрнутый ответ

В экосистеме Go flame graph доступен через go tool pprof -http=:8080, который открывает веб-интерфейс с вкладкой Flame Graph. Каждая полоса — функция, отсортированная по алфавиту слева направо, с цветным кодированием. По горизонтали ширина пропорциональна значению (cpu time, allocations). Клики позволяют зумить в определённый стек.

Альтернативно, утилита github.com/google/pprof умеет генерировать интерактивные flame-графы. В 2017 году Брендан Грегг адаптировал формат для анализа программ. В Go flame graph получил официальную поддержку.

Под капотом (internals)

pprof строит дерево вызовов из протобуф сэмплов. Для flame graph применяется алгоритм «icicle graph», где последовательность фреймов стека рисуется как вложенные прямоугольники. Цвета назначаются автоматически на основе имени функции.

Типичные ошибки кандидатов

  • 2D-трактовка как графа вызовов без вложенности: некоторые думают, что это плоская гистограмма, и упускают, что можно переключаться на контекст родительских функций.
  • Игнорирование cum метрики при сравнении: flame graph CPU по умолчанию базируется на flat, но можно переключить на cum, чтобы видеть общее влияние.
  • Не фильтруют системные вызовы: в огромных проектах платформенные вызовы могут загромождать картинку; нужно использовать focus/ignore в pprof для очистки.

Возможные дополнительные вопросы интервьюера

  • Как сгенерировать flame graph из произвольного pprof профиля? go tool pprof -http=:8080 profile.out или pprof -flame flame.svg profile.out.
  • Чем flame graph лучше обычного дерева? Наглядно показывает «где больно», особенно широкие плоские участки, что ускоряет поиск узких мест.
  • Можно ли построить flame graph по памяти? Да, переключив отображение на inuse_space или alloc_space.

Как найти узкое место (bottleneck)?

Краткий ответ

Комбинируя профили CPU (поиск горячих функций), Memory (избыточные аллокации), Block/Mutex (конкуренция) и трассировку (задержки синхронизации/GC). Методология: измеряем, гипотеза, оптимизируем. Ключевой инструмент — pprof, flame graph, и сравнительный анализ (benchstat).

Развёрнутый ответ

Алгоритм поиска узкого места:

  1. Собрать CPU профиль под нагрузкой — найти функции с высоким flat+cum.
  2. Проверить аллокации — alloc_space для выявления чрезмерного создания объектов; inuse_space для утечек.
  3. Конкуренция — block/mutex профили при высоком параллелизме.
  4. Трассировка — визуализация задержек на синхронизации и GC.
  5. Изоляция — написать бенчмарк с go test -bench и профилированием.
  6. Анализ ассемблера — go tool compile -S или objdump для выявления неэффективных инструкций.

Часто узкое место не является очевидным по flat, требуется смотреть cum — может тормозить вызов в глубине стека. Для определения влияния GC используют GODEBUG=gctrace=1.

Под капотом (internals)

Все профили используют единый механизм сэмплирования стека. Для более точного определения bottleneck можно уменьшить MemProfileRate, BlockProfileRate, но это увеличит оверхед. Профилировщики используют runtime.callers и буферы, не блокируя целевые горутины.

Типичные ошибки кандидатов

  • Преждевременная оптимизация без профиля: догадки «скорее всего, вот здесь тормозит» без доказательств — главная ошибка.
  • Фокус только на CPU, игнорируя ожидания: программа может быть заблокирована вводом-выводом или мьютексами, что CPU профиль не покажет.
  • Игнорирование масштаба: микрооптимизация функции, занимающей 0.1% времени, не даст прироста.

Возможные дополнительные вопросы интервьюера

  • Как отличить CPU-bound от IO-bound? Профиль CPU покажет высокую загрузку; при IO-bound потоки часто в состоянии select/epoll, что видно в goroutine profile или трассировке.
  • Как подтвердить, что устранение узкого места дало эффект? Сравнением бенчмарков до/после с использованием benchstat для оценки значимости.
  • Что делать, если узкое место в стандартной библиотеке? Проверить версию Go, возможно, известная проблема; можно обернуть вызов с минимизацией аллокаций или написать адаптированную реализацию.

Как найти утечку памяти (memory leak)?

Краткий ответ

Снимаем heap profile (inuse_space) несколько раз с интервалом, находим растущие объекты. Используем diff-профили в go tool pprof -base или сравниваем два снимка. Утечка в Go чаще всего вызвана зависшими горутинами, глобальными структурами, забытыми Close() на ресурсах или циклическими ссылками, удерживаемыми в слайсах/мапах.

Развёрнутый ответ

Методика:

  1. Запустить сервис под нагрузкой, записать heap1.
  2. Подождать стабилизации или длительной работы, записать heap2.
  3. Анализировать go tool pprof -base=heap1 heap2, показывает дельту (alloc_space и inuse_space). Растущие стеки указывают на утечку.
  4. Если утечка не в inuse, а в виртуальной памяти, проверить GODEBUG=gctrace=1 — возможно, GC не возвращает память ОС из-за MADV_FREE (Linux) или настройки GOGC.

Также помогают goroutine profile (утечка горутин часто = утечка памяти) и debug.FreeOSMemory() для диагностики.

Под капотом (internals)

Распределитель памяти Go выделяет блоки через mheap и освобождает через mcentral. Утечки происходят, когда указатель на объект остаётся живым из-за достижимости из корней (стек, глобалы). GC использует трёхцветный алгоритм: если объект не помечен как мусор, память не возвращается. Профиль отражает срезы живых объектов; рост inuse_space означает, что объекты не освобождаются.

Типичные ошибки кандидатов

  • Смотреть только на alloc_space: он кумулятивный и всегда растёт, не показывая утечку. Нужно использовать inuse_space и diff.
  • Не учитывать фрагментацию: память может быть освобождена, но не возвращена ОС из-за scavenger; runtime.MemStats.HeapIdle - HeapReleased покажет разницу.
  • Искать утечку только в объектах, забывая о горутинах: горутина с блокирующим каналом удерживает стек (минимум 2 Кб+), что эквивалентно утечке.

Возможные дополнительные вопросы интервьюера

  • Как найти точное место, где переменная перестаёт быть нужной, но не удаляется? Использовать утилиту view-alloc и pprof -list для анализа кода, либо точечные профили с runtime.ReadMemStats.
  • Может ли sync.Pool вызвать утечку? Может, если пул накапливает большие объекты и не очищается; но это не утечка в классическом смысле, а избыточное удержание.
  • Как MADV_FREE vs MADV_DONTNEED влияет на метрики RSS? MADV_FREE не сразу уменьшает RSS, создавая иллюзию утечки в top.

Как найти утечку горутин (goroutine leak)?

Краткий ответ

Снимаем goroutine profile несколько раз, ищем стеки с постоянно увеличивающимся количеством. Признаки: горутины зависли на chan receive/send, select, sync.Mutex.Lock без соответсвующего отправителя/получателя или контекста. Инструменты: pprof.Lookup("goroutine"), runtime.NumGoroutine, трассировка.

Развёрнутый ответ

Мониторинг: expvar публикует NumGoroutine. При обнаружении неограниченного роста:

  • curl http://server/debug/pprof/goroutine?debug=2 > goroutines.txt
  • Анализируем стеки. Большое количество горутин в состоянии [chan receive] на одном канале — вероятно, отсутствует писатель.
  • Используем runtime.Stack в коде для выборочной печати.

Трассировка помогает увидеть создание горутин и их жизненный цикл. Важно использовать context.Context для отмены горутин; утечка часто возникает из-за горутин, запущенных без механизма остановки.

Под капотом (internals)

Структура g содержит atomicstatus, waitreason (например, chan receive). Планировщик хранит все G в глобальном слайсе allgs. При утечке множество G переходит в состояние Gwaiting и никогда не завершается. Стек G освобождается только при её завершении (goexit), поэтому память стека не возвращается, вызывая утечку.

Типичные ошибки кандидатов

  • Анализ по абсолютному числу, а не по тренду: при росте нагрузки число горутин может расти и падать. Утечка — монотонный рост на холостом ходу.
  • Не проверяют, что канал закрыт, а отправка в него продолжается: это паника (send on closed channel), но часто горутина просто блокируется на отправке без закрытия.
  • Забывают wg.Wait() в тестах: горутины не завершаются, тест проходит, но детектор гонок может не сработать; нужно использовать leaktest библиотеки.

Возможные дополнительные вопросы интервьюера

  • Как заблокировать создание новых горутин, чтобы избежать утечки? Паттерн ограничения через семафор (buffered chan) или пул воркеров; нет встроенного механизма, только структура кода.
  • Может ли горутина утечь без блокировки? Да, если она в бесконечном цикле без context.Done() и не завершается — утечка вычислительных ресурсов.
  • Что покажет debug=2? Кроме стека, покажет состояние блокировки и время ожидания: [chan receive, 23 minutes].

Что такое трейсинг (tracing)?

Краткий ответ

Трейсинг — это запись событий выполнения программы во времени (syscalls, GC, планировщик, сеть) для анализа производительности с высоким временным разрешением. В Go это runtime/trace, который фиксирует события горутин, процессов, планировщика, позволяя выявить задержки синхронизации и GC-паузы.

Развёрнутый ответ

Трейсинг принципиально отличается от профилирования: профилировщик агрегирует данные за период, трейсинг даёт временную шкалу. runtime/trace записывает бинарный лог, который просматривается go tool trace. Полезно для:

  • Поиска причин «хвостовых» задержек.
  • Визуализации переключений контекста горутин.
  • Анализа, почему горутина не могла запуститься (занятость P, blocked syscalls).
  • Понимание паттернов работы GC.

Пример запуска:

Go
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()

Под капотом (internals)

Трейсер встроен в рантайм: каждая значимая операция (создание G, блокировка, syscall, GC-фазы) вызывает traceEvent. События записываются в буфер, асинхронно сбрасываемый в выходной поток. Формат основан на хронологии с метками времени, идентификаторами потоков и горутин. Визуализация go tool trace восстанавливает состояния.

Типичные ошибки кандидатов

  • Путать с CPU профилем: трейсинг показывает, когда произошло событие, а не где потрачено время; он дополняет профилировщик.
  • Не учитывать оверхед: запись всех событий может замедлить выполнение на 10-20%, искажая картину.
  • Не анализировать GC-метки: в трейсе видны все фазы GC; игнорируя их, можно пропустить причину задержек.

Возможные дополнительные вопросы интервьюера

  • Что такое «асинхронная предвытеснение» и как оно видно в трейсе? Видно как события traceEvGoPreempt без кооперативных точек.
  • Как трейсер обрабатывает потерянные события? Использует циклический буфер; при переполнении счётчик сброса, видимый в go tool trace.
  • Можно ли включить трейсинг на отдельном участке кода? Да, через trace.StartRegion(ctx, "name") и trace.WithRegion.

Как работает runtime/trace?

Краткий ответ

Пакет runtime/trace включает глобальный трейсер, который подключается к runtime-хукам и записывает события в буфер. Он предоставляет функции Start/Stop для захвата данных о горутинах, планировщике, GC, сети и пользовательских регионах.

Развёрнутый ответ

Использование:

Go
import "runtime/trace"

func main() {
    f, _ := os.Create("trace.out")
    defer f.Close()
    trace.Start(f)
    defer trace.Stop()
    // программа
}

Можно добавлять пользовательские задачи и регионы:

Go
ctx, task := trace.NewTask(context.Background(), "process")
defer task.End()
region := trace.StartRegion(ctx, "compute")
compute()
region.End()

Все события записываются в бинарный поток, который включает заголовок версии, события с временными метками (в наносекундах).

Под капотом (internals)

Главная структура — runtime.traceBuf, цепочка буферов, заполняемых атомарно. Каждый P имеет локальный буфер для уменьшения конкуренции. События кодируются с помощью traceEvent (тип, timestamp, args). runtime.traceStart инициализирует глобальный трейсер и регистрирует хуки на GC-циклы, sysmon, планировщик. При переполнении буфера событие теряется, но счётчик потерь увеличивается. Формат описан в src/runtime/trace.go.

Типичные ошибки кандидатов

  • Останавливают трейс до того, как все события записаны: после trace.Stop() нужно дождаться, чтобы все буферы были сброшены, иначе лог обрезан. Лучше использовать defer.
  • Создание трейса на длительное время без ротации: файл может вырасти до гигабайт; на продакшене включают на короткие промежутки.
  • Не фильтруют события при анализе: go tool trace позволяет фокусироваться на конкретной горутине или временном диапазоне.

Возможные дополнительные вопросы интервьюера

  • Как трейсер обрабатывает переключение потоков? Каждый M привязывается к событиям через trace.acquirem, записываются переходы GoStart/GoStop.
  • Почему трейс показывает фиктивные горутины? Это GC worker и sysmon; они маркируются специальными ID.
  • Можно ли программно парсить трейс? Да, через internal/trace (неэкспортируемый), но для внешнего использования есть сторонние парсеры.

Как посмотреть трассировку в go tool trace?

Краткий ответ

Команда go tool trace trace.out запускает веб-сервер с интерактивным интерфейсом, открывающимся в браузере. Основные вкладки: «View trace» (временная диаграмма горутин и процессов), «Goroutine analysis», «Network blocking profile», «Scheduler latency profile», «Minimum mutator utilization» и другие.

Развёрнутый ответ

После генерации trace.out выполнить:

TEXT
go tool trace trace.out

Откроется HTTP-сервер (по умолчанию на случайном порту). В браузере будет список ссылок:

  • View trace: визуализация временной шкалы: строки для каждого P, на них горутины (G) в виде цветных полос (running, runnable, syscall). Можно зумить, смотреть события GC, сетевые опросы.
  • Goroutine analysis: группировка горутин по имени, их количество, время выполнения, диапазоны задержек.
  • Network/Sync/Syscall blocking profile: какие вызовы вызывают блокировки.
  • Minimum mutator utilization: график доли времени, когда процессор выполняет полезную работу (исключая GC и блокировки).

Под капотом (internals)

go tool trace использует пакет internal/trace для парсинга бинарного файла и генерации HTML/JSON данных. JS-код визуализации основан на Trace-Viewer (Catapult project). Диаграмма синхронизации восстанавливает состояния G/M/P посредством анализа событий traceEvGoStart, traceEvGoStop, traceEvGoBlock, traceEvGoUnblock. Точность наносекундная.

Типичные ошибки кандидатов

  • Открытие трейса без -http: путают с go tool pprof -http, думая, что go tool trace выводит ту же визуализацию; нет, это отдельный UI.
  • Паника при отсутствии трассировочного файла: go tool trace требует файл, созданный runtime/trace.
  • Анализ только View trace без Goroutine analysis: упускается полезная статистика по каждой горутине.

Возможные дополнительные вопросы интервьюера

  • Как экспортировать определённый фрагмент трейса? Через curl к конкретному URL, например /trace?start=...&end=... (не напрямую, но можно использовать gotrace утилиты).
  • Можно ли сравнить два трейса? Родного инструмента нет, но можно выгрузить JSON и сравнить скриптами.
  • Что означает пробел на дорожке P в View trace? P простаивает — либо нет готовых горутин, либо занят в блокирующем syscall.

Как отлаживать код с помощью delve?

Краткий ответ

delve — интерактивный отладчик для Go, поддерживающий точки останова, пошаговое выполнение, просмотр переменных, стеков, горутин. Запускается через dlv debug или dlv attach к работающему процессу, обеспечивает глубокую интеграцию с символами и планировщиком Go.

Развёрнутый ответ

Установка: go install github.com/go-delve/delve/cmd/dlv@latest. Пример сессии:

TEXT
dlv debug main.go
(dlv) break main.main
(dlv) continue
(dlv) next
(dlv) print variable

Поддерживает команды:

  • break / clear
  • step, next, stepout
  • goroutines — список всех горутин, goroutine <id> frame ...
  • stack, frame
  • sources, funcs
  • cond <breakpoint> <expr> — условные брейкпоинты.
    Для production можно использовать dlv attach <pid>, после чего программа останавливается до продолжения.

Под капотом (internals)

Delve использует системные вызовы (ptrace на Linux, Mach на macOS) для контроля исполнения и чтения памяти. Он читает DWARF-информацию из исполняемого файла для определения типов и местоположений переменных. Для Go-специфичных фич парсит runtime-структуры (g, m, allgs) для итерации горутин. Для установки брейкпоинтов заменяет инструкцию на INT3 и обрабатывает сигнал SIGTRAP.

Типичные ошибки кандидатов

  • Не включают -gcflags="-N -l" при сборке: отключение оптимизаций и инлайнинга критично для корректного отображения переменных и step-by-step.
  • Пытаются отладить горутину, не переключив контекст: нужно goroutines, выбрать ID, затем frame команды.
  • Игнорирование headless режима для CI: delve поддерживает --headless --listen=:2345, полезно для удаленной отладки.

Возможные дополнительные вопросы интервьюера

  • Как посмотреть все локальные переменные в текущем фрейме? locals.
  • Можно ли менять значения переменных на лету? Да, через set variable = value (экспериментально, может требовать отключения оптимизаций).
  • Как отлаживать core dump? dlv core <binary> <core> — анализирует посмертный дамп.

Как поставить breakpoint, смотреть переменные, стек в delve?

Краткий ответ

break <location> (имя функции, файл:строка) ставит точку останова; continue запускает выполнение до брейкпоинта; print <var> / locals показывает переменные; stack выводит стек вызовов. Эти базовые действия обеспечивают полный контроль над состоянием программы.

Развёрнутый ответ

Пример сессии:

TEXT
(dlv) break main.go:42
(dlv) continue
(dlv) print myStruct.Field
(dlv) locals
(dlv) stack 10
(dlv) goroutine 5

Команда frame позволяет перемещаться по стеку. args показывает аргументы текущей функции. Условные брейкпоинты: break myFunc -cond 'i > 10'. Также есть trace (tracepoint) — печатает сообщение без остановки.

Под капотом (internals)

При установке брейка delve записывает INT 3 по адресу функции/строки (используя DWARF line table). При срабатывании ОС доставляет SIGTRAP, delve перехватывает, восстанавливает оригинальную инструкцию, парсит стек и переменные. Вычисление выражений реализовано через интерпретатор с пониманием Go-типов, используя DWARF для маппинга регистров и стека.

Типичные ошибки кандидатов

  • Ставят брейкпоинты на несуществующие строки: оптимизация может объединить строки; всегда компилировать с -gcflags="all=-N -l".
  • Попытка напечатать интерфейсное значение напрямую: нужно разыменовать до конкретного типа (print x.(type)) или использовать whatis x.
  • Не используют config alias: сессию можно ускорить, задав сокращения.

Возможные дополнительные вопросы интервьюера

  • Можно ли ставить брейкпоинт на системный вызов? Нет, delve работает на уровне Go- кода, не поддерживает ядерные трассировки (но можно использовать stap).
  • Как отобразить значение указателя как срез? print *(*[10]int)(ptr) — ручное приведение типов.
  • Какой флаг отключает инлайнинг в тестах? go test -gcflags="-l".

Как работает GODEBUG?

Краткий ответ

GODEBUG — переменная окружения, управляющая выводом отладочной информации рантаймом Go. Через пары ключ=значение можно включить трассировку сборщика мусора (gctrace), планировщика (schedtrace), аллокатора (allocfreetrace) и другие, помогая быстро диагностировать проблемы без изменения кода.

Развёрнутый ответ

Пример: GODEBUG=gctrace=1,schedtrace=1000 ./myapp. Это печатает в stderr информацию о каждом цикле GC и каждую секунду снимок планировщика. Основные параметры:

  • gctrace=1 — логи GC.
  • schedtrace=X — состояние планировщика каждые X миллисекунд.
  • scheddetail=1 — детализация вместе с schedtrace.
  • allocfreetrace=1 — трейс каждой аллокации и освобождения.
  • scavtrace=1 — возврат памяти ОС.
  • asyncpreemptoff=1 — отключает асинхронную вытеснение.
  • cgocheck=2 — усиленная проверка cgo вызовов.

Под капотом (internals)

В функции runtime.schedinit вызывается runtime.parsedebugvars, парсящая GODEBUG и устанавливающая глобальные переменные: debug.gctrace, debug.schedtrace, debug.allocfreetrace и т.д. Эти флаги затем проверяются в соответствующих компонентах (например, gcStart печатает строку при debug.gctrace > 0).

Типичные ошибки кандидатов

  • Включение allocfreetrace=1 на продакшене: печатает каждую аллокацию/освобождение — катастрофически снижает производительность и забивает stderr.
  • Путают GODEBUG с GOGC: GODEBUG=gctrace=1 только выводит логи, не меняет поведение GC (что делает GOGC).
  • Не учитывают формат вывода gctrace: неправильно интерпретируют числа, не зная, где смотреть суммарное время паузы.

Возможные дополнительные вопросы интервьюера

  • Как включить gctrace только для отдельных этапов? Нельзя, включение печатает все циклы; для фильтрации парсят логи.
  • Может ли GODEBUG замедлить программу без трассировки? Только при включении verbose опций, например allocfreetrace.
  • Как отключить асинхронное вытеснение для отладки? GODEBUG=asyncpreemptoff=1.

Как включить gctrace и что он показывает?

Краткий ответ

Установить GODEBUG=gctrace=1. В stderr печатается строка на каждый цикл GC, содержащая номер цикла, процент освобождённой памяти, продолжительность фаз, финальную кучу и цель. Помогает оценить частоту и длительность сборок, эффективность сборщика.

Развёрнутый ответ

Пример строки:

TEXT
gc 1 @0.008s 2%: 0.018+0.23+0.003 ms clock, 0.036+0.15/0.12/0.076+0.006 ms cpu, 4->4->1 MB, 5 MB goal, 4 P

Расшифровка:

  • gc 1 — номер цикла.
  • @0.008s — время с запуска.
  • 2% — доля времени CPU, затраченного на GC с последнего цикла.
  • 0.018+0.23+0.003 ms clock — стены: STW mark root, concurrent mark, STW sweep (с Go 1.5 sweep конкурирует, но пишется).
  • 4->4->1 MB — размер кучи до GC, после GC, живых объектов.
  • 5 MB goal — целевой размер для следующего запуска.
  • 4 P — число процессоров.

Под капотом (internals)

Вывод генерируется в runtime.gcTraceDone() после завершения цикла. Данные берутся из work структуры (runtime.gcController). heapGoal вычисляется на основе GOGC. Проценты вычисляются из memstats.heap_live.

Типичные ошибки кандидатов

  • Считать, что STW sweep всё ещё существует: начиная с Go 1.5 sweep выполняется асинхронно и не вызывает значительных пауз; часть STW осталась только для mark termination (кратковременная).
  • Игнорировать cpu метрику: clock time vs cpu time показывает параллелизм.
  • Не сопоставлять рост heap goal и реальное потребление: при высокой скорости аллокаций goal отстаёт, вызывая частые GC.

Возможные дополнительные вопросы интервьюера

  • Что значит 2% и как его уменьшить? Процент времени, проведённого в GC; уменьшить можно снижением аллокаций или увеличением GOGC.
  • Как узнать, что GC не справляется? Если heap_live постоянно превышает heap_goal и циклы идут непрерывно — требуется настройка.
  • Какая фаза занимает больше всего времени в gctrace? Обычно concurrent mark, но может быть и sweep для больших куч.

Как работает schedtrace?

Краткий ответ

GODEBUG=schedtrace=X заставляет рантайм печатать состояние планировщика каждые X миллисекунд: число горутин, потоков, очередей, статус m, p. Полезна для диагностики загруженности, блокировок и миграции горутин.

Развёрнутый ответ

Вывод:

TEXT
SCHED 1000ms: gomaxprocs=4 idleprocs=2 threads=9 spinningthreads=1 idlethreads=2 runqueue=0 [1 2 0 0]
  • gomaxprocs — число P.
  • idleprocs — число простаивающих P.
  • threads — общее количество M.
  • spinningthreads — M в активном поиске работы (spin).
  • idlethreads — M в ожидании.
  • runqueue — глобальная очередь G, затем локальные очереди по P.

Состояние показывает, есть ли дисбаланс или голодание потоков.

Под капотом (internals)

runtime.schedtrace — функция, вызываемая по истечении таймера из sysmon. Она блокирует sched.lock, собирает данные из полей sched и allp, и выводит через print. Использует printint, printstr.

Типичные ошибки кандидатов

  • Интерпретация runqueue=0 как простой: локальные очереди могут быть заполнены, это не глобальная очередь.
  • Не смотрят на spinningthreads: указывает на то, что потоки активно пытаются украсть работу; высокое значение — нормально под нагрузкой.
  • Считать idleprocs признаком проблемы: при IO-bound нагрузке много P могут простаивать.

Возможные дополнительные вопросы интервьюера

  • Как увидеть детали планировщика с каждым P? GODEBUG=scheddetail=1 вместе с schedtrace.
  • Что значит runqueue=0 [0 0 0 0] при высокой нагрузке? Вероятно, все горутины заблокированы на сетевых вызовах или мьютексах, смотреть block профиль.
  • Можно ли программно вызвать вывод? Через runtime.Gosched() нет; можно использовать debug.FreeOSMemory или экспортируемые переменные, но лучше expvar.

Как включить allocfreetrace?

Краткий ответ

Установкой переменной окружения GODEBUG=allocfreetrace=1. Тогда runtime будет выводить на stderr трейс каждого выделения и освобождения памяти: адрес, размер, стек вызовов. Чрезвычайно подробно, только для локальной отладки небольших программ.

Развёрнутый ответ

Использование:

TEXT
GODEBUG=allocfreetrace=1 ./program 2>&1 | head

Каждая строка содержит tracealloc или tracefree, указатель, размер, и стек. Позволяет найти точное место, где выделяется объект, и кто его освобождает (часто free без стека из-за того, что GC освобождает без контекста — показывается как tracefree с пометкой gc).

Это экстремально детальный инструмент, так как вывод колоссальный и замедляет программу в сотни раз.

Под капотом (internals)

Включение устанавливает debug.allocfreetrace = 1. В функциях runtime.mallocgc (перед выделением) и в freeSpan/gcSweep (при освобождении) вызывается tracealloc/tracefree, которые печатают с использованием print и callers для получения стека. Работает на очень низком уровне, до того, как объект может быть перемещён.

Типичные ошибки кандидатов

  • Запуск на продакшене или под нагрузкой: приведёт к падению производительности и заполнению дисков.
  • Полное доверие стеку tracefree: в случае GC tracefree печатает стек той горутины, которая вызвала GC, а не той, которая аллоцировала — может сбивать с толку.
  • Не фильтровать вывод: обязательно использовать grep по адресу для отслеживания жизни объекта.

Возможные дополнительные вопросы интервьюера

  • Как узнать, кто освободил объект, если освобождение от GC? Никак, GC не сохраняет контекст; можно использовать финализатор runtime.SetFinalizer для отладки.
  • Можно ли включить только для определённого класса объектов? Нет, глобально; для целевой отладки используют pprof или инструменты типа asan.
  • Влияет ли на escape analysis? Нет, только добавляет логирование.

Как отлаживать с помощью print vs log?

Краткий ответ

print/println — встроенные функции для вывода в stderr без форматирования, подходят для быстрой отладки. log пакет предоставляет форматирование, уровни, временные метки и настраиваемый вывод. Для продакшен-отладки предпочтительнее структурированное логирование с уровнями.

Развёрнутый ответ

println("var:", var) — не требуют импортов, работают даже до инициализации os.Stderr (важно в ранних стадиях загрузки). Используются в ядре runtime. Для нормальной отладки лучше log.Printf, т.к. даёт временную метку и возможность писать в разные io.Writer (файл, syslog). Однако для конвейерной обработки в production полный fmt.Printf может создавать лишние аллокации. В современных проектах применяют slog (структурное логирование) с уровнями.

При поиске утечек горутин часто используют log.Println("goroutine started/stopped") вручную.

Под капотом (internals)

print/println реализованы в компиляторе как встроенные, используют runtime.printlock и напрямую пишут в fd 2. Они безопасны для вызова из обработчиков сигналов. log пакет надстраивается над fmt, использует мьютекс для атомарности вывода.

Типичные ошибки кандидатов

  • Забыть удалить println в production коде: они засоряют stderr и не фильтруются; линтеры могут это отловить, но не всегда.
  • Использовать println для отладки конкурентных программ без защиты: выводы могут перемежаться, усложняя анализ.
  • Полагать, что fmt.Println атомарен: он не атомарен, строки могут разрываться; log гарантирует атомарность.

Возможные дополнительные вопросы интервьюера

  • Можно ли перенаправить print в файл? Только на уровне ОС (dup stderr), но лучше использовать log.SetOutput.
  • Что использовать для высоконагруженного сервиса? Структурное логирование с асинхронной записью (zap, zerolog).
  • Почему print не требует импорта? Это builtin, компилятор знает о нём.

Что такое экспортируемые переменные через expvar?

Краткий ответ

Пакет expvar предоставляет стандартизированный способ публикации внутренних метрик приложения (счетчики, гистограммы) в формате JSON через HTTP-эндпоинт /debug/vars. Позволяет оперативно мониторить состояние без изменения кода.

Развёрнутый ответ

Публикация:

Go
import "expvar"

var hits = expvar.NewInt("hits")
hits.Add(1)

// Регистрация своего типа
type stats struct{ ... }
expvar.Publish("custom", expvar.Func(func() interface{} { return collectStats() }))

Эндпоинт выводит все зарегистрированные переменные, включая стандартные: cmdline, memstats, num_goroutine. Можно использовать для мониторинга через Prometheus или простой curl.

Под капотом (internals)

При импорте expvar регистрируется HTTP-обработчик (если используется http.DefaultServeMux, можно через expvar.Handler()). memstats обновляется при вызове runtime.ReadMemStats (кэшируется). Горутина runtime.updateMstats периодически обновляет runtime.memstats. Публикация сериализует в JSON с помощью json.Encoder, обходя структуры с учётом интерфейса Var.

Типичные ошибки кандидатов

  • Прямой вызов runtime.ReadMemStats на каждый запрос: вызывает STW-паузу для сбора точной статистики, замедляя приложение; лучше полагаться на встроенный expvar с дефолтным кэшированием (~5 с).
  • Регистрация переменных без init(): паника при дублировании ключа.
  • Добавление чувствительных данных: /debug/vars может быть доступен всем; не публиковать пароли, ключи.

Возможные дополнительные вопросы интервьюера

  • Как экспортировать expvar на другой порт? Создать отдельный http.Server с expvar.Handler().
  • Чем отличается от метрик Prometheus? Prometheus — pull-модель с гистограммами; expvar — просто JSON, менее мощный, но встроенный.
  • Как кастомизировать формат? Имплементировать интерфейс Var (метод String()) и опубликовать.

Как работает net/http/pprof?

Краткий ответ

Импорт _ "net/http/pprof" автоматически регистрирует маршруты /debug/pprof/ с обработчиками для CPU, heap, goroutine и других профилей через HTTP. Это самый простой способ добавить конечные точки профилирования в веб-сервер.

Развёрнутый ответ

Достаточно импортировать:

Go
import _ "net/http/pprof"

Если используется кастомный роутер (не DefaultServeMux), можно вручную зарегистрировать:

Go
r.HandleFunc("/debug/pprof/", pprof.Index)
r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
r.HandleFunc("/debug/pprof/profile", pprof.Profile)
// и т.д.

Эндпоинты:

  • /debug/pprof/ — список доступных профилей.
  • /debug/pprof/profile?seconds=30 — CPU профиль.
  • /debug/pprof/heap — memory.
  • /debug/pprof/goroutine — все горутины.

Под капотом (internals)

Функции-обработчики в net/http/pprof оборачивают вызовы runtime/pprof. Например, pprof.Profile запускает pprof.StartCPUProfile, ждёт указанное количество секунд, затем StopCPUProfile, и пишет в ResponseWriter. Поддерживается сжатие. Данные передаются напрямую в поток.

Типичные ошибки кандидатов

  • Открытие эндпоинтов на публичном интерфейсе без аутентификации: любой может загрузить профиль, содержащий стеки и потенциально чувствительную информацию.
  • Изменять дефолтные маршруты бездумно: при использовании http.NewServeMux() нужно регистрировать все /debug/pprof/*.
  • Забыть про таймауты: запрос profile может висеть целые seconds, блокируя горутину сервера.

Возможные дополнительные вопросы интервьюера

  • Можно ли отключить какой-то профиль? Да, не регистрируя соответствующий обработчик.
  • Как ограничить доступ? Обернуть обработчики middleware с проверкой IP или токена.
  • Как использовать с TLS? Как обычно, защита транспортного уровня решается на уровне сервера.

Как безопасно открыть pprof endpoint в production?

Краткий ответ

Слушать отдельный порт на loopback-интерфейсе с аутентификацией и/или обёрткой middleware, запрещать доступ извне. Альтернативно — использовать Unix-сокет с правами доступа, или доступ по VPN/SSH-туннелю.

Развёрнутый ответ

Паттерн:

Go
adminMux := http.NewServeMux()
adminMux.HandleFunc("/debug/pprof/", pprof.Index)
// ... другие хендлеры
admin := &http.Server{
    Addr:    "127.0.0.1:6060",
    Handler: authMiddleware(adminMux),
}
go admin.ListenAndServe()

Аутентификация: HTTP Basic Auth, токен, или mTLS. Также можно открыть только через SSH-туннель: ssh -L 6060:localhost:6060 user@host.

Никогда не выставлять /debug/pprof на 0.0.0.0 без защиты — это прямой путь к утечке данных и потенциальной DoS.

Под капотом (internals)

Не касается runtime, только конфигурация HTTP. Обработчики профилей не потребляют значительных ресурсов при простое, но CPU-профиль может нагрузить процессор на время съёмки.

Типичные ошибки кандидатов

  • Открытие на всех интерфейсах с надеждой, что никто не найдёт: порты автоматически обнаруживаются сканерами.
  • Использование pprof на основном порту с продакшен-трафиком: профиль может занять всю полосу пропускания при скачивании.
  • Нет rate limiting: злоумышленник может запросить profile?seconds=10 много раз, создав нагрузку.

Возможные дополнительные вопросы интервьюера

  • Как организовать сбор профилей без открытия порта? Запустить отдельную горутину, которая пишет профиль в файл по SIGHUP или по таймеру.
  • Можно ли использовать Unix-сокет? Да, net.Listen("unix", "/tmp/pprof.sock") с правами 0600.
  • Как интегрировать с Kubernetes? Порт объявляется как containerPort, доступ ограничивается NetworkPolicy или sidecar-прокси.

Как интерпретировать heap profile (inuse, alloc, alloc_space)?

Краткий ответ

inuse_space — объем памяти, занятой живыми объектами на момент снимка. alloc_space — совокупный объём всех выделений за время работы (включая освобождённые). inuse_objects/alloc_objects аналогично по количеству объектов. Выбор зависит от задачи: утечки — inuse_space, горячие аллокации — alloc_space.

Развёрнутый ответ

При открытии heap профиля в pprof можно переключать Sample: inuse_space, inuse_objects, alloc_space, alloc_objects. Вкладка Graph или Flame отражает выбранную метрику. alloc_space показывает, сколько байт было выделено через конкретный стек, даже если они уже освобождены. Это помогает найти функции, которые порождают много мусора.

Пример: если alloc_space показывает гигабайты в json.Marshal, но inuse_space мало — это не утечка, а высокая аллокационная нагрузка.

Под капотом (internals)

Профиль хранит для каждого bucket счётчики AllocBytes, AllocObjects, FreeBytes, FreeObjects. inuse_space = AllocBytes - FreeBytes. При записи профиля с флагом debug=0 в протобуф добавляются все четыре метрики как сэмплы с разными sample_type, что позволяет переключаться в pprof.

Типичные ошибки кандидатов

  • Путают alloc_space с текущей утилизацией: видят 10 ГБ alloc_space и думают, что утечка; нужно посмотреть inuse_space.
  • Не обращают внимания на inuse_objects: иногда утечка в большом количестве мелких объектов выявляется по числу, а не байтам.
  • Игнорируют -base диффы: сравнивают два профиля без -base и делают неверные выводы.

Возможные дополнительные вопросы интервьюера

  • Как gc учитывается в профиле? При inuse учитываются объекты, которые ещё не собраны сборщиком; после GC inuse падает.
  • Что такое alloc_space до GC? Он не меняется, это накопительный счётчик с запуска программы.
  • Как смотреть память, занятую стеками горутин? Отдельного профиля нет; используют goroutine профиль или runtime.Stack.

Как снять профиль "разницы" (diff профиль) для поиска утечек?

Краткий ответ

Снимаем два heap профиля с интервалом, затем анализируем go tool pprof -base=old.prof new.prof. Diff показывает прирост inuse_space (или alloc_space) по стекам, выявляя объекты, которые не освобождаются. Это ключевой метод поиска утечек памяти.

Развёрнутый ответ

Процесс:

  1. Прогреть приложение под нагрузкой, записать heap1.out.
  2. Подождать некоторое время (секунды/минуты), записать heap2.out.
  3. go tool pprof -base=heap1.out heap2.out
  4. В режиме top или list смотрим положительную дельту inuse_space.
    Растущие стеки — кандидаты на утечку. Также можно использовать -diff_base для сравнения двух профилей в визуальном режиме.

Для утечек горутин аналогично: go tool pprof -base=g1.out g2.out.

Под капотом (internals)

pprof вычитает значения соответствующих стека из base из нового профиля; результирующий профиль содержит только изменившиеся (положительные или отрицательные) локации. Алгоритм работает на уровне протобуф, матчинг производится по идентификатору функции и линии.

Типичные ошибки кандидатов

  • Сравнивать профили, снятые при разной нагрузке: количество аллокаций может меняться, необходимо стабильное состояние.
  • Использовать alloc_space вместо inuse_space: diff alloc_space почти всегда положителен, что зашумляет поиск утечек.
  • Не дожидаться стабилизации GC: после первого профиля может произойти несколько GC, и второй профиль будет меньше; нужно ждать steady state.

Возможные дополнительные вопросы интервьюера

  • Как исключить GC-эффект из diff? Можно вручную вызвать runtime.GC() перед каждым снимком.
  • Можно ли diff-ить CPU профили? Нет смысла, так как они временные; сравнивают через бенчмарки.
  • Что делать, если diff показывает отрицательный рост? Значит, память освободилась; это нормально, но неинтересно для поиска утечек.

Как работает trace в части визуализации G, M, P?

Краткий ответ

go tool trace визуализирует события планировщика: на временной шкале каждая дорожка соответствует P, а цветные блоки — горутинам (G) в разных состояниях (running, runnable, syscall). Потоки (M) неявно видны по миграции горутин между P и привязке к syscall.

Развёрнутый ответ

В веб-интерфейсе View trace:

  • Вертикальная ось: несколько строк (обычно по количеству GOMAXPROCS). Каждая строка — это P (логический процессор).
  • Цветные полосы на строке — горутины, выполняющиеся на этом P. Цвета кодируют состояние:
    • Зелёный — running.
    • Оранжевый — runnable (в очереди, ожидающая).
    • Синий — syscall (блокирующий).
    • Красный — блокировка на синхронизации/GC.
  • Интерактивно можно выделить конкретную горутину по ID и отследить её путь.

В разделе «Goroutine analysis» показано распределение времени по состояниям.

Под капотом (internals)

События traceEvGoStart, traceEvGoStop, traceEvGoBlock, traceEvGoUnblock, traceEvGoSysCall записывают переходы с временными метками. go tool trace парсит лог и строит timeline, восстанавливая связь G ↔ P по событиям GoStart с указанием P ID. M идентифицируются, но визуализируются опционально.

Типичные ошибки кандидатов

  • Путать строку с M: в классическом trace-viewer строки соответствуют P, а не M; это непривычно для тех, кто знаком с потоками ОС.
  • Игнорировать idle-периоды: пустое пространство на дорожке говорит о простое P из-за отсутствия работы.
  • Не использовать «flow events»: они показывают передачу объектов между горутинами, важны для отладки каналов.

Возможные дополнительные вопросы интервьюера

  • Как увидеть кражу работы (work-stealing)? В трейсе при переходе горутины на другую дорожку без syscall — это воровство.
  • Что такое proc start / proc stop? События связывания P с M или переход в системный вызов с освобождением.
  • Можно ли отфильтровать трейс по времени? Да, в UI есть ползунок зума.

Как найти долгие блокировки с помощью block profile?

Краткий ответ

Включаем runtime.SetBlockProfileRate(1) и снимаем профиль блокировок. Смотрим на записи с большим значением delay (время) в go tool pprof. Стек указывает, где горутина была заблокирована. Анализируем, почему задержка большая (конкуренция за канал, мьютекс, долгий системный вызов).

Развёрнутый ответ

Пример:

Go
runtime.SetBlockProfileRate(10_000) // блокировки длительностью > 10 мкс
// работаем...
pprof.Lookup("block").WriteTo(f, 0)

go tool pprof block.out показывает две метрики: delay (накопленные наносекунды) и contentions (количество событий). Сортировка по delay выявляет самые затратные блокировки. Стек обычно ведёт к операции: chan send, select, sync.Mutex.Lock. Далее нужно понять, почему операция долгая (например, медленный потребитель канала).

Под капотом (internals)

blockevent фиксируется в момент снятия блокировки — записывается время с момента блокировки (cycles). Таким образом измеряется именно длительность блокировки. Профиль не показывает активное выполнение.

Типичные ошибки кандидатов

  • Установка rate=1 без крайней нужды: каждый блокировочный вызов добавляет запись, создавая огромный оверхед.
  • Игнорирование блокировок на sync.Cond: они не всегда попадают в блок профиль (зависит от реализации), но могут быть источником проблем.
  • Путают причину и следствие: видят долгую блокировку на chan send, но не ищут, кто должен читать из канала.

Возможные дополнительные вопросы интервьюера

  • Можно ли профилировать блокировки на чтение файла? Только если файл открыт в блокирующем режиме; чаще используют syscall и они видны в трейсе.
  • Как block профиль соотносится с mutex профилем? Mutex — подмножество, но с более точным учётом конкуренции.

Как работает runtime.SetBlockProfileRate?

Краткий ответ

Функция SetBlockProfileRate(rate int) задаёт порог в наносекундах: события блокировок короче rate не записываются. Значение 0 отключает профилирование, отрицательное — не меняет настройку. Используется для уменьшения накладных расходов.

Развёрнутый ответ

Пример: runtime.SetBlockProfileRate(1_000_000) // 1 ms. Тогда в профиль попадут только блокировки длительностью ≥ 1 мс. Типичное использование в production — высокий порог для детектирования аномальных задержек без существенного оверхеда.

Обработчик net/http/pprof для block profile автоматически не включает его; нужно вызвать в коде при инициализации.

Под капотом (internals)

Внутренняя переменная runtime.blockprofilerate. При каждом событии блокировки вычисляются cycles (разница в тактах между блокировкой и разблокировкой). Проверяется rate > 0 && int64(cycles) >= rate. Если да, вызывается saveblockevent с сохранением стека. Изменение blockprofilerate атомарное.

Типичные ошибки кандидатов

  • Вызов после запуска горутин без синхронизации: значение применяется для будущих событий, уже идущие не затронуты, но это не критично.
  • Ожидание точной микросекундной границы: измерение в тактах, зависит от частоты, но приблизительно соответствует наносекундам.
  • Установка rate меньшего, чем таймер ОС: теоретически ограничено разрешением, но на практике наносекунды точны.

Возможные дополнительные вопросы интервьюера

  • Как получить значение текущего rate? Прямого геттера нет, но можно считать, что сохранённое значение — то, что передали.
  • Можно ли комбинировать с SetMutexProfileFraction? Да, это независимые профили.
  • Влияет ли async preemption на замер времени блокировки? Нет, измерение идёт по monotonic clock.

Как работает runtime.SetMutexProfileFraction?

Краткий ответ

Устанавливает коэффициент rate для мутексного профилировщика: 1 — профилировать каждое конкурирующее разблокирование, 0 — выключить. При конкуренции за мьютекс фиксируется стек и задержка. Более высокие значения (например, 1000) профилируют ~1/1000 событий, сокращая оверхед.

Развёрнутый ответ

Go
runtime.SetMutexProfileFraction(100) // примерно 1% событий

Применяется к sync.Mutex и sync.RWMutex. Профиль показывает задержку и количество конкуренций. Использование ненулевого значения активирует вставку событий в runtime.mutexevent.

Под капотом (internals)

Переменная runtime.mutexprofilerate хранит fraction. При каждом Unlock, если были ожидающие, вызывается mutexevent. Внутри проверяется mutexprofilerate > 0 и случайный отбор на основе быстрых PRNG (runtime.fastrand() % rate == 0). Запись происходит в тот же бакет, что и block профиль, но другой key.

Типичные ошибки кандидатов

  • Установка rate в 0 при попытке включить: 0 отключает, нужно ≥ 1.
  • Считать, что профилируются все Lock вызовы: только те, где был contention.
  • Не учитывать sampling ошибки: при большом rate малые, но частые задержки могут быть не учтены.

Возможные дополнительные вопросы интервьюера

  • Как проверить, что профилировщик работает? Записать профиль, открыть в pprof, должен показывать ненулевые задержки при наличии конкуренции.
  • Как fraction влияет на производительность? При 1 максимальный оверхед, при 1000 — минимальный.

Как отлаживать проблему "too many open files"?

Краткий ответ

Проверить текущее количество открытых файлов через /proc/<pid>/fd или lsof -p <pid>. Установить лимиты ulimit -n. В Go утечка может быть из-за незакрытых os.File, net.Conn, syscall.Socket. Использовать профилирование горутин и pprof для поиска стека утечки.

Развёрнутый ответ

Симптом: ошибка too many open files при вызове Accept, os.Open. Шаги:

  1. lsof -p <pid> | wc -l — увидеть число.
  2. ls -l /proc/<pid>/fd — типы сокетов (socket, pipe, файл).
  3. Если растут сокеты — скорее всего, TCP-соединения не закрываются. Проверить net.Conn.Close() во всех путях.
  4. Goroutine profile поможет найти горутины, заблокированные на Read/Write, которые удерживают сокет.
  5. Установить SetDeadline или SetReadDeadline для предотвращения вечного висения.
  6. Использовать runtime.SetFinalizer для отладки, но не для основного закрытия.

Под капотом (internals)

Низкоуровневое: netFD в net-пакете содержит sysfd int. При Close вызывается syscall.Close, освобождая fd. Если горутина продолжает читать после закрытия, fd не переиспользуется. Утечка может быть на уровне ОС.

Типичные ошибки кандидатов

  • Забытое resp.Body.Close() для HTTP-клиента: приводит к болтающимся соединениям.
  • Отсутствие defer file.Close() или ошибка до defer: паника до регистрации defer.
  • Игнорирование ошибки Close: не освобождает ресурс в некоторых ОС.

Возможные дополнительные вопросы интервьюера

  • Как настроить keep-alive для предотвращения накопления соединений? http.Transport с IdleConnTimeout.
  • Что делать, если lsof показывает много can't identify protocol? Возможно, внутренние pipe.
  • Можно ли увеличить ulimit внутри Go-процесса? Да, через syscall.Setrlimit, но это требует привилегий.

Как отлаживать "high CPU" в продакшене?

Краткий ответ

Снять CPU-профиль через pprof (по HTTP или сигналу), проанализировать функции, потребляющие наибольшее flat время; оценить, не связано ли с бесконечным циклом, избыточными аллокациями, частыми вызовами GC, или спин-блокировкой. При необходимости использовать трассировку для выявления задержек.

Развёрнутый ответ

Схема действий:

  • Открыть /debug/pprof/profile?seconds=10 и загрузить.
  • В go tool pprof выполнить top20, list <func>.
  • При обнаружении, что время уходит в runtime.mallocgc — много аллокаций, смотреть heap, уменьшать.
  • Если время в runtime.gcBgMarkWorker — частый GC, увеличить GOGC.
  • Если высокая конкуренция — futex, pthread_cond_wait — смотреть mutex/block profile.
  • Если подозрение на бесконечный цикл — goroutine profile покажет состояние.

Под капотом (internals)

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

Типичные ошибки кандидатов

  • Анализ профиля с очень маленьким интервалом: 1-2 секунды могут не захватить периодическую активность.
  • Не учитывать syscall время: в Linux CPU-профиль по умолчанию включает только пользовательское время; системные вызовы могут не отображаться, их видно в трассировке.
  • Путаница flat и cum: если flat маленький, а cum большой — функция вызывает тяжёлые вычисления.

Возможные дополнительные вопросы интервьюера

  • Как снять профиль для процесса внутри контейнера без HTTP? С помощью kill -SIGPROF (если программа обрабатывает и пишет профиль) или exec-ом с pprof в контейнер.
  • Что делать, если профиль показывает высокую загрузку в runtime.usleep? Возможно, активное ожидание; проверить time.Sleep с 0.
  • Как частота профилирования влияет на результаты? 100 Гц достаточно для большинства сценариев.

Как strace и lsof помогают в отладке Go-приложений?

Краткий ответ

strace перехватывает системные вызовы, показывая, что делает процесс (файловый ввод-вывод, сеть, блокировки), помогает выявить узкие места на уровне ОС. lsof перечисляет открытые файловые дескрипторы, выявляя утечки файлов и сокетов. Оба инструмента незаменимы, когда стандартных профилей Go недостаточно.

Развёрнутый ответ

  • strace -f -p <pid> (или strace -f ./app) показывает каждый syscall с аргументами и результатом. При зависании можно увидеть, на каком вызове спит горутина (epoll_wait, futex). При утечке — заметить несбалансированные open/close.
  • lsof -p <pid> — список открытых дескрипторов. Повторный запуск выявляет рост. Можно узнать IP/порт сокета (lsof -i -a -p <pid>).
  • strace -c -p <pid> даст сводку по частоте syscall'ов; высокая доля futex — contention, write/read — ввод/вывод.

Под капотом (internals)

Go работает поверх syscall; strace показывает все, включая вызовы рантайма. Это может быть шумно, но помогает найти, например, что epoll_wait возвращает много событий, а горутины не успевают обрабатывать.

Типичные ошибки кандидатов

  • Не фильтровать вывод strace: полный лог огромен; нужны фильтры (-e trace=network,file).
  • Запуск strace на высоконагруженном продакшене: может замедлить процесс и даже изменить поведение (Heisenberg effect).
  • Прямое сопоставление syscall с горутиной: syscall не маркирован goroutine ID, нужно сопоставлять по tid.

Возможные дополнительные вопросы интервьюера

  • Как узнать, какой горутине принадлежит syscall в strace? По /proc/pid/task/tid/comm или логированием runtime.GoID() с привязкой к tid через gettid().
  • Что значит EAGAIN в strace? Неблокирующий ввод/вывод не готов; нормально для сетевого цикла Go.
  • Чем заменить lsof в контейнере без привилегий? /proc/<pid>/fd доступен без root, часто этого достаточно.

Как профилировать использование сети (net profile)?

Краткий ответ

Сетевой профиль в pprof (/debug/pprof/net) показывает вызовы net.Dial, net.Listen, Accept, и время операций ввода-вывода (по событиям). Однако в Go 1.22+ рекомендуется использовать трассировку для детального анализа сети, а также net/http/pprof предоставляет базовый профиль блокировок сети.

Развёрнутый ответ

net/http/pprof регистрирует /debug/pprof/net (если импортирован). Профиль содержит записи о создании соединений, времени их жизни и блокировках на чтение/запись. Для HTTP-сервера можно анализировать через go tool pprof http://server/debug/pprof/net. Альтернативно, используя трассировку, виден сетевой опрос (poll) и epoll-события.

Доступные метрики в net профиле: Delay (время от Dial до успешного соединения), Connect, Accept, и т.д. Позволяет найти медленные DNS-запросы, долгие TCP-рукопожатия.

Под капотом (internals)

Пакет net регистрирует хуки в pprof через net.NewProfiler. При операциях Dial запускается таймер, записывающий задержку в профиль. Профиль net реализован как pprof.Profile с именем "net", хранит время операций. Для блокировок на чтении используется block профиль с метками net.Read.

Типичные ошибки кандидатов

  • Попытка найти TCP retransmission через net профиль: он не даёт такого уровня; нужны ss, netstat или tcpdump.
  • Ожидание профиля всех сетевых вызовов: профиль охватывает только пакет net, а не CGO или чистые сокеты.
  • Не учитывают кэширование DNS: медленный первый Dial может быть из-за DNS.

Возможные дополнительные вопросы интервьюера

  • Как мониторить число активных соединений? Через expvar или метрики Prometheus, либо кастомный счётчик.
  • Что делать, если сетевой профиль показывает высокую задержку Dial? Проверить resolv.conf, DNS-сервер, таймауты на Dialer.
  • Можно ли профилировать HTTP/2 отдельно? net/http2 имеет свои внутренние метрики, но они не экспортируются в net профиль.

Как настроить pprof для долгоживущих сервисов?

Краткий ответ

Использовать отдельный HTTP-сервер на административном порту с доступом только для мониторинга, настроить периодическую запись профилей на диск по сигналу или расписанию, и обеспечить мониторинг метрик (goroutines, heap) через expvar без постоянного профилирования.

Развёрнутый ответ

Рекомендации:

  1. Запустить admin-сервер (net/http/pprof) на локальном порту с basic auth.
  2. Настроить автоматический дамп heap-профиля при достижении порога (например, через signal.Notify на SIGUSR1):
Go
signalCh := make(chan os.Signal, 1)
signal.Notify(signalCh, syscall.SIGUSR1)
go func() {
    for range signalCh {
        f, _ := os.Create(fmt.Sprintf("heap-%d.pprof", time.Now().Unix()))
        pprof.WriteHeapProfile(f)
        f.Close()
    }
}()
  1. Для CPU можно запускать профилирование по крону через обращение к эндпоинту profile?seconds=30 и сохранять результат.
  2. Собирать базовые метрики (горутины, память) с помощью expvar и Prometheus.

Под капотом (internals)

Ничего специфичного, кроме стандартных механизмов. Важно, что pprof профили не сохраняются сами, нужна внешняя автоматизация.

Типичные ошибки кандидатов

  • Постоянно включённый CPU-профиль: StartCPUProfile на весь срок службы сервера забивает память буфером и замедляет; использовать короткие интервалы.
  • Сбор heap профиля без вызова runtime.GC(): профиль может содержать много мусора; часто принудительно запускают GC перед снимком.
  • Хранение профилей без ротации: может закончиться место на диске.

Возможные дополнительные вопросы интервьюера

  • Как организовать сбор профилей в Kubernetes? Sidecar или скрипт в init-контейнере, обращающийся к эндпоинту приложения.
  • Как избежать остановки мира при записи heap профиля? WriteHeapProfile не вызывает STW полностью, но может немного затормозить.
  • Можно ли использовать --headless delve для профилирования? Нет, delve для интерактивной отладки, не для периодического сбора.

Как работает continuous profiling (например, Pyroscope)?

Краткий ответ

Постоянное профилирование автоматически собирает CPU, heap, block профили с заданной периодичностью и загружает в хранилище для анализа трендов. Pyroscope интегрирует агента в Go-приложение, позволяя видеть исторические данные и сравнивать версии.

Развёрнутый ответ

Концепция: вместо ручного снятия профилей при инцидентах, профили собираются всегда (CPU 10-30 сек каждый N минут, heap реже). Pyroscope предоставляет Go-клиент, который можно встроить:

Go
import _ "github.com/pyroscope-io/pyroscope/pkg/agent/profiler"

Или использовать отдельный агент, подключающийся к pprof-эндпоинту. Данные отправляются на сервер Pyroscope, где можно строить flame graph за любой промежуток времени, искать регрессии.

Это позволяет мгновенно ответить на вопрос: «когда началась деградация производительности и в какой функции».

Под капотом (internals)

Агент использует стандартные механизмы runtime/pprof, периодически вызывая WriteHeapProfile и читая cpuProfile. Он добавляет метаданные (теги, версия) и отправляет в хранилище (обычно S3/RocksDB). Pyroscope сервер агрегирует в индексы для быстрого поиска.

Типичные ошибки кандидатов

  • Слишком частое профилирование CPU: может создать постоянный оверхед; обычно интервал не меньше 30-60 секунд с паузой в несколько минут.
  • Не контролируют объём данных: профили могут занимать много места; нужна политика retention.
  • Забывают про блокировку профиля при загрузке: отправка по сети может добавлять нагрузку.

Возможные дополнительные вопросы интервьюера

  • Чем отличается от периодического снятия профиля вручную? Непрерывность данных, автоматическое обнаружение аномалий.
  • Какие альтернативы Pyroscope? Parca, Google Cloud Profiler, Datadog Continuous Profiler.
  • Можно ли реализовать самостоятельно? Да, с опросом pprof эндпоинта и сохранением в time-series базу.

Что такое execution tracer и чем он отличается от CPU profiler?

Краткий ответ

Execution tracer (runtime/trace) записывает хронологию событий выполнения (запуск/блокировка горутин, GC, syscall), в то время как CPU profiler агрегирует статистику времени по функциям. Трейсер показывает, почему горутина не выполнялась (заблокирована, вытеснена), CPU profiler — где тратится процессорное время.

Развёрнутый ответ

CPU profiler отвечает на вопрос «в какой функции горят процессорные ресурсы». Execution tracer дополняет картину: «почему программа недозагружена, хотя задач много», «как долго горутина ждала канала». Трейсер нужен для диагностики задержек, планировщика и взаимодействия компонентов.

Пример: CPU профиль показывает высокую загрузку в runtime.selectgo, но трейсер может показать, что горутина пробуждается и сразу снова блокируется, указывая на неправильную логику select.

Под капотом (internals)

Трейсер интегрирован в планировщик, записывает события при каждом изменении состояния G. CPU profiler периодически прерывает поток и записывает pc. Профайлер имеет низкий оверхед, трейсер — значительно больший (до 20%).

Типичные ошибки кандидатов

  • Пытаться заменить трейсером профилирование CPU: трейс не покажет горячие функции так же чётко.
  • Включение трейсера на длительное время из-за его детальности: нужно ограничивать несколькими секундами.
  • Анализ трейса без понимания модели G-M-P: можно неправильно интерпретировать простои.

Возможные дополнительные вопросы интервьюера

  • Можно ли совместить трейсер и CPU профиль? Технически да, но на практике оверхед слишком велик.
  • Как трейсер помогает находить проблемы с GC? Показывает фазы GC и паузы в timeline.
  • Чем трассировка отличается от логирования? Трейсинг фиксирует контрольные точки автоматически, лог — по воле программиста.

Как профилировать аллокации объектов определенного типа?

Краткий ответ

Стандартный pprof heap профиль показывает все аллокации, но не группирует по типам. Для этого используют pprof -list с регулярным выражением по имени типа, либо кастомный подход с runtime.SetFinalizer и счётчиками, или инструменты вроде go-torch и memory profiler. В pprof можно фильтровать по функции-аллокатору.

Развёрнутый ответ

Чтобы найти, где создаются объекты конкретного типа, можно:

  • Снять профиль alloc_objects и в интерактивном pprof выполнить list NewMyStruct или list mypkg.New.
  • Фильтровать в UI pprof с помощью focus=mypkg.MyStruct или регулярки.
  • Написать обёртку с expvar и счётчиками вызовов конструктора.
  • Использовать pprof Tags API: pprof.Do(ctx, pprof.Labels("type", "MyStruct"), func(ctx context.Context) { ... }), чтобы аннотировать профиль.

Под капотом (internals)

Тип объекта не сохраняется напрямую в профиле, только стек вызовов. Поэтому фильтр по типу — это фильтр по функции, которая этот тип создаёт (конструктор, new, make). Tags позволяют группировать выборки по произвольным ключам.

Типичные ошибки кандидатов

  • Ожидание, что heap профиль показывает поле type: тип можно только косвенно вывести.
  • Поиск по имени типа без учёта инлайнинга: конструктор мог быть встроен, тогда стек не содержит его.
  • Забыть про анонимные структуры в слайсах: они могут быть учтены как []struct{...} без явного имени.

Возможные дополнительные вопросы интервьюера

  • Как посчитать, сколько объектов типа X живо? Через runtime.SetFinalizer с глобальным счётчиком (finalizer вызывается при сборке).
  • Можно ли профилировать аллокации строк отдельно? Строки часто создаются компилятором, можно искать по функциям rawstring, slicebytetostring и т.д.
  • Как pprof Tags влияют на размер профиля? Добавляют метаданные, увеличивают файл.

Как работает debug/elf и debug/macho для анализа бинарных файлов?

Краткий ответ

Пакеты debug/elf и debug/macho позволяют разбирать исполняемые файлы в форматах ELF (Linux) и Mach-O (macOS): читать символы, секции, DWARF-отладочную информацию. Полезны для написания инструментов интроспекции, аналогичных nm, objdump, но на Go.

Развёрнутый ответ

Пример чтения таблицы символов ELF:

Go
f, _ := elf.Open("binary")
defer f.Close()
symbols, _ := f.Symbols()
for _, s := range symbols {
    fmt.Println(s.Name, s.Value, s.Size)
}

Можно извлечь DWARF-данные: f.DWARF() и прочитать типы, локации переменных. Это основа для отладчиков и delve. Аналогично debug/macho для macOS.

Под капотом (internals)

Пакеты реализуют парсинг структур заголовков и таблиц в соответствии с форматами. debug/elf содержит определения констант elf.R_X86_64_PC32 и т.д. DWARF предоставляется через стандартный debug/dwarf.

Типичные ошибки кандидатов

  • Предположение, что бинарник всегда содержит секцию DWARF: стрипнутые бинарники (с флагом -w -s) не имеют отладочной информации.
  • Забыть про кросскомпиляцию: форматы зависят от целевой ОС.
  • Парсинг больших бинарников в память: может быть затратно; лучше mmap или читать потоково.

Возможные дополнительные вопросы интервьюера

  • Как узнать версию Go, которой скомпилирован бинарник? Через runtime.buildVersion или символ go.buildinfo в debug/buildinfo.
  • Можно ли модифицировать бинарник через эти пакеты? Только чтение; для изменения нужны debug/pe и бинарная запись.
  • Как получить размер стека по DWARF? DWARF содержит .debug_frame, через который можно вычислить frame size.

Как использовать go tool objdump для дизассемблирования?

Краткий ответ

go tool objdump binary дизассемблирует исполняемый файл, показывая машинный код с аннотациями Go-функций. Помогает анализировать, во что компилируется код, почему не инлайнится, и искать микрооптимизации на уровне инструкций.

Развёрнутый ответ

Команда:

TEXT
go tool objdump -s myFunc binary

Вывод:

TEXT
TEXT main.myFunc(SB) ...
  main.go:5   0x4a0b20   MOVQ  AX, 0x8(SP)

Флаг -gnu показывает синтаксис GNU. Можно дизассемблировать всю программу, но обычно фильтруют по функции. Полезно смотреть на ассемблерные следствия escape analysis и инлайнинга.

Под капотом (internals)

objdump использует внутренний пакет cmd/internal/objfile для открытия бинарника (ELF, Mach-O, PE) и дизассемблер cmd/internal/obj/arm64 и т.д., плюс arch.Disasm. Он читает таблицу символов, сопоставляет адреса с функциями и строками.

Типичные ошибки кандидатов

  • Пытаться дизассемблировать без отладочных символов: у стрипнутого бинарника не будет имён функций.
  • Забыть флаг -s и получить слишком много вывода.
  • Неправильная интерпретация ассемблера без знания ABI: нужно знать, что регистры используются для аргументов.

Возможные дополнительные вопросы интервьюера

  • Как убедиться, что инлайнинг произошёл? go tool objdump покажет отсутствие CALL на функцию.
  • Как посмотреть, где используется XCHG для sync.Mutex? Дизассемблировать sync.(*Mutex).Lock.
  • Можно ли дизассемблировать в Intel синтаксисе? -gnu нет, только AT&T или Go-специфичный.

Что такое async preemption и как оно отражается в трейсах?

Краткий ответ

С Go 1.14 рантайм может вытеснить выполняющуюся горутину асинхронно, без её содействия (вставки проверок). В трейсах это событие traceEvGoPreempt; горутина переходит из running в runnable, а на её P встаёт другая. Обеспечивает более справедливое планирование и меньшие задержки.

Развёрнутый ответ

До 1.14 вытеснение происходило только в точках кооперативных вызовов функций. Плотный цикл без вызовов мог монополизировать P на длительное время. Async-вытеснение использует сигнал SIGURG (Linux) для мягкого прерывания потока. В трейсе это видно как полоска running, которая прерывается и переходит в runnable, даже если не было вызова.

Под капотом (internals)

Принцип: существует горутина sysmon, которая мониторит длительность выполнения на P. Если больше 10 мс, отправляет сигнал потоку. Обработчик сигнала сохраняет стек и выполняет asyncPreempt, меняя контекст на другую горутину. В трейсе генерируется событие traceEvGoPreempt. Флаг GODEBUG=asyncpreemptoff=1 отключает механизм.

Типичные ошибки кандидатов

  • Путать с кооперативной вытеснением на системных вызовах: sysmon также перехватывает зависшие syscall, но механизм другой.
  • Не учитывать влияние на профилировщик: с async-вытеснением CPU профиль может фиксировать стек в середине ассемблерной инструкции?
  • Пытаться отключить без понимания последствий: увеличится tail latency.

Возможные дополнительные вопросы интервьюера

  • Как async preemption соотносится с GOMAXPROCS? Работает на всех P независимо.
  • Может ли это вызывать проблемы с атомарностью? Нет, так как Go гарантирует неатомарность только если компилятор не вставил проверки, но асинхронное вытеснение останавливает в safe-point.
  • Почему используется SIGURG? Редко используется приложениями, доставляется в поток.

Как найти "false sharing" с помощью профилировщика кэш-миссов?

Краткий ответ

Стандартный Go-профилировщик не показывает события кэш-промахов. Необходимы Linux perf (perf record -e cache-misses, perf annotate) и pprof, интегрированный с perf (конвертировать perf.data в профиль pprof через perf_to_profile). go tool pprof может отобразить аннотированный исходный код с метриками промахов, позволяя найти поля структур, разделяющих кэш-линии.

Развёрнутый ответ

Алгоритм:

  1. perf record -e cache-misses -p <pid> sleep 10
  2. Преобразовать с помощью github.com/google/perf_data_converter или скрипта.
  3. Загрузить в pprof, увидеть функции с высоким числом кэш-промахов.
  4. При подозрении на false sharing, проанализировать макет структуры с помощью unsafe.Sizeof и aligncheck. False sharing возникает, когда два часто обновляемых поля разных горутин находятся в одной кэш-линии (обычно 64 байта), вызывая взаимные инвалидации. Решение — добавить padding.

Под капотом (internals)

Процессор обновляет кэш L1/L2; perf.cache-misses считает промахи L1. Профиль покажет, какие инструкции привели к промахам. Инструменты вроде go-trace не видят аппаратные счётчики, только perf.

Типичные ошибки кандидатов

  • Пытаться найти false sharing только через mutex profile: конкуренция может быть без мьютексов, например, atomic-операции на соседних полях.
  • Добавлять случайный padding без измерения: может не помочь, если кэш-линия 128 байт.
  • Не учитывать, что GC может перемещать объекты: в стеке false sharing редко.

Возможные дополнительные вопросы интервьюера

  • Как проверить, что padding реально помог? Сравнением бенчмарков с perf stat.
  • Как Go-рантайм борется с false sharing? Внутренние структуры часто имеют ручной padding (например, p в рантайме).
  • Есть ли встроенный инструмент для обнаружения? Нет, нужно использовать внешние.

Как pprof работает с symbolization?

Краткий ответ

При записи профиля pprof не включает имена функций, только адреса. Позже при анализе (go tool pprof) происходит символизация: сопоставление адресов с именами функций и номерами строк через DWARF или таблицу символов бинарника. Для этого pprof автоматически ищет локальный бинарник, с которым был собран профиль.

Развёрнутый ответ

Когда профиль снимается на продакшене, но анализ делается на машине разработчика, нужно предоставить тот же бинарник (или образ контейнера) с отладочной информацией. Команда go tool pprof -symbolize=local profile пытается найти бинарник по пути, сохранённому в профиле, или можно указать явно через -local_symbols.

При использовании go tool pprof он читает секции .symtab или .gopclntab (Go-специфичная таблица) для быстрой символизации.

Под капотом (internals)

В профиле хранятся mapping и location с address. pprof загружает бинарник, читает text смещение, таблицу pclntab (go-specific), чтобы маппить pc на функцию и строку. Механизм оптимизирован для Go, не требует внешних DWARF, хотя и использует их при наличии.

Типичные ошибки кандидатов

  • Забыть сохранить бинарник после деплоя: без него профиль бессмыслен (только адреса). Нужно хранить билд-артефакты в реестре.
  • Использовать стрипнутый бинарник (-ldflags="-w -s"): тогда даже pclntab может отсутствовать; символизация невозможна.
  • Пытаться анализировать профиль с чужой машины без указания пути: нужно -add_comment с путём или использовать ту же версию.

Возможные дополнительные вопросы интервьюера

  • Как pprof находит бинарник на удалённом сервере? По умолчанию ищет по пути из профиля; можно использовать -symbolize=remote и http://server/debug/pprof/symbol.
  • Что такое perf.map для JIT? Для Go неактуально, так как компиляция в машинный код заранее.
  • Можно ли символизировать без бинарника, имея лишь .pclntab? Файлы .symtab обычно внутри бинарника, отдельно извлечь можно, но pprof ожидает исполняемый файл.

Как настроить сбор профилей в условиях ограниченной памяти?

Краткий ответ

Использовать pprof с осторожностью: предпочитать потоковые профили (HTTP-эндпоинт с записью в файл по мере готовности) вместо буферизации в памяти, ограничивать частоту сбора, увеличить MemProfileRate (меньшая детализация), и применять runtime.GC() перед снятием heap для уменьшения размера.

Развёрнутый ответ

В условиях жёсткой памяти:

  • CPU профиль по умолчанию использует кольцевой буфер (~64 Кб), что приемлемо.
  • Heap профиль может быть большим (десятки мегабайт). Можно увеличить runtime.MemProfileRate (по умолчанию 512 Кб, можно сделать 1 МБ), чтобы снизить количество записей.
  • Не хранить несколько профилей одновременно; сразу сбрасывать на диск.
  • Использовать /debug/pprof/heap?gc=1 для принудительной сборки мусора перед профилированием.
  • Profile блокировок/мьютексов может расти значительно; включать только на короткие промежутки.
  • Рассмотреть continuous profiling с внешним агентом, который сразу забирает данные, не храня локально.

Под капотом (internals)

Структура heap bucket занимает память; если приложение создаёт миллионы аллокаций, профиль может быть огромен. Увеличение MemProfileRate уменьшает число сэмплов. Флаг -sample_index=inuse_objects также влияет, но меньше.

Типичные ошибки кандидатов

  • Снимать heap профиль без GC: содержит много мусора, размер завышен, тратится память на сериализацию.
  • Не контролировать память, занятую самим профилем: профиль записывается в память как буфер перед отправкой; можно писать напрямую в net.Conn через pprof.WriteHeapProfile(w) на удалённый сервер.
  • Запускать go tool pprof на сервере: это потребляет доп. память, нужно копировать профиль на локальную машину.

Возможные дополнительные вопросы интервьюера

  • Как уменьшить размер heap профиля? Увеличить MemProfileRate, использовать inuse_space вместо alloc_space.
  • Можно ли отключить некоторые профили? Да, просто не регистрировать их.
  • Влияет ли pprof на фрагментацию кучи? Нет.

Что такое "полноценный core dump" и как его проанализировать с delve core?

Краткий ответ

Core dump — снимок памяти процесса в момент аварийного завершения (SIGQUIT, SIGABRT). delve может открыть core-файл командой dlv core <binary> <core> для посмертной отладки: просмотра стеков, переменных, горутин, причин падения. Аналог gdb для Go.

Развёрнутый ответ

Генерация core dump на Linux:

TEXT
ulimit -c unlimited
GOTRACEBACK=crash ./myapp

При панике создаётся core. Затем:

TEXT
dlv core ./myapp core.12345
(dlv) goroutines
(dlv) goroutine 1
(dlv) stack
(dlv) vars

Позволяет узнать, что привело к панике, даже если программа уже завершена. Core dump может быть очень большим (содержит всю память). Полезно для отладки трудновоспроизводимых ошибок на продакшене.

Под капотом (internals)

delve читает core файл в формате ELF, используя /proc/vmcore или дамп, восстанавливает образ памяти, структуры runtime (g, m, allgs) и сопоставляет с DWARF бинарника. Команда core подменяет обычный attach.

Типичные ошибки кандидатов

  • Не включать GOTRACEBACK=crash: по умолчанию паника печатает стек и завершается, core не создаётся.
  • Забыть, что core-файл занимает много места: нужно мониторить место на диске.
  • Пытаться отлаживать без отладочной информации в бинарнике: -ldflags="-w -s" убирает DWARF, delve будет ограничен.

Возможные дополнительные вопросы интервьюера

  • Как заставить программа оставить core dump без паники? kill -SIGQUIT <pid> при GOTRACEBACK=crash.
  • Можно ли открыть core в delve без бинарника? Нет, нужен точный исполняемый файл.
  • Как уменьшить размер core дампа? Через coredump_filter в /proc/<pid> — можно исключить shared memory, но не для Go.

Как использовать go tool compile -l для отключения инлайнинга при отладке?

Краткий ответ

Флаг -l (или -l -l для большей деоптимизации) отключает инлайнинг при компиляции, что упрощает отладку в delve (чёткие границы функций, предсказуемые стеки) и анализ профилей. Используется как go build -gcflags="-l" или go test -gcflags="-l".

Развёрнутый ответ

Инлайнинг (встраивание функций) мешает отладке, так как брейкпоинты могут не срабатывать на ожидаемой строке, значения переменных оптимизированы. -gcflags="-l" полностью отключает инлайнинг. -gcflags="-l -l" отключает ещё агрессивнее (например, начиная с Go 1.21+). Для delve рекомендуется всегда отключать инлайнинг и оптимизации (-N -l).

Под капотом (internals)

Флаг передаётся компилятору compile; он снижает maxInlineBudget до 0, предотвращая встраивание. Это увеличивает размер бинарника и немного снижает производительность, но делает отладку детерминированной.

Типичные ошибки кандидатов

  • Использовать -l только для основного пакета, забывая про all=-l: зависимости всё равно инлайнены.
  • Оставлять -l в production сборке: замедляет код, увеличивает бинарник.
  • Полагать, что -l отключает все оптимизации: нет, только инлайнинг; нужны -N для отключения оптимизаций.

Возможные дополнительные вопросы интервьюера

  • Как включить инлайнинг обратно? Без флага.
  • Как проверить, что инлайнинг действительно отключён? go build -gcflags="-m -m" покажет сообщения об инлайнинге.
  • Почему -gcflags требует =? Синтаксис компилятора: флаг и значение разделены пробелом в go tool compile, но в -gcflags можно через -l -l или -l=2.

Как race detector помогает в отладке?

Краткий ответ

Race detector (-race) встраивает в бинарник инструментацию для отслеживания обращений к памяти и синхронизации, выявляя гонки данных (data races). При обнаружении печатает стек двух конфликтующих горутин, что позволяет найти трудноуловимые конкурентные баги.

Развёрнутый ответ

Включается флагом: go test -race, go build -race. Во время выполнения каждый доступ к памяти (чтение/запись) записывается в тени отображения, проверяя, не было ли доступа из другой горутины без синхронизации. При гонке выводит предупреждение:

TEXT
WARNING: DATA RACE
Write at ... by goroutine 10:
  main.write()
Previous read at ... by goroutine 11:
  main.read()

Детектор замедляет программу в 5-10 раз и увеличивает потребление памяти, поэтому используется только в тестах и CI.

Под капотом (internals)

Инструментация вставляет вызовы runtime.raceread/racewrite и raceacquire/racerelease для операций синхронизации. Использует библиотеку ThreadSanitizer (TSan), алгорим "предикатной блокировки" (shadow state machine). Метаданные хранятся в shadow-памяти (8 байт на каждый 16-байтный блок).

Типичные ошибки кандидатов

  • Запускать -race на продакшене: огромный оверхед и ложные срабатывания при использовании unsafe.Pointer.
  • Игнорировать гонки на операциях с bool: нет атомарности, даже запись bool небезопасна.
  • Считать, что -race находит все гонки: детектор динамический, нужны тесты, покрывающие конкурирующие пути.

Возможные дополнительные вопросы интервьюера

  • Как race detector обрабатывает sync/atomic? Он понимает атомарные операции как синхронизированные, не сообщает о гонках.
  • Можно ли использовать race detector вместе с pprof? Да, но ещё медленнее.
  • Какие есть аналоги? Go -race единственный; есть статический анализатор golang.org/x/tools/go/analysis.

Как go vet и staticcheck помогают находить потенциальные проблемы?

Краткий ответ

go vet — встроенный статический анализатор, выявляющий подозрительные конструкции (неправильный формат Printf, unreachable код, блокировки мьютексов по значению). staticcheck — расширенный линтер с более глубокими проверками (ненужные присваивания, устаревшие API, утечки time.Tick). Они предотвращают ошибки до исполнения.

Развёрнутый ответ

Запуск: go vet ./... анализирует пакеты. staticcheck устанавливается отдельно: staticcheck ./.... Примеры находок:

  • Printf с несоответствием аргументов.
  • loopclosure — захват переменной цикла в горутине (до Go 1.22).
  • mutex by value.
  • unmarshal в неэкспортируемые поля.
  • time.Tick утечка горутины (предлагает time.NewTicker).

Под капотом (internals)

go vet использует пакет go/analysis для регистрации анализаторов. Каждый анализатор обходит AST и сообщает. staticcheck использует более сложный IR (промежуточное представление) и межпроцедурный анализ.

Типичные ошибки кандидатов

  • Игнорировать предупреждения vet: множество багов отлавливаются на CI позже.
  • Отключать vet без понимания: например, //nolint должен быть осознанным.
  • Не обновлять staticcheck: старые версии могут не знать новых API.

Возможные дополнительные вопросы интервьюера

  • Чем go vet отличается от go build -race? Vet — статический анализ исходников, race — динамический.
  • Можно ли писать свои анализаторы? Да, на базе golang.org/x/tools/go/analysis.
  • Как интегрировать в CI? Запускать go vet ./... и staticcheck ./... с флагом ошибки.

Как отлаживать зависшие горутины с помощью panic и SIGQUIT?

Краткий ответ

Отправить сигнал SIGQUIT процессу (при GOTRACEBACK=all или crash), чтобы получить дамп стека всех горутин в stderr. Или программно вызвать panic с отладочным сообщением. Стеки покажут, где каждая горутина заблокирована, что позволяет определить причину зависания.

Развёрнутый ответ

Для живого процесса:

  1. Установить GOTRACEBACK=all (или crash для core).
  2. Выполнить kill -SIGQUIT <pid>. Вывод будет содержать stacktrace всех горутин с состоянием.
  3. Или в коде разместить условный panic/debug.Stack в подозрительном месте.
    Используя SIGQUIT, программа может продолжить работу (если GOTRACEBACK=all без crash). Это безопасный способ диагностики на продакшене.

В Go есть возможность обработать SIGQUIT и записать стеки в файл:

Go
signal.Notify(ch, syscall.SIGQUIT)
go func() {
    for range ch {
        buf := make([]byte, 1<<20)
        runtime.Stack(buf, true)
        os.Stderr.Write(buf)
    }
}()

Под капотом (internals)

Обработчик сигнала SIGQUIT в runtime (при соответствующем GOTRACEBACK) вызывает runtime.dopanic или debug.Stack, итерирует allgs и печатает. GOTRACEBACK=system добавляет системные горутины.

Типичные ошибки кандидатов

  • Использовать SIGKILL вместо SIGQUIT: SIGKILL убивает без дампа.
  • Не установить GOTRACEBACK: по умолчанию выводятся только стеки текущей горутины.
  • **Забыть про ограничения вывода: stderr может быть перехвачен контейнерным оркестратором; нужно настроить логирование.

Возможные дополнительные вопросы интервьюера

  • Как получить дамп только одной проблемной горутины? Через pprof.Lookup("goroutine") с фильтрацией.
  • Можно ли вызывать SIGQUIT изнутри программы? Да, через syscall.Kill(syscall.Getpid(), syscall.SIGQUIT).
  • Безопасно ли вызывать runtime.Stack в обработчике сигнала? Да, он специально сделан безопасным для асинхронных вызовов.

Как работает gops агент и как его использовать для удаленной отладки?

Краткий ответ

gops — легковесный агент, встраиваемый в Go-приложение, который слушает команды на отдельном порту (по умолчанию TCP). Позволяет удалённо просматривать список горутин, стек, память, GC-статистику и даже выполнять pprof профили без HTTP-сервера.

Развёрнутый ответ

Импорт и запуск агента:

Go
import "github.com/google/gops/agent"
agent.Listen(agent.Options{})

Затем командная строка gops подключается к агенту:

TEXT
gops
> PID  APP
> 1234 myapp
gops stack 1234
gops memstats 1234
gops pprof-heap 1234

Преимущество: не требует отдельного порта с HTTP, использует бинарный протокол, легче, удобен для отладки в контейнерах без открытия порта (можно через exec). Агент аутентификации не предоставляет, так что для продакшена нужна осторожность.

Под капотом (internals)

Агент регистрирует TCP-лисенер на loopback (127.0.0.1:случайный порт); протокол простой: запрос типа команды и ответа в виде бинарных данных. Команды транслируются в вызовы runtime.Stack, pprof.Lookup и др. Присутствует минимальная проверка на PID.

Типичные ошибки кандидатов

  • Открыть агента на всех интерфейсах: опасно без защиты.
  • Забыть закрыть агента: утечка TCP-порта.
  • Использовать вместе с net/http/pprof без необходимости: дублирует функциональность, но gops удобен, когда HTTP нежелателен.

Возможные дополнительные вопросы интервьюера

  • Как gops находит процессы? Сканирует /proc или tasklist на наличие открытых портов gops.
  • Можно ли настроить порт? Да, agent.Listen(agent.Options{Addr: "127.0.0.1:9999"}).
  • Чем gops отличается от expvar? Агент даёт больше деталей (стек, pprof) и не требует HTTP.

Как интерпретировать вывод runtime.Stack(buf, true) в продакшене?

Краткий ответ

runtime.Stack(buf, true) записывает в буфер стек всех горутин в текстовом виде, аналогично debug=2 goroutine profile. Использование в продакшене помогает в реальном времени получить снимок состояния горутин для анализа зависаний, утечек или дедлоков.

Развёрнутый ответ

Пример:

Go
buf := make([]byte, 1<<20) // 1 МБ
n := runtime.Stack(buf, true)
os.Stderr.Write(buf[:n])

Вывод содержит список горутин, например:

TEXT
goroutine 1 [running]:
main.main()
        /app/main.go:10 +0x35

goroutine 6 [chan receive]:
main.worker(0xc0000b8000)
        /app/worker.go:15 +0x4a

Расшифровка:

  • ID, состояние (running, chan receive, IO wait, select, sleep).
  • Трассировка вызовов с адресами.
    Полезна для регистрации в обработчике сигналов или периодического логирования. В отличие от pprof, не требует HTTP.

Под капотом (internals)

Функция проходит runtime.allgs (защищая allglock), вызывает runtime.goroutineheader и runtime.callers/runtime.gcallers для каждого G, форматирует строки. Это безопасно для вызова из обработчика сигнала, если buf выделен заранее.

Типичные ошибки кандидатов

  • Слишком маленький буфер: стеки обрезаются; нужно выделять достаточно памяти или проверять флаг n == len(buf).
  • Вызывать синхронно при большом количестве горутин: может вызвать задержку и конкуренцию на allglock; лучше в отдельной горутине.
  • Не фильтровать выходные данные: тысячи строк сложно анализировать; нужно агрегировать.

Возможные дополнительные вопросы интервьюера

  • Как часто можно вызывать runtime.Stack? Зависит от числа горутин; 10 тысяч горутин могут создать мегабайты вывода и занять несколько миллисекунд.
  • Можно ли писать в файл с ротацией? Да, обернуть в вызов логирования.

Что такое FlightRecorder в Go 1.25?

Краткий ответ (30–60 секунд)

runtime/trace.FlightRecorder (Go 1.25+) — это встроенный в рантайм «бортовой самописец» (black-box recorder) для трассировки событий. Вместо тяжелой непрерывной записи трейса на диск (которая раздувает гигабайты логов и создает оверхед ввода-вывода), FlightRecorder непрерывно пишет события рантайма (переключения горутин, GC, системные вызовы) в кольцевой буфер в оперативной памяти фиксированного объема или времени (например, последние 30 секунд). При аварии, панике или p99-спайке буфер можно мгновенно сбросить в файл для ретроспективного расследования.

Развёрнутый ответ

Раньше трассировка через runtime/trace.Start требовала предварительного включения. Если в продакшене происходил внезапный 2-секундный спайк latency раз в сутки, поймать его классическим трейсом было невозможно без постоянной записи сотен гигабайт на диск.

С FlightRecorder трассировка работает непрерывно в памяти с наносекундным оверхедом:

Go
import (
    "bytes"
    "net/http"
    "runtime/trace"
    "time"
)

func initFlightRecorder() {
    fr := trace.NewFlightRecorder()
    fr.SetPeriod(30 * time.Second) // Хранить скользящее окно за последние 30 сек
    fr.Start()

    // Сброс скользящего окна по HTTP-запросу инженера во время инцидента
    http.HandleFunc("/debug/flight-trace", func(w http.ResponseWriter, r *http.Request) {
        var buf bytes.Buffer
        fr.WriteTo(&buf)
        w.Header().Set("Content-Type", "application/octet-stream")
        w.Write(buf.Bytes())
    })
}

Под капотом (internals)

Рантайм выделяет кольцевой буфер из блоков памяти. События планировщика и сетевого поллера записываются без системных вызовов в буфер текущего логического процессора P. При вызове fr.WriteTo() рантайм синхронизирует заголовки генераций трассировки и выдает валидный файл .out, полностью совместимый с go tool trace.

Типичные ошибки кандидатов

  • Попытка одновременно использовать старый trace.Start() и FlightRecorder: рантайм Go поддерживает только одного активного сборщика трассировки.
  • Опасения за неконтролируемый рост памяти: буфер строго ограничен по объему или временному окну (SetPeriod), старые события перезаписываются по кольцу.

Что такое профилирование goroutineleak в Go 1.26–1.27?

Краткий ответ (30–60 секунд)

В Go 1.26 (экспериментально) и Go 1.27 (Generally Available) в пакет runtime/pprof и эндпоинт /debug/pprof/goroutineleak добавлен специализированный профилировщик зависших горутин (Goroutine Leak Profiling). В отличие от стандартного профиля goroutine (который выводит стеки всех существующих горутин, включая миллионы полезных воркеров), профиль goroutineleak анализирует граф достижимости объектов сборщика мусора и фильтрует только те горутины, которые заблокированы на каналах или мьютексах, ссылки на которые полностью утеряны программой (то есть горутины, которые гарантированно никогда не смогут проснуться).

Развёрнутый ответ

Классический поиск утечек горутин в высоконагруженных системах превращался в ад: при 100 000 активных горутинах дамп /debug/pprof/goroutine?debug=2 весил десятки мегабайт текста, где 99.9% были штатными воркерами пулов.

Профиль goroutineleak запускает алгоритм обратного анализа достижимости:

Bash
# Получение только гарантированно утёкших горутин
curl http://localhost:6060/debug/pprof/goroutineleak?debug=1

Либо программно:

Go
import "runtime/pprof"

func dumpLeakedGoroutines() {
    p := pprof.Lookup("goroutineleak")
    if p != nil {
        p.WriteTo(os.Stdout, 1)
    }
}

Под капотом (internals)

В фазе разметки сборщика мусора (Mark phase) рантайм отслеживает корневые указатели и живые объекты кучи:

  1. Если горутина спит на операции chan receive (runtime.chanrecv), она зарегистрирована в hchan.recvq как sudog.
  2. Если на сам объект hchan больше нет сильных ссылок ни из стеков других горутин, ни из глобальных переменных — канал становится недостижимым мусором.
  3. Горутина признается «навечно застрявшей» (leaked), и ее стек помещается в профиль утечек для немедленного устранения бага в коде.

Типичные ошибки кандидатов

  • Путаница с debug/pprof/goroutine: кандидаты не понимают разницы: goroutine показывает снимок всех потоков, а goroutineleak — автоматический детектор мертвого кода с помощью reachability-графа GC.
  • Ожидание мгновенного обнаружения: для детекции утери ссылок на канал или мьютекс должен пройти хотя бы один цикл сборки мусора (GC cycle).