Метрики Prometheus в Go-сервисе: чек-лист перед продакшеном
Гистограмма с дефолтными бакетами врёт про перцентили, лейбл с ID запроса раздувает память, а /metrics открыт наружу. Разбираем, что проверить в инструментации Go-сервиса до выката.

Первый инцидент с метриками выглядит так: график p99 latency показывает ровные 100 мс, а пользователи жалуются на секундные ответы. Причина — дефолтные бакеты prometheus.DefBuckets заканчиваются на 10 секундах с крупным шагом, и всё, что между 100 мс и 250 мс, схлопнулось в один бакет. Перцентиль считается по границам бакетов, поэтому цифра на дашборде — не реальная задержка, а интерполяция по грубой сетке.
Это один из десятка способов получить метрики, которые технически работают, но врут или роняют сервис. Ниже — чек-лист для Go-сервиса на клиентской библиотеке prometheus/client_golang версии 1.19+ (актуальная линейка на конец 2024 года). Проверяется перед тем, как endpoint /metrics уедет в прод и на него подпишется реальный Prometheus со scrape-интервалом 15 секунд.
Кардинальность: главный способ уронить и себя, и Prometheus
Каждая уникальная комбинация значений лейблов — это отдельный временной ряд. Библиотека держит их все в памяти процесса, а Prometheus — в своей TSDB. Ряд с лейблом user_id или request_id порождает столько рядов, сколько у вас пользователей. Через сутки это гигабайты в heap Go-процесса и захлебнувшийся Prometheus.
Лейбл — это измерение, а не поле лога. Если значение почти уникально для каждого события, ему не место в метрике. Такие данные идут в логи или трейсы, а не в Prometheus.
Что проверить в каждом лейбле перед выкатом:
- Значение из ограниченного множества. HTTP-метод (около десятка), код ответа (несколько десятков), имя роута из шаблона (
/users/:id, а не/users/12345) — годятся. ID сущностей, email, URL с параметрами, полныйUser-Agent— нет. - Роут нормализован до шаблона. Если вы берёте
r.URL.Pathнапрямую, каждый ID в пути станет новым рядом. Используйте паттерн из роутера: вchiэтоchi.RouteContext(r.Context()).RoutePattern(), вgorilla/mux—mux.CurrentRoute(r).GetPathTemplate(). - Оценена верхняя граница. Перемножьте мощности всех лейблов метрики. 10 методов × 20 роутов × 15 кодов = 3000 рядов на одну метрику. Приемлемо. Если в произведении появляется что-то неограниченное — переделывайте.
Как это ловить до прода
Поднимите сервис локально, прогоните по нему нагрузочный сценарий с реалистичными данными (разные ID, ошибки, редкие роуты) и посмотрите на /metrics. Посчитайте строки: curl -s localhost:8080/metrics | wc -l. Если после сотни разных запросов число строк растёт линейно с числом запросов — где-то течёт кардинальность.
Гистограммы: бакеты под вашу задержку, а не дефолтные
Дефолтные бакеты рассчитаны на диапазон от 5 мс до 10 с. Для HTTP-API, где интересна разница между 20 мс и 200 мс, они бесполезны. Задайте бакеты под реальное распределение задержек сервиса.
package metrics
import "github.com/prometheus/client_golang/prometheus"
// Бакеты под API, где основная масса ответов 10-500 мс.
// Значения в секундах, потому что Prometheus меряет длительность в секундах.
var RequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Длительность обработки HTTP-запроса",
Buckets: []float64{0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5},
},
[]string{"method", "route", "status"},
)Проверьте по чек-листу:
- Границы покрывают ваш SLO. Если целитесь в p99 < 300 мс, нужен бакет рядом с 0.3 — иначе не увидите, попадаете вы в цель или нет.
- Единица измерения — секунды, а не миллисекунды. Экосистема Prometheus (Grafana-дашборды, алерты, правила
histogram_quantile) по соглашению ждёт секунды и суффикс_secondsв имени. - Число бакетов разумное. Каждый бакет × комбинация лейблов = отдельный ряд. Гистограмма с 12 бакетами и 3000 комбинаций лейблов — это уже под 40 тысяч рядов только на одну метрику.
Гистограмма или Summary
Соблазн взять Summary с готовыми квантилями велик: он считает перцентили на стороне клиента и не требует подбора бакетов. Но квантили из Summary нельзя агрегировать между инстансами — сложить p99 с трёх подов в общий p99 математически невозможно. Гистограмму агрегировать можно через histogram_quantile по сумме бакетов. Для сервиса, который работает в нескольких репликах, берите гистограмму почти всегда.
Регистрация метрик: без глобального Registry в тестах
prometheus.MustRegister паникует при повторной регистрации метрики с тем же именем. В тестах, где пакет инициализируется несколько раз, это ловит на duplicate metrics collector registration attempted. Регистрируйте метрики в конструкторе структуры и держите свой *prometheus.Registry, а не глобальный:
type Metrics struct {
RequestDuration *prometheus.HistogramVec
Registry *prometheus.Registry
}
func New() *Metrics {
reg := prometheus.NewRegistry()
m := &Metrics{
Registry: reg,
RequestDuration: prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Длительность обработки HTTP-запроса",
Buckets: []float64{0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5},
},
[]string{"method", "route", "status"},
),
}
reg.MustRegister(m.RequestDuration)
// Стандартные метрики процесса и рантайма Go.
reg.MustRegister(collectors.NewGoCollector())
reg.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
return m
}Пакет collectors — это github.com/prometheus/client_golang/prometheus/collectors. Точный набор экспортируемых конструкторов сверьте в документации вашей версии client_golang, поведение NewGoCollector менялось между версиями.
Handler и безопасность endpoint
Отдавайте метрики через promhttp.HandlerFor, привязанный к вашему registry, а не через глобальный promhttp.Handler():
mux.Handle("/metrics", promhttp.HandlerFor(
m.Registry,
promhttp.HandlerOpts{},
))Чек-лист по endpoint перед продом:
- /metrics не торчит в интернет. Метрики раскрывают внутреннее устройство: имена роутов, объёмы, коды ошибок. Слушайте их на отдельном порту, доступном только из внутренней сети или через сервисную сеть кластера. Публичный балансировщик на этот порт не смотрит.
- Endpoint не под общим middleware. Если ваш HTTP-middleware сам пишет метрику про каждый запрос, а Prometheus скрейпит
/metricsкаждые 15 секунд, вы получите ряды про скрейпы собственных метрик. Исключайте этот путь из инструментирующего middleware.
Метрики рантайма и бизнес-логики
NewGoCollector даёт горутины, heap, GC-паузы — это то, по чему вы будете дебажить утечки и всплески latency. Отдельно проверьте, что у вас есть метрики на границах с внешним миром:
- счётчик и гистограмма на исходящие вызовы к БД и внешним API — именно там прячется хвост latency;
- счётчик ошибок с лейблом типа ошибки (из ограниченного множества, не текст ошибки);
- gauge на размеры очередей, пулов соединений, размер воркер-пула.
Что чаще всего ломается на проде
- Рост RSS процесса. Обычно — незамеченная утечка кардинальности через лейбл, значение которого оказалось не таким ограниченным, как думали. Смотрите на число рядов конкретной метрики:
count({__name__=~"имя_метрики.*"})в самом Prometheus. - Счётчик, который сбрасывается. Counter должен только расти. Если вы где-то присваиваете ему значение вместо инкремента или пересоздаёте метрику в рантайме,
rate()покажет ложные всплески при каждом сбросе. - Дырки на графиках. Скрейп не успевает за scrape_timeout, если
/metricsгенерируется долго при десятках тысяч рядов. Это следствие проблемы с кардинальностью, а не самостоятельная беда. - Разъехавшиеся единицы. Часть метрик в миллисекундах, часть в секундах. Алерты и дашборды начинают врать в тысячу раз. Держите единый суффикс
_secondsи секунды везде.
Что сделать дальше
Перед выкатом прогоните endpoint под нагрузочным сценарием и посчитайте итоговое число рядов — это цифра, которую вы отдаёте в Prometheus навсегда. После выката настройте в самом Prometheus лимит на кардинальность через sample_limit в scrape-конфиге: он оборвёт скрейп, если инстанс вдруг начнёт отдавать взрывной рост рядов, и защитит хранилище от одного сломанного сервиса. Дальше — рекординг-правила для тяжёлых histogram_quantile, чтобы дашборды не считали перцентили на лету при каждой перерисовке.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: Prometheus Go client library — документация client_golang, Prometheus: Metric and label naming best practices
Частые вопросы
У меня client_golang старее 1.19, чек-лист подойдёт?
Что делать, если RSS процесса уже растёт из-за метрик на проде?
Гистограмма или Summary для перцентилей?
Не сработало: histogram_quantile показывает NaN или ровные числа
Насколько опасно оставить /metrics открытым наружу?
Сколько временных рядов на один сервис — это нормально?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.