---
title: "Встроенные метрики процесса Go"
description: "Go метрики процесса в /metrics: go_goroutines, go_gc_duration_seconds и process_* бесплатно — что означает каждая серия и когда пора добавлять свои."
course: "Мониторинг Go с Prometheus: метрики, Grafana, алерты"
url: "https://golangmaster.ru/courses/prometheus/lessons/11-default-metriki/"
published: "2026-09-19"
updated: "2026-09-24"
minutes: 16
---

# Встроенные метрики процесса Go

*Курс «Мониторинг Go с Prometheus: метрики, Grafana, алерты», урок 11 из 25. ~16 минут. Обновлено 24.09.2026. Источник: https://golangmaster.ru/courses/prometheus/lessons/11-default-metriki/*

Среди go метрик процесса, которые client_golang отдаёт сразу после запуска, есть всё для первой линии обороны: горутины, память, паузы сборщика мусора, файловые дескрипторы и процессорное время. Ни одна из этих серий не потребовала от вас ни строчки кода: два встроенных коллектора регистрируются в реестре сами. Разберём, что лежит в /metrics бесплатно, как читать каждую группу серий и в какой момент встроенного набора перестаёт хватать.

Откройте /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, в котором нет ни одной вашей метрики

```go
package 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_ серии ещё до любого вашего запроса.

### Как читать экспозицию встроенных серий

```go
go_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, — но это опциональный слой, для старта хватает базовой пары.

### Свой реестр с полным набором

```go
var 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 иногда неделями тишина, а линия-то уже ползёт. Разбор ловушек с каналами и счётчиками собственных горутин — в [уроке про метрики горутин](/courses/prometheus/lessons/16-metriki-gorutin/), а основы самих горутин при желании освежите в [курсе продвинутого Go](/courses/go-advanced/lessons/04-gorutiny/).

### 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, прежде чем списывать всё на троттлинг.

> **Память: alloc падает, а сервис умирает.** Классика разбора инцидентов: 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](/courses/prometheus/lessons/13-grafana-obzor/).

### Когда встроенного набора мало

Встроенные метрики отвечают на вопрос «здоров ли процесс», но не на вопрос «здоров ли сервис». В них нет доли ошибок по маршрутам, нет латентности обработки, нет числа заказов и конверсий — всё это ваши метрики из предыдущих уроков. Практичный порядок такой: встроенный набор включён всегда, он бесплатен и ловит проблемы самого рантайма; поведенческие метрики вы добавляете по мере того, как сервис обрастает логикой, начиная с [бизнес-событий](/courses/prometheus/lessons/10-biznes-metriki/). Полезно и обратное знание: если тестовый сервис внезапно отдаёт тысячи серий, проверьте, не создаёте ли вы вектор на каждый запрос — MustRegister паникует на дубликатах регистрации, а вот новый вектор с тем же именем плодит серии молча. Разделение «процесс» и «поведение» экономит время разбора: процесс здоров, а сервис болеет — виновата логика, и искать нужно в собственных сериях.

### Проверка: тип метрики

**Вопрос:** К какому типу относится go_goroutines?

- Counter
- ✅ Gauge
- Histogram
- Summary

Число горутин — текущее состояние процесса: оно и растёт, и убывает, поэтому gauge. Счётчик предполагает монотонный рост с момента старта, а горутины завершаются в каждый момент времени.

### Проверка: свой реестр

**Вопрос:** Вы создали prometheus.NewRegistry(), зарегистрировали в нём только свой счётчик и экспортировали через HandlerFor. Что увидит скрейп?

- Полный набор: go_, process_ и счётчик
- ✅ Только ваш счётчик: дефолтные коллекторы в новый реестр не попадают
- Пустой ответ: реестр требует регистрации GoCollector до создания метрик
- Ошибку скрейпа: HandlerFor работает только с DefaultGatherer

NewRegistry возвращает пустой набор, и go_ с process_ сериями в нём нет, пока вы явно не зарегистрировали NewGoCollector и NewProcessCollector. Скрейп при этом пройдёт нормально: реестр отдаст одну вашу метрику. Глобальный реестр продолжит жить своей жизнью — HandlerFor никак с ним не связан.

### Научная нотация

```go
process_resident_memory_bytes 3.2e+07
```

**Что выведет программа?**

- 3.2 мегабайта
- ✅ 32 мегабайта
- 320 мегабайт
- 3200 байт

3.2e+07 — это 3.2 на десять в седьмой, то есть около 32 миллионов байт, примерно 32 мегабайта. Экспозиция Prometheus пишет большие числа в научной нотации, и конвертация в голове экономит минуту на каждом разборе.

### Соберите свой реестр

Допишите создание изолированного реестра с рантайм-коллектором и его экспорт.

```go
var reg = prometheus.___()

reg.MustRegister(
        prometheus.___(),
        prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}),
        ordersTotal,
)

http.Handle("/metrics", promhttp.___(reg, promhttp.HandlerOpts{}))
```

Подсказка: Пустой реестр создаёт NewRegistry, рантайм-коллектор возвращает NewGoCollector, а экспорт конкретного реестра выполняет HandlerFor.

Правильные ответы в пропусках: `NewRegistry`, `NewGoCollector`, `HandlerFor`

### Порядок регистрации

Расставьте строки сервиса с изолированным реестром в правильном порядке.

Правильный порядок строк:

1. `reg := prometheus.NewRegistry()`
2. `ordersTotal := prometheus.NewCounter(prometheus.CounterOpts{Name: "orders_total"})`
3. `reg.MustRegister(prometheus.NewGoCollector())`
4. `reg.MustRegister(ordersTotal)`
5. `http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))`
6. `http.ListenAndServe(":8080", nil)`

### Карточки: встроенные метрики

- **go_goroutines** — Gauge из GoCollector: число живых горутин на момент скрейпа. Стабильный профиль — норма; ступенчатый рост без возврата — признак утечки.
- **go_gc_duration_seconds** — Summary пауз сборщика мусора с квантилями 0, 0.25, 0.5, 0.75, 1. Квантиль 1 — худшая пауза за жизнь процесса.
- **process_cpu_seconds_total** — Counter из ProcessCollector: суммарное процессорное время с начала работы. rate по нему даёт загрузку CPU в ядрах.
- **process_open_fds** — Gauge: открытые файловые дескрипторы. Вечный рост означает незакрытые файлы, сокеты и тела HTTP-ответов.
- **DefaultRegisterer** — Глобальный реестр client_golang: GoCollector и ProcessCollector зарегистрированы в нём по умолчанию, экспорт — promhttp.Handler().
- **NewRegistry и HandlerFor** — Пара для изолированных наборов: prometheus.NewRegistry() и promhttp.HandlerFor(reg, ...). Дефолтные go_ и process_ серии возвращают явно.

Следующий шаг — [урок про PromQL](/courses/prometheus/lessons/12-promql-basics/): все серии из таблицы выше станут аргументами запросов rate и histogram_quantile, а пока закрепите чтение экспозиции. Для этого нужен локальный Go: браузерная песочница сайта client_golang не исполняет, поэтому запустите минимальный сервис из этого урока и почитайте /metrics глазами. Сравните go_memstats_alloc_bytes и process_resident_memory_bytes до и после нагрузки из hey — разница в поведении кучи и RSS станет нагляднее любой теории. Заодно посмотрите на go_gc_duration_seconds{quantile="1"} до и после: худшая пауза покажет, насколько сборщик мешает латентности. Полная программа курса — на [странице Prometheus](/courses/prometheus/); если вы перескакивали уроки, вернитесь к материалу про [первый /metrics endpoint](/courses/prometheus/lessons/03-ustanovka-client-golang/), где дефолтный реестр фигурирует впервые.

## Коротко: что запомнить

- 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 о дубликате.
