Олег Марков
Мониторинг производительности - практическое руководство для разработчиков
Введение
Мониторинг производительности (performance monitoring) — это не просто набор графиков и дашбордов. Это системный подход, который помогает вам понимать, как реально работает ваше приложение под нагрузкой, где оно тратит время и ресурсы, и почему пользователи иногда видят «крутящийся спиннер» вместо быстрого отклика.
Если вы когда-либо:
- оптимизировали запрос «на глаз»;
- пытались угадать, где «тормозит» система;
- ловили проблемный релиз уже в продакшене;
то вам нужен продуманный мониторинг производительности.
Давайте разберемся, что именно мониторить, как это делать технически и какие инструменты использовать, чтобы быстрее находить и устранять узкие места.
Основные цели мониторинга производительности
Зачем он нужен разработчику и команде
Мониторинг производительности решает несколько практических задач:
Раннее обнаружение деградаций
Например, время ответа API выросло с 100 до 400 мс — вы увидите это до того, как пользователи начнут массово жаловаться.Поиск узких мест
Понимание, где именно теряется время: в базе данных, в очереди, в внешнем API, в GC, в сетевом слое и т.д.Подготовка к росту нагрузки
Вы можете оценить, как система ведет себя при 100, 1000 и 10 000 запросов в секунду, и что нужно масштабировать.Подтверждение эффектов оптимизаций
Любая оптимизация должна быть измерима: до и после. Мониторинг дает цифры, а не ощущения.Поддержка SLO / SLA
Если вы обещаете, что 95 процентов запросов обрабатываются быстрее 300 мс, вам нужны метрики, подтверждающие это в реальном времени.
Типы мониторинга производительности
Инфраструктурный мониторинг
Здесь вы следите за состоянием серверов, контейнеров, сетей:
- загрузка CPU;
- использование памяти;
- диск (IOPS, latency, заполненность);
- сеть (пропускная способность, ошибки, задержки);
- состояние контейнеров / pod’ов.
Этот уровень отвечает на вопрос:
«Хватает ли ресурсов машине, на которой все крутится?»
Мониторинг приложений (APM)
APM (Application Performance Monitoring) позволяет увидеть:
- время обработки запросов (endpoint’ы, методы, маршруты);
- трассировки (traces) — поэтапный путь запроса через сервисы;
- медленные SQL-запросы;
- ошибки и исключения;
- потребление ресурсов именно приложением (CPU, память, GC и т.д.).
Здесь вы отвечает на вопрос:
«Где именно в коде или в цепочке микросервисов тратится время?»
Мониторинг баз данных
Вы смотрите:
- время исполнения запросов;
- план выполнения (explain);
- блокировки (locks);
- кеширование запросов;
- нагрузку на диск и буферный кеш.
Этот уровень отвечает:
«Почему база данных отвечает медленно и какие запросы в этом виноваты?»
Мониторинг с точки зрения пользователя (RUM, Synthetic)
Есть два подхода:
RUM (Real User Monitoring) — сбор данных прямо из браузера или мобильного приложения:
- TTFB (time to first byte);
- FCP (First Contentful Paint);
- LCP (Largest Contentful Paint);
- CLS, FID, INP и другие Web Vitals.
Synthetic monitoring — роботы сами ходят по вашему сайту / API:
- делают регулярные проверки;
- измеряют время ответа;
- проверяют доступность (uptime).
Здесь вы видите:
«Что реально видит пользователь и насколько быстро все работает для него?»
Ключевые метрики производительности
Время ответа (latency) и хвостовые задержки
Смотрите, это одна из главных метрик. Важно учитывать не только среднее значение, но и перцентили:
- p50 — медиана, «типичный» запрос;
- p90 — 90 процентов запросов быстрее этой величины;
- p95, p99 — хвост, где живут самые медленные запросы.
Почему это важно:
- Среднее время может быть 100 мс, но p99 — 2 секунды.
Значит, 1 процент запросов сильно страдают.
В коде метрики обычно собирают с помощью библиотек вроде Prometheus client. Пример на Go:
// Импортируем клиент Prometheus
import "github.com/prometheus/client_golang/prometheus"
// Создаем гистограмму для измерения времени ответа HTTP
var httpRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds", // Имя метрики
Help: "Время обработки HTTP запросов",
Buckets: prometheus.DefBuckets, // Стандартные интервалы
},
[]string{"method", "path", "status"}, // Метки для детализации
)
func init() {
// Регистрируем метрику в Prometheus
prometheus.MustRegister(httpRequestDuration)
}
// Обертка над HTTP обработчиком, которая измеряет время запроса
func InstrumentHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now() // Фиксируем начало обработки
ww := &statusWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(ww, r) // Вызываем реальный обработчик
elapsed := time.Since(start).Seconds()
// Записываем измеренное время в гистограмму
httpRequestDuration.
WithLabelValues(r.Method, r.URL.Path, strconv.Itoa(ww.status)).
Observe(elapsed)
})
}
Как видите, этот код оборачивает существующий HTTP-обработчик и автоматически собирает время ответа.
Пропускная способность (throughput) и RPS
Пропускная способность — это количество операций в единицу времени, например:
- RPS (requests per second);
- сообщений в очереди в секунду;
- операций чтения / записи в базу.
В Prometheus такую метрику обычно измеряют счетчиком и потом берут rate. Пример:
// Счетчик всех HTTP запросов
var httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total", // Общее число запросов
Help: "Счетчик HTTP запросов",
},
[]string{"method", "path", "status"},
)
func init() {
prometheus.MustRegister(httpRequestsTotal)
}
func InstrumentHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ww := &statusWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(ww, r)
// Увеличиваем счетчик запросов
httpRequestsTotal.
WithLabelValues(r.Method, r.URL.Path, strconv.Itoa(ww.status)).
Inc()
})
}
Потом в PromQL вы можете посчитать RPS:
rate(http_requests_total[1m])
Использование CPU и памяти
Эти метрики можно смотреть:
- на уровне ОС (node exporter, cAdvisor);
- на уровне процесса / приложения (профилирование, runtime метрики).
Например, в Go можно включить экспорт runtime-метрик:
import "github.com/prometheus/client_golang/prometheus/collectors"
func init() {
// Экспорт статистики runtime - GC, горутины, аллокации
prometheus.MustRegister(collectors.NewGoCollector())
}
Теперь вы увидите:
go_goroutines— число горутин;go_memstats_alloc_bytes— текущие аллокации;go_gc_duration_seconds— время сборки мусора.
Ошибки и таймауты
Без метрики ошибок мониторинг производительности будет неполным. Важно отслеживать:
- долю запросов с 4xx / 5xx;
- количество исключений в приложении;
- количество таймаутов при обращении к внешним сервисам.
Пример счетчика ошибок:
var httpErrorsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_errors_total",
Help: "Количество HTTP ошибок по статус коду",
},
[]string{"status", "path"},
)
func init() {
prometheus.MustRegister(httpErrorsTotal)
}
func countError(status int, path string) {
// Если статус код 4xx или 5xx - считаем это ошибкой
if status >= 400 {
httpErrorsTotal.
WithLabelValues(strconv.Itoa(status), path).
Inc()
}
}
Инструменты мониторинга производительности
Prometheus и Grafana
Это сочетание чаще всего используют для backend-систем и микросервисов.
Prometheus:
- сам опрашивает приложения по HTTP endpoint’у
/metrics; - хранит временные ряды (time series);
- поддерживает язык запросов PromQL.
- сам опрашивает приложения по HTTP endpoint’у
Grafana:
- строит дашборды по данным Prometheus;
- позволяет настраивать алерты и визуализацию.
Схема выглядит так:
- Вы добавляете в приложение Prometheus-клиент.
- Приложение отдает метрики по HTTP.
- Prometheus периодически «scrape-ит» эти метрики.
- Grafana подключается к Prometheus и рисует графики.
Пример фрагмента конфигурации Prometheus:
scrape_configs:
- job_name: "my_service" # Имя задания
scrape_interval: 15s # Как часто забирать метрики
static_configs:
- targets: ["my-service:9090"] # Адрес сервиса с /metrics
APM: Jaeger, Zipkin, OpenTelemetry, коммерческие решения
Если вам нужно видеть, как один запрос проходит через десяток микросервисов, используется распределенное трассирование.
Подход:
- На входе в систему вы создаете trace-id.
- Каждая часть обработки добавляет свой span (фрагмент трассы).
- Все данные отправляются в APM-систему.
Сегодня стандартный путь — OpenTelemetry:
- вы добавляете SDK в свое приложение;
- оно автоматически создает спаны для HTTP-запросов, SQL-запросов и т.п.;
- данные уходят в бекенд (Jaeger, Tempo, коммерческий APM и т.д.).
Пример на Go с OpenTelemetry (упрощенный):
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
// Инициализируем глобальный tracer
var tracer trace.Tracer = otel.Tracer("my-service")
func handler(w http.ResponseWriter, r *http.Request) {
// Начинаем новый span вокруг обработки HTTP запроса
ctx, span := tracer.Start(r.Context(), "http_handler")
defer span.End()
// Передаем контекст дальше - все вложенные вызовы смогут продолжить trace
processRequest(ctx)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}
func processRequest(ctx context.Context) {
// Внутренний span - например вызов базы данных
_, span := tracer.Start(ctx, "db_query")
defer span.End()
// Здесь вы делаете запрос в базу - но сам запрос я опускаю
}
Теперь вы увидите в Jaeger/Tempo цепочку спанов и сразу поймете, где именно «застрял» запрос.
Логирование и корреляция с метриками
Мониторинг и логи работают лучше всего вместе:
- логи дают контекст — что именно произошло;
- метрики показывают, насколько часто и как это влияет на производительность.
Полезный подход — добавлять в лог trace-id или request-id, чтобы можно было:
- увидеть метрику (время ответа);
- перейти в трассу;
- перейти в логи по этому же запросу.
Настройка мониторинга на практике
Шаг 1. Выбор ключевых метрик (SLO)
Для начала ответьте на вопрос:
«Что значит, что система работает хорошо?»
Примеры:
- 99 процентов запросов
GET /api/ordersобрабатываются быстрее 300 мс; - Доля ошибок 5xx меньше 0.5 процента;
- Время ответа базы данных p95 не превышает 50 мс.
Эти значения вы оформляете как SLO (service level objectives), а на их основе строите:
- дашборды;
- алерты.
Шаг 2. Инструментирование кода
Здесь я покажу общий подход, независимо от языка:
Выделяете основные точки:
- входные HTTP endpoint’ы;
- вызовы базы данных;
- внешние API;
- очереди и фоновые задания.
Для каждой точки:
- измеряете время выполнения;
- увеличиваете счетчик успешных/ошибочных операций;
- добавляете теги/labels (тип операции, статус, ресурс).
Проверяете, что метрики:
- не содержат слишком много разных label-значений (cardinality);
- не дублируют друг друга.
Шаг 3. Построение дашбордов
В Grafana удобно разбить дашборды:
Overview (обзор сервиса):
- RPS;
- latency p50/p90/p99;
- error rate;
- использование CPU / памяти.
API:
- по endpoint’ам;
- по статусам;
- детализация по перцентилям.
База данных:
- самые медленные запросы;
- общее время, проведенное в БД;
- блокировки.
Давайте посмотрим пример набора метрик для одного HTTP-сервиса:
http_requests_totalс label’амиmethod,path,status;http_request_duration_secondsс теми же label’ами;go_goroutines,go_memstats_alloc_bytesи другие runtime-метрики;db_query_duration_secondsс label’амиquery_type,table.
Теперь у вас есть основа, чтобы строить аналитические панели.
Шаг 4. Настройка алертов
Мониторинг без алертов — это просто красивые графики.
Алерты должны быть:
- привязаны к SLO;
- четкими и понятными;
- без «шума» и ложных срабатываний.
Пример алерта на рост времени ответа:
# Условие - p95 времени ответа больше 0.3с в течение 5 минут
histogram_quantile(0.95,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
) > 0.3
Или алерт на долю ошибок:
# Ошибки 5xx за последние 5 минут больше 1 процента
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
> 0.01
Мониторинг производительности в микросервисной архитектуре
Проблемы, которые добавляют микросервисы
В монолите вы видите один процесс, одну базу, одну кодовую базу. В микросервисах появляется:
- множество сервисов;
- сетевые вызовы между ними;
- очереди, брокеры сообщений;
- разные языки и стеки.
Из-за этого:
- проблемы возникают «на стыке» сервисов;
- сложно понять, какой сервис на самом деле виноват в деградации;
- увеличивается число мест, где нужно собирать метрики.
Что помогает в микросервисах
Единая система метрик
Например, везде Prometheus + единый набор базовых метрик.Распределенное трассирование
OpenTelemetry с общим trace-id по всем сервисам.Корреляция логов и трасс
Логируете trace-id и request-id в каждом сервисе.Единый формат / контракт метрик
Например, каждый сервис:- имеет метрики
http_requests_total,http_request_duration_seconds; - использует общие label’ы:
service,endpoint,status.
- имеет метрики
Профилирование как часть мониторинга производительности
Чем отличается профилирование от мониторинга
- Мониторинг — постоянно работающая система, с низкой нагрузкой, показывает тренды.
- Профилирование — более детальный и «тяжелый» анализ, который вы включаете, когда надо глубже разобраться.
Профилирование отвечает на вопросы:
- Какие функции занимают больше всего CPU?
- Где происходят основные аллокации памяти?
- Почему пошел пик GC?
Пример: pprof в Go
В Go есть встроенный пакет net/http/pprof. Покажу, как вы можете включить профили:
import (
"log"
"net/http"
_ "net/http/pprof" // Импортируем pprof - он регистрирует обработчики
)
func main() {
// Запускаем pprof сервер на localhost:6060
go func() {
log.Println("pprof listen on :6060")
// http.DefaultServeMux уже содержит pprof endpoints
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Здесь запускается основное приложение - его код опущен
startApp()
}
Теперь вы можете:
- посмотреть профили в браузере:
http://localhost:6060/debug/pprof/; - снять CPU-профиль через
go tool pprof; - анализировать, какие функции занимают больше всего ресурсов.
Такое профилирование удобно сочетать с метриками:
- по метрикам вы видите, что время ответа выросло;
- включаете pprof и ищете, где именно CPU стал загружаться сильнее.
Практические советы по организации мониторинга
Что внедрять в первую очередь
Если вы только начинаете:
Соберите:
- время ответа (latency);
- RPS;
- долю ошибок (4xx, 5xx);
- метрики CPU, памяти, диска.
Добавьте дашборд «Обзор сервиса».
Введите базовые алерты:
- сервис недоступен (нет метрик / uptime);
- p95 latency превышает порог;
- error rate выше нормы.
После этого можно углубляться: база данных, распределенное трассирование, профилирование.
Как не утонуть в метриках
Смотрите, часто команды делают ошибку: добавляют слишком много метрик и label’ов. В итоге:
- Prometheus начинает потреблять много памяти;
- графики становятся медленными;
- разобраться в лесу метрик сложно.
Лучше:
- начинать с ограниченного набора;
- стандартизировать имена и label’ы;
- регулярно пересматривать, какие метрики действительно используются.
Мониторинг в CI / нагрузочном тестировании
Полезная практика — использовать те же инструменты мониторинга во время:
- нагрузочных тестов;
- pre-production окружений.
Вы можете:
- запускать нагрузочные сценарии (например, k6, JMeter, Locust);
- параллельно смотреть на метрики:
- как растет latency при увеличении RPS;
- в какой момент CPU/память упираются в лимиты;
- как ведет себя база данных.
Такая связка позволяет заранее проверить, выдержит ли система планируемый рост нагрузки.
Заключение
Мониторинг производительности — это не один инструмент и не один график, а целая система измерений и наблюдаемости, которая:
- помогает вам понимать реальное состояние приложения;
- показывает, где находятся узкие места;
- позволяет планировать развитие и масштабирование;
- снижает время поиска и устранения проблем.
Вы видите, что в основе такого мониторинга лежат:
- четкие цели (SLO и ключевые метрики);
- правильное инструментирование кода;
- выбранный стек (Prometheus, Grafana, OpenTelemetry и т.д.);
- связка метрик, логов и трасс.
Если вы выстроите эту систему постепенно — от базовых метрик к распределенному трассированию и профилированию — у вас появится надежный фундамент для работы с производительностью, а оптимизации перестанут быть «угадайкой» и станут управляемым инженерным процессом.
Частозадаваемые технические вопросы
1. Как избежать слишком высокой кардинальности метрик в Prometheus
Не добавляйте в label значения, которые могут часто меняться или иметь много вариантов
Например, user_id, request_id, email
Используйте label’ы только с ограниченным набором значений
Если нужно анализировать по пользователям — отправляйте это в логи, а не в метрики
Регулярно просматривайте топ-метрик по количеству series и удаляйте лишние комбинации label’ов
2. Как мониторить производительность запросов к базе без изменения всего кода
Используйте обертки над драйвером БД или ORM с поддержкой hooks
Многие библиотеки позволяют перехватывать начало и конец запроса
В этих хуках измеряйте время и увеличивайте счетчики
Например, для PostgreSQL можно использовать proxy-слой PgBouncer или pgbadger для анализа логов и метрик без изменения приложения
3. Как связать логи и трассировки если стек уже частично реализован
Добавьте в код генерацию и проброс trace_id через контекст запроса
В логгер внедрите middleware который будет автоматом добавлять trace_id в каждую запись
В APM/Tracing системе настройте экспорт того же trace_id
Так вы сможете по одному идентификатору находить и трассу и набор логов по конкретному запросу
4. Как безопасно включать профилирование на продакшене
Давайте доступ к pprof или аналогам только по защищенному интерфейсу
Ограничьте его внутренней сетью или VPN
Включайте тяжелые профили (CPU, trace) на короткое время через отдельные команды или feature-флаги
Следите за overhead - если нагрузка большая, используйте выборочное профилирование на копии трафика или staging окружении
5. Что делать если метрики показывают норму а пользователи жалуются на медленную работу
Проверьте мониторинг с точки зрения пользователя - включите RUM или synthetic проверки
Убедитесь что метрики собираются во всех зонах и регионах а не только из одного датацентра
Проверьте сетевые задержки CDN DNS и работу фронтенда - возможно проблема в рендеринге или тяжелых ресурсах а не в backend API
Сопоставьте время по часам - иногда деградации происходят только при пиковой нагрузке или в определенные периоды дня