Метрики Prometheus в Go-сервисе: сравниваем варианты
В Go-экосистеме есть минимум три способа отдавать метрики Prometheus: официальный client_golang, OpenTelemetry SDK и VictoriaMetrics/metrics. Разбираем цену каждого по памяти, аллокациям и удобству, чтобы вы выбрали осознанно, а не по инерции.

Зачем выбирать, если есть «официальный» клиент
Большинство Go-сервисов подключают prometheus/client_golang — он упомянут в документации Prometheus, лежит в организации prometheus на GitHub и работает «из коробки». Но у него есть цена: высокие аллокации на каждый вызов Observe для гистограмм, тяжёлый реестр, скрытые мьютексы при сборе метрик. Если сервис отдаёт десятки тысяч уникальных лейбл-комбинаций — вы это почувствуете на /debug/pprof/heap.
Альтернативы существуют: OpenTelemetry Go SDK умеет экспортировать метрики в формат Prometheus, а пакет VictoriaMetrics/metrics сознательно жертвует совместимостью ради скорости. Ниже — сравнение трёх подходов по критериям, которые реально влияют на выбор в продакшене.
Участники сравнения
prometheus/client_golang
Репозиторий github.com/prometheus/client_golang. Де-факто стандарт. Поддерживает все типы метрик Prometheus: Counter, Gauge, Histogram, Summary. Начиная с версии 1.19 появились «нативные гистограммы» (Native Histograms, экспериментальная фича), которые резко сокращают кардинальность, но требуют поддержки на стороне сервера Prometheus 2.40+.
Главная боль — HistogramVec. Каждый вызов WithLabelValues проходит через sync.Map + хэширование лейблов. При высокой кардинальности это горячая точка.
OpenTelemetry Go SDK
Пакеты go.opentelemetry.io/otel/sdk/metric + экспортёр go.opentelemetry.io/otel/exporters/prometheus. OTel обещает vendor-neutral API: один код — экспорт в Prometheus, OTLP, Datadog и дальше. За универсальность платите слоем абстракции: Meter → Instrument → Aggregation → Export. Каждый слой — это аллокации и блокировки.
Формат экспозиции совместим с Prometheus: экспортёр поднимает обычный /metrics-эндпоинт. Но маппинг типов не всегда очевиден: OTel Histogram — это не Prometheus Histogram один в один, границы бакетов нужно задавать явно через View.
VictoriaMetrics/metrics
Пакет github.com/VictoriaMetrics/metrics. Минималистичный API, нет реестра в привычном смысле: метрики регистрируются глобально при первом вызове GetOrCreateCounter. Нет Vec-обёрток — лейблы вклеиваются в имя строкой: requests_total{method="GET"}. Это ограничение по дизайну: авторы считают, что динамическая кардинальность — ошибка пользователя.
Аллокации минимальны: горячий путь — атомарный инкремент без мьютексов.
Сравнительная таблица
| Критерий | client_golang | OTel SDK + exporter | VictoriaMetrics/metrics |
|---|---|---|---|
| Типы метрик | Counter, Gauge, Histogram, Summary, Native Histogram | Counter, UpDownCounter, Histogram (маппится в Prometheus-типы) | Counter, Gauge, Histogram, Summary |
| Лейблы | Vec-обёртки, динамические | Attributes (динамические) | Вклеиваются в имя строкой, фактически статические |
| Горячий путь (Counter.Inc) | atomic + поиск по map при WithLabelValues | atomic + поиск по атрибутам + слой агрегации | atomic, без поиска (метрика уже найдена) |
| Экспозиция | text/plain, OpenMetrics, protobuf | text/plain, OpenMetrics | text/plain |
| Vendor lock-in | Prometheus-only API | Vendor-neutral, сменить бэкенд — замена экспортёра | Prometheus-only API |
| Размер зависимостей (go.sum) | Умеренный | Большой (десятки транзитивных пакетов OTel) | Нулевой (без внешних зависимостей) |
| Совместимость с Grafana-дашбордами | Полная | Полная (имена метрик слегка отличаются по умолчанию) | Полная |
| Native Histograms | Да (экспериментально) | Нет | Нет |
Как выглядит код
Регистрация HTTP-счётчика запросов — самый частый кейс. Ниже три фрагмента, Go 1.22+.
client_golang (v1.20+)
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var httpRequests = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Количество HTTP-запросов",
},
[]string{"method", "path", "status"},
)
func init() {
prometheus.MustRegister(httpRequests)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/ping", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
httpRequests.WithLabelValues(r.Method, "/api/ping", "200").Inc()
})
mux.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8080", mux)
}
OTel SDK + Prometheus exporter (v0.50+)
package main
import (
"context"
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
otelprom "go.opentelemetry.io/otel/exporters/prometheus"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
)
func main() {
// Экспортёр сам регистрирует /metrics через prometheus/client_golang
exporter, _ := otelprom.New()
provider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(exporter))
otel.SetMeterProvider(provider)
meter := otel.Meter("myservice")
requestCounter, _ := meter.Int64Counter("http_requests_total")
mux := http.NewServeMux()
mux.HandleFunc("/api/ping", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
requestCounter.Add(r.Context(), 1,
api.WithAttributes(
attribute.String("method", r.Method),
attribute.String("path", "/api/ping"),
attribute.String("status", "200"),
),
)
})
// Эндпоинт /metrics уже зарегистрирован экспортёром
http.ListenAndServe(":8080", mux)
}
Обратите внимание: OTel-экспортёр под капотом использует client_golang, поэтому зависимость всё равно тянется. Конкретный API атрибутов может отличаться между минорными версиями OTel SDK — сверяйтесь с документацией go.opentelemetry.io для вашей версии.
VictoriaMetrics/metrics
package main
import (
"fmt"
"net/http"
"github.com/VictoriaMetrics/metrics"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/ping", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
// Лейблы — часть имени; метрика создаётся при первом вызове
name := fmt.Sprintf(`http_requests_total{method=%q,path="/api/ping",status="200"}`, r.Method)
metrics.GetOrCreateCounter(name).Inc()
})
mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
metrics.WritePrometheus(w, true)
})
http.ListenAndServe(":8080", mux)
}
Формирование имени через fmt.Sprintf на каждый запрос — аллокация. Если путь и метод известны заранее, лучше закешировать строку или сам счётчик в переменной.
Когда что выбирать
Универсальный клиент — миф. Выбор зависит от ответа на один вопрос: кто потребитель ваших метрик через год?
- client_golang — если вы точно останетесь в экосистеме Prometheus / Thanos / Mimir и хотите Native Histograms. Это самый документированный путь: любой пример из интернета написан для него.
- OTel SDK — если в компании принят OpenTelemetry как стандарт или сервис должен одновременно экспортировать метрики и трейсы через единый SDK. Будьте готовы к тому, что API ещё не полностью стабилен (на момент написания Metrics API помечен как stable, а SDK — stable с оговорками по отдельным фичам, сверяйтесь с
opentelemetry.io/docs/specs/otel/versioning). - VictoriaMetrics/metrics — если сервис высоконагруженный, кардинальность метрик контролируется жёстко и вам не нужен
Vec-стиль с динамическими лейблами. Идеален для sidecar-утилит и CLI-тулинга, где тянуть дерево зависимостей OTel — перебор.
Подводные камни в продакшене
Кардинальность убивает всех
Неважно, какой клиент вы выбрали: если в лейбл попадает user_id или trace_id, Prometheus при скрейпе получит миллионы временных рядов, а ваш сервис — OOM при сборе /metrics. client_golang позволяет ограничить это через ConstrainedLabels (появились в v1.19), у OTel есть View с фильтрацией атрибутов, а VictoriaMetrics/metrics не защищает никак — ответственность на вызывающем коде.
Стоимость /metrics при большом числе метрик
Сбор метрик — синхронная операция. client_golang при каждом скрейпе обходит реестр под мьютексом, сериализует результат в буфер и отдаёт. Если метрик десятки тысяч, эндпоинт /metrics может отвечать сотни миллисекунд и блокировать запись новых значений. Решения: выделенный реестр на подсистему, уменьшение кардинальности, переход на протобаф-формат экспозиции (быстрее парсится на стороне Prometheus).
OTel: неявное переименование метрик
OTel Prometheus exporter по умолчанию добавляет суффикс _total к счётчикам и меняет точки на подчёркивания в именах. Если вы мигрируете с client_golang и у вас уже настроены алерты в Alertmanager — имена разъедутся. Проверяйте /metrics после миграции и корректируйте PromQL-запросы.
Сноска: кардинальность (cardinality) метрик — количество уникальных комбинаций значений лейблов для одной метрики. Кардинальность в 10 000 для одного имени метрики — уже повод насторожиться; в 100 000 — почти гарантированная проблема с памятью Prometheus.
Что делать дальше
Выбрав клиент, не останавливайтесь на счётчиках запросов. Следующий шаг — настроить гистограммы латентности с осмысленными бакетами (не дефолтные 0.005–10 секунд, а подогнанные под SLO вашего сервиса) и подключить алерты по RED-метрикам (Rate, Errors, Duration). Если выбрали OTel — сразу настройте корреляцию trace_id с метриками через exemplars: это единственный способ быстро перейти от всплеска на графике к конкретному трейсу. И в любом случае добавьте в CI проверку, что /metrics отдаёт 200 и формат валиден — сломанный эндпоинт метрик вы заметите последним, когда алерты уже перестанут приходить.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: prometheus/client_golang — GitHub, OpenTelemetry Go — Prometheus Exporter
Частые вопросы
Можно ли использовать client_golang и OTel SDK одновременно?
Что будет, если Prometheus не успевает скрейпить /metrics за таймаут?
У VictoriaMetrics/metrics нет Vec — как мне работать с динамическими лейблами?
Версия OTel SDK сильно влияет на API — как не сломаться при обновлении?
А если у меня другая версия Go или библиотеки и примеры не компилируются?
Какие накладные расходы на метрики реалистичны?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.