17. Метрики и базовый monitoring
Лорд Кельвин однажды заметил: «Если вы не можете измерить то, о чем говорите, и выразить это в цифрах, ваше знание скудно и неудовлетворительно». В распределенных системах эта истина становится вопросом выживания.
Если логи можно сравнить с «черным ящиком» самолета, который расшифровывают уже после катастрофы, то метрики — это приборная панель в кабине пилота. Они показывают текущую высоту, скорость, температуру турбин и давление масла в реальном времени.
Метрики — это формализованный способ измерять поведение системы во времени. В отличие от логов, которые рассказывают что случилось, метрики показывают как система ведет себя под нагрузкой. В Go-экосистеме де-факто стандартом стала связка prometheus/client_golang + Pull-модель сбора. Правильная настройка метрик позволяет предсказывать сбои до их наступления, диагностировать утечки памяти и оптимизировать узкие места без гадания на кофе.
flowchart LR
A["Go Сервис<br>накопление метрик в памяти"] -->|"HTTP GET /metrics"| B["Prometheus<br>скрейпер"]
B -->|"Парсинг текста<br>экспозишн формат"| C["TSDB<br>хранение временных рядов"]
C -->|"PromQL запросы"| D["Grafana<br>визуализация"]
classDef srv fill:#064e3b,stroke:#10b981,stroke-width:2px,color:#f8fafc;
classDef prom fill:#0f172a,stroke:#6366f1,stroke-width:2px,color:#f8fafc;
classDef tsdb fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#f8fafc;
class A srv;
class B,C,D prom;
1. Pull-модель и архитектура сбора
В классических push-системах (статистика, старые APM) клиент сам отправляет данные на сервер. Это создает проблемы с backpressure: если сервер метрик тормозит, клиент тратит CPU на буферизацию и retry, замедляя бизнес-логику.
Prometheus использует Pull-модель: сервер самостоятельно скрейпит HTTP-эндпоинт /metrics целевых сервисов с заданным интервалом (обычно 15-60 секунд).
2. Типы метрик и семантика
Выбор типа метрики диктует, как данные будут агрегироваться в TSDB и какие функции PromQL к ним применимы.
- Counter: Только монотонно растет. Сбрасывается только при рестарте процесса. Используется для подсчета событий: количество запросов, ошибок, обработанных сообщений. В PromQL используется
rate()илиincrease()для получения RPS. - Gauge: Может расти и падать. Отражает текущее состояние: количество активных горутин, использование памяти, длина очереди, температура CPU.
- Histogram: Агрегирует значения в предопределенные бакеты. Идеален для измерения длительности запросов или размеров payload. Позволяет вычислять процентили через
histogram_quantile(). - Summary: Клиентский расчет процентилей. Дает точные значения, но не агрегируется по лейблам и не может быть усреднен. В Go-экосистеме почти всегда заменяется на Histogram из-за проблем с агрегацией в кластере.
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
HTTPRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
}, []string{"method", "handler", "status_code"})
HTTPRequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Duration of HTTP requests in seconds",
Buckets: prometheus.DefBuckets, // [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
}, []string{"method", "handler"})
)
3. Под капотом. Атомики, блокировки и кэш-линии
Клиентская библиотека prometheus/client_golang оптимизирована для работы в конкурентной среде, но ее устройство напрямую влияет на производительность сервиса.
Каждая метрика хранится в памяти процесса. При вызове .Inc() или .Observe():
- Библиотека находит нужный вектор по комбинации лейблов (использует
sync.Mapили мьютекс для поиска) - Обновляет внутреннее значение с помощью
sync/atomic - Возвращает управление горутине
Для Histogram запись сложнее: библиотека вычисляет индекс бакета (часто через math.Log или бинарный поиск) и атомарно инкрементирует счетчик конкретного бакета. Это дороже, чем Counter.Inc(), но необходимо для процентилей.
4. Интеграция в HTTP-сервис
Идиоматичный способ добавить метрики — использовать http.Handler обертку или promhttp инструменты.
package main
import (
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func setupMetricsHandler(mux *http.ServeMux) {
// Эндпоинт для Prometheus
mux.Handle("/metrics", promhttp.HandlerFor(
prometheus.DefaultGatherer,
promhttp.HandlerOpts{
EnableOpenMetrics: true, // Оптимизация для современных скрейперов
ErrorLog: customLogger{},
},
))
}
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Создаем обертку для захвата статуса ответа
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(rw, r)
duration := time.Since(start).Seconds()
// Запись метрик (без блокировки основного потока)
HTTPRequestsTotal.With(prometheus.Labels{
"method": r.Method,
"handler": r.URL.Path,
"status_code": http.StatusText(rw.statusCode),
}).Inc()
HTTPRequestDuration.With(prometheus.Labels{
"method": r.Method,
"handler": r.URL.Path,
}).Observe(duration)
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
5. Опасность высокой кардинальности и GC-давление
Кардинальность метрики — это количество уникальных комбинаций лейблов. Это самая опасная метрика в production.
// АНТИПАТТЕРН: Кардинальный взрыв
HTTPRequestsTotal.With(prometheus.Labels{
"user_id": userID, // 1 000 000 уникальных пользователей
"request_id": reqID, // 10 000 000 уникальных запросов
}).Inc()
Каждая уникальная комбинация создает новый объект в памяти (запись в sync.Map или map, структура метрики). При высокой кардинальности:
- OOM в сервисе: Go-процесс расходует гигабайты RAM на хранение лейблов и значений.
- GC Pressure: Сборщик мусора вынужден сканировать миллионы живых объектов. Это увеличивает паузы STW (Stop-The-World) и снижает пропускную способность.
- Crash Prometheus: Скрейпер пытается загрузить миллионы временных рядов в свою TSDB, что приводит к OOM на стороне мониторинга.
6. Практические правила эксплуатации
- Лейблы должны быть bounded: Только известные конечные множества (
method,status_code,service_name,handler). Никогда не используйтеuser_id,email,ip,uuidв лейблах. - Именование:
namespace_subsystem_name_unit(например,http_request_duration_seconds). Единицы измерения всегда в суффиксе. - Goroutine/Memory метрики: Рантайм Go уже экспортирует
go_goroutines,go_memstats_*. Не дублируйте их вручную. - Таймауты на скрейпинге: Настройте
scrape_timeoutв Prometheus чуть больше времени выполнения/metrics, но меньшеscrape_interval.
Итог
- Pull-модель Prometheus защищает бизнес-логику от backpressure и упрощает архитектуру мониторинга.
- Counter для событий, Gauge для состояния, Histogram для распределений. Summary в Go-стеке практически не используется.
- Запись метрик использует атомарные операции, но при высокой конкуренции вызывает contention на кэш-линиях CPU.
- Высокая кардинальность лейблов — главный враг стабильности. Она ведет к OOM сервиса и краху TSDB.
/metricsendpoint должен быть легковесным и предсказуемым по времени выполнения.- Метрики проектируются до написания кода: лейблы, имена и бакеты согласуются командой на этапе design review.
Следующая статья: 18. Rate limiting