Содержание урока · 10 разделов
- Go метрики процесса: состав встроенного набора
- Свой реестр: NewRegistry и потерянные дефолты
- Goroutines метрика: индикатор утечки под рукой
- go_threads, GOMAXPROCS и процессор в контейнере
- Шпаргалка: встроенные серии и их смысл
- Первые графики из встроенных серий
- Когда встроенного набора мало
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Откройте /metrics сервиса из третьего урока и посчитайте строки, которые вы не писали: их окажется больше, чем написанных. Несколько десятков серий с префиксами go_ и process_ появляются при старте процесса, и это не мусор, а результат работы двух коллекторов, которые client_golang регистрирует в реестре по умолчанию. GoCollector снимает данные из рантайма: горутины, потоки операционной системы, статистику кучи и сборщика мусора. ProcessCollector читает то, что операционная система знает о процессе: процессорное время, резидентную память, открытые файловые дескрипторы, момент старта. Разделение не случайное: префикс go_ — внутренний мир рантайма Go, префикс process_ — то, как процесс выглядит снаружи. Первые рассказывают о поведении вашей программы, вторые — о потреблении ресурсов. Практический вывод: если сервис из урока про первый /metrics endpoint уже поднят, половина мониторинга у вас есть, и осталось только научиться её читать.
Go метрики процесса: состав встроенного набора#
Самая частая серия — go_goroutines: gauge с числом живых горутин на момент скрейпа. Рядом go_threads: потоки ОС, которые рантайм держит под планировщик; их число растёт при блокирующих системных вызовах и возвращается не всегда — это нормально. Память занимает целая семья go_memstats_*: go_memstats_alloc_bytes — байты под живые объекты, go_memstats_sys_bytes — сколько памяти рантайм взял у системы, go_memstats_heap_inuse_bytes — куча, находящаяся в работе. Сборщик мусора представлен серией go_gc_duration_seconds — это summary с квантилями 0, 0.25, 0.5, 0.75 и 1: квантиль 1 показывает худшую паузу за жизнь процесса, а через _sum и _count выводится средняя. Отдельная серия go_info с лейблом версии — gauge со значением 1, маркирующий, каким компилятором собран процесс. Из process_-набора чаще всего смотрят process_cpu_seconds_total, process_resident_memory_bytes, process_open_fds и process_start_time_seconds — unix-время старта, из которого делается график аптайма.
Endpoint, в котором нет ни одной вашей метрики
GOpackage main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8080", nil)
}Шесть строк, а /metrics уже отдаёт десятки серий. promhttp.Handler() собирает метрики из DefaultGatherer — глобального реестра, в котором при импорте пакета уже зарегистрированы GoCollector и ProcessCollector. Проверьте локальным curl: curl localhost:8080/metrics | head — среди первых строк будут go_ и process_ серии ещё до любого вашего запроса.
Как читать экспозицию встроенных серий
GOgo_goroutines 42
go_threads 8
go_gc_duration_seconds{quantile="0.5"} 0.000231
go_gc_duration_seconds{quantile="1"} 0.004012
go_memstats_alloc_bytes 8.7e+06
go_info{version="go1.22.5"} 1
process_cpu_seconds_total 12.4
process_resident_memory_bytes 3.2e+07
process_open_fds 27
process_start_time_seconds 1.7212e+09Числа записаны в научной нотации: 8.7e+06 — это 8.7 мегабайта кучи, 3.2e+07 — около 32 мегабайт резидентной памяти, 1.7212e+09 — unix-время старта. Типы различаются по содержанию: go_goroutines и process_open_fds — gauge, process_cpu_seconds_total — counter, значения которого растут и с дробной частью. process_start_time_seconds редко смотрят напрямую: из него строят аптайм выражением time() - process_start_time_seconds.
Свой реестр: NewRegistry и потерянные дефолты#
Глобальный реестр удобен приложению, но библиотеке он не подходит: регистрируя метрики в DefaultRegisterer, вы мусорите в чужом процессе. Поэтому в библиотеках и тестах берут собственный: prometheus.NewRegistry() возвращает пустой изолированный набор, в котором есть ровно то, что вы зарегистрировали сами. И здесь поджидает ловушка из заголовка: go_ и process_ серии в новом реестре не появятся, потому что их туда никто не положил. Возвращаются они явно: prometheus.NewGoCollector() и prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}) регистрируются в вашем реестре вместе с бизнес-метриками. Пустые ProcessCollectorOpts означают отсутствие namespace — имена останутся process_, а не myapp_process_. Для экспорта реестра нужен уже не promhttp.Handler(), а promhttp.HandlerFor(reg, promhttp.HandlerOpts{}), который собирает выдачу именно из вашего набора. В свежих версиях client_golang есть и тонкая настройка через пакет collectors — дополнительные метрики рантайма по runtime/metrics, — но это опциональный слой, для старта хватает базовой пары.
Свой реестр с полным набором
GOvar reg = prometheus.NewRegistry()
var ordersTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{Name: "orders_total", Help: "Заказы"},
[]string{"status"},
)
func main() {
reg.MustRegister(
prometheus.NewGoCollector(),
prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}),
ordersTotal,
)
http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
http.ListenAndServe(":8080", nil)
}Реестр — обычная переменная пакета: его удобно передавать в тесты и собирать под конкретный набор. NewGoCollector и NewProcessCollector создаются при регистрации, хранить их в отдельных переменных незачем. HandlerFor вместо Handler — принципиальная строка: без неё вы снова экспортировали бы глобальный реестр, и заказы из ordersTotal не попали бы в выдачу.
Goroutines метрика: индикатор утечки под рукой#
Из всего встроенного набора чаще всего спасает именно goroutines метрика. Нормальный профиль сервиса стабилен: число горутин зависит от пула воркеров и наплыва трафика, поднимается в пик-час, возвращается к ночной отметке. Опасная картина другая: линия растёт ступеньками после каждого релиза и никогда не опускается — значит, какая-то горутина не завершается. Классические причины — бесконечный цикл без условия выхода, чтение из канала, который никто не закроет, запрос без таймаута во внешний API. Каждая зависшая горутина висит и в памяти. График go_goroutines за неделю находит такую утечку раньше алерта по памяти: горутины занимают килобайты, и до OOM иногда неделями тишина, а линия-то уже ползёт. Разбор ловушек с каналами и счётчиками собственных горутин — в уроке про метрики горутин, а основы самих горутин при желании освежите в курсе продвинутого Go.
go_threads, GOMAXPROCS и процессор в контейнере#
Пара go_goroutines и go_threads демонстрирует модель M:N: горутин могут быть тысячи, а потоков ОС — в пределах пары десятков, потому что планировщик рантайма мультиплексирует первые на вторые. Скачок go_threads без роста горутин подсказывает на блокирующие системные вызовы: файловые операции и cgo держат поток занятым. Процессор же честнее смотреть по process_cpu_seconds_total: rate(process_cpu_seconds_total[5m]) даёт потребление в ядрах, и в контейнере это число сравнивают с лимитом из cgroup. Нюанс, о котором больно узнают на дежурстве: исторически рантайм читал GOMAXPROCS из числа ядер машины и не знал о лимитах контейнера, поэтому подожатый лимитом сервис строил очереди планировщика внутри себя — деградация латентности при спокойном CPU-графике. В свежих версиях Go поведение починили, но проверьте версию рантайма через лейбл go_info, прежде чем списывать всё на троттлинг.
Классика разбора инцидентов: go_memstats_alloc_bytes стоит на месте, а контейнер уходит в OOMKilled. Обе метрики честны — просто они про разное. alloc — живые объекты кучи, которые GC держит в узде; process_resident_memory_bytes — память, которую процесс реально занял у ОС, и её рантайм возвращает не сразу и не всю. Куча выросла под ночным джобом, GC всё подчистил, alloc упал, а RSS остался на пике. Смотрите обе серии и их разрыв: стабильный зазор — норма, растущий — повод лезть в профили и настройки рантайма. И помните про сжатие: большая куча при маленьком alloc — чаще артефакт стратегии возврата памяти, чем утечка.
Шпаргалка: встроенные серии и их смысл#
| Серия | Тип | О чём сигналит |
|---|---|---|
| go_goroutines | gauge | число горутин: ступенчатый рост без возврата — утечка |
| go_gc_duration_seconds | summary | паузы GC: квантиль 1 — худшая за жизнь процесса |
| go_memstats_alloc_bytes | gauge | живые объекты кучи |
| process_cpu_seconds_total | counter | суммарное процессорное время с начала работы |
| process_resident_memory_bytes | gauge | память, занятая у операционной системы |
| process_open_fds | gauge | дескрипторы: вечный рост — незакрытые файлы и соединения |
| process_start_time_seconds | gauge | время старта: база для графика аптайма |
Первые графики из встроенных серий#
Четыре графика собираются из встроенных серий без единой собственной метрики, и каждый закрывает частый вопрос дежурства. Скорость потребления CPU: rate(process_cpu_seconds_total[5m]) — линия в ядрах, на которой троттлинг виден как упор в лимит. Резидентная память: process_resident_memory_bytes в динамике — медленный вечный рост при ровной куче подсказывает фрагментацию или побочный эффект очередей. Дескрипторы: process_open_fds за неделю — идеальный детектор незакрытых файлов и тел ответов, потому что дескрипторы конечны и лимит на них у процесса жёсткий. Аптайм: time() - process_start_time_seconds рисует пилу перезапусков — на одном экране видно, сколько раз сервис перезапустился за сутки, даже если никто не вёл журнал. Эти четыре панели стоит сделать первыми: они работают для любого Go-сервиса одинаково, и шаблон для них живёт в уроке про Grafana.
Когда встроенного набора мало#
Встроенные метрики отвечают на вопрос «здоров ли процесс», но не на вопрос «здоров ли сервис». В них нет доли ошибок по маршрутам, нет латентности обработки, нет числа заказов и конверсий — всё это ваши метрики из предыдущих уроков. Практичный порядок такой: встроенный набор включён всегда, он бесплатен и ловит проблемы самого рантайма; поведенческие метрики вы добавляете по мере того, как сервис обрастает логикой, начиная с бизнес-событий. Полезно и обратное знание: если тестовый сервис внезапно отдаёт тысячи серий, проверьте, не создаёте ли вы вектор на каждый запрос — MustRegister паникует на дубликатах регистрации, а вот новый вектор с тем же именем плодит серии молча. Разделение «процесс» и «поведение» экономит время разбора: процесс здоров, а сервис болеет — виновата логика, и искать нужно в собственных сериях.
Проверка: тип метрики
К какому типу относится go_goroutines?
Проверка: свой реестр
Вы создали prometheus.NewRegistry(), зарегистрировали в нём только свой счётчик и экспортировали через HandlerFor. Что увидит скрейп?
Научная нотация
Прочитайте код и выберите, что он выведет:
process_resident_memory_bytes 3.2e+07Соберите свой реестр
Допишите создание изолированного реестра с рантайм-коллектором и его экспорт.
Порядок регистрации
Расставьте строки сервиса с изолированным реестром в правильном порядке.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: встроенные метрики
Следующий шаг — урок про PromQL: все серии из таблицы выше станут аргументами запросов rate и histogram_quantile, а пока закрепите чтение экспозиции. Для этого нужен локальный Go: браузерная песочница сайта client_golang не исполняет, поэтому запустите минимальный сервис из этого урока и почитайте /metrics глазами. Сравните go_memstats_alloc_bytes и process_resident_memory_bytes до и после нагрузки из hey — разница в поведении кучи и RSS станет нагляднее любой теории. Заодно посмотрите на go_gc_duration_seconds{quantile="1"} до и после: худшая пауза покажет, насколько сборщик мешает латентности. Полная программа курса — на странице Prometheus; если вы перескакивали уроки, вернитесь к материалу про первый /metrics endpoint, где дефолтный реестр фигурирует впервые.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- GoCollector и ProcessCollector регистрируются в реестре по умолчанию: десятки серий go_ и process_ в /metrics без единой строки кода.
- go_goroutines — главный индикатор утечки: ступенчатый рост без возврата означает незавершающиеся горутины.
- go_gc_duration_seconds — summary пауз GC; process_cpu_seconds_total — counter процессорного времени; RSS смотрят по process_resident_memory_bytes.
- Свой реестр prometheus.NewRegistry() пуст: go_ и process_ возвращают явно через NewGoCollector и NewProcessCollector.
- Экспорт изолированного реестра — promhttp.HandlerFor, глобального — promhttp.Handler().
- alloc и RSS — про разное: живые объекты кучи против памяти, занятой у ОС; смотрят обе серии.
- Встроенные метрики отвечают за здоровье процесса; поведение сервиса описывают ваши собственные метрики.
Домашнее задание
Запустите локально минимальный сервис урока и изучите /metrics: найдите go_goroutines, go_gc_duration_seconds и process_start_time_seconds. Соберите изолированный реестр с NewGoCollector, NewProcessCollector и своим счётчиком, убедитесь curl-ом, что в выдаче есть и встроенные, и ваши серии. Потом уберите GoCollector из регистрации и посмотрите, что исчезнет. Песочница client_golang не исполняет — нужен локальный Go.
Личные заметкиПусто
Вопросы и ответы по уроку
Сколько серий добавляют встроенные коллекторы?
Около сорока суммарно на процесс: несколько десятков go_-серий, включая память и GC, плюс процессорное время, память, дескрипторы и время старта из process_-набора. Объём стабилен и не растёт со временем, поэтому встроенный набор включают всегда.
process_ метрики одинаковы на всех платформах?
Нет. На Linux ProcessCollector читает /proc и отдаёт полный набор; на других ОС часть серий может отсутствовать или считаться иначе. Прод-мониторинг почти всегда Linux, так что расхождение затрагивает в основном локальную разработку.
Нужно ли регистрировать GoCollector в глобальном реестре самому?
Нет, при импорте client_golang дефолтный реестр уже содержит GoCollector и ProcessCollector. Явная регистрация нужна только в собственных реестрах — повторная в глобальном приведёт к панике MustRegister о дубликате.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
WebSocket в Go: real-time чаты и уведомления
Курс WebSocket на Go: gorilla/websocket, эхо-сервер, hub-паттерн, комнаты, авторизация соединений, ping/pong, reconnect и масштабирование. 25 уроков.
- Поднимать WS-сервер и апгрейдить HTTP-соединения
- Писать readPump/writePump без гонок на каждое соединение
- Строить hub: регистрацию, рассылку и комнаты
- Авторизовывать соединения и ограничивать сообщения