awesome-everything EN
↑ Обратно к восхождению

Наблюдаемость

Инструментация RED в Prometheus: счётчики, гистограммы и дисциплина cardinality

Суть Три канонические Prometheus-метрики для RED, почему Duration обязана быть гистограммой (никаких среднего), как работает histogram_quantile и железная дисциплина label, которая удерживает cardinality под контролем.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Команда настраивает алерт на среднюю задержку запросов. Фикс бага поднимает p99 с 200 мс до 800 мс — но среднее почти не двигается. On-call пропускает инцидент на 40 минут. На SLO-ревью выясняется, что алерт на среднее никогда не срабатывал при реальном impact на пользователей. Гистограммы сработали бы за 2 минуты.

Три канонические RED-метрики

Каждый HTTP-сервис должен эмитировать ровно три группы метрик с консистентными именами:

http_requests_total        # counter — Rate
http_request_errors_total  # counter — Errors (5xx или label status)
http_request_duration_seconds  # histogram — Duration

PromQL в Prometheus даёт все три измерения RED:

  • Rate: rate(http_requests_total[5m])
  • Error rate: rate(http_request_errors_total[5m]) / rate(http_requests_total[5m])
  • Duration p99: histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))

Почему Duration обязана быть гистограммой

Среднее прячет всё, что замечают пользователи. Сервис, где 99% запросов выполняются за 50 мс, а 1% — за 5000 мс, имеет ту же среднюю задержку (~100 мс), что сервис, где все запросы по 100 мс. Первый убивает пользователей на ретраях; второй — нет.

histogram_quantile(q, buckets) в Prometheus читает накопленные счётчики бакетов за временное окно и оценивает q-й перцентиль линейной интерполяцией между соседними бакетами. Точность целиком зависит от плотности бакетов у нужного перцентиля.

Требование by (le). Правильная форма всегда такая:

histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))

Если пропустить by (le), суммирование сворачивает все label-измерения включая le (label границы бакета), оставляя histogram_quantile одну точку вместо распределения — результат NaN или мусор. Это реальная, частая ошибка, которая молча выдаёт неверные значения.

Сигнал задержкиЧто он прячетПрименение
Среднее (sum/count)Поведение медленного хвоста, которое замечают пользователиНикогда для SLO-алертов
Prometheus summaryНельзя агрегировать по репликамТолько когда одна реплика владеет данными
Prometheus histogramТочность зависит от плотности бакетовFleet-wide p99-алерты

Стратегия бакетов

Стандартные бакеты Prometheus-клиента — [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] секунд — неправильны для большинства сервисов. Для checkout-API с SLO 200 мс большая часть трафика попадает в диапазон 50–250 мс. Один бакет покрывает весь этот диапазон (100–250 мс), поэтому p99 может быть где угодно внутри него — нечитаемо.

Продакшн-правило: 10–15 бакетов, наибольшая плотность вокруг SLO-цели. Для SLO 200 мс:

[0.01, 0.025, 0.05, 0.1, 0.2, 0.4, 0.8, 1.6, 3.2, 10]

Три бакета ниже 200 мс (100, 25, 50 мс дают разрешение), три выше (400, 800, 1600 мс), жёсткий cap на таймаут сервиса (10 с). Соседние бакеты отличаются не более чем в 2× вблизи SLO.

Дисциплина label — железное правило

Каждая уникальная комбинация значений label на Prometheus-метрике создаёт отдельный временной ряд. Наивная инструментация RED с label user_id в сервисе с 100к активных пользователей вырастает с нескольких сотен рядов до сотен тысяч за часы.

Что допустимо в label:

  • route — шаблон URL (/cart, не /cart?u=12345)
  • method — HTTP-метод (GET / POST / …)
  • status_class — 2xx / 4xx / 5xx (не точный код)
  • service — инжектируется deployment как мета-label

Запрещено в label: user ID, request ID, email, session token, query string, код страны (если не ограничен и невелик). Всё это — неограниченная cardinality.

Математика экономии: сворачивание 200/201/204 в 2xx сокращает 60 уникальных статус-кодов до 4 классов. При 20 роутах × 4 методах: 60 × 20 × 4 = 4800 рядов → 4 × 20 × 4 = 320 рядов, снижение в 15× без потери полезной alerting-мощности.

Почему это работает

Если действительно нужен алерт на конкретный статус-код на конкретном роуте — строй его из логов, не из метрики с высоко-кардинальным label. Логи — естественный дом для high-cardinality данных (каждое событие — одна запись). Метрики — дом для агрегированных счётчиков временных рядов (каждый ряд — отдельный in-memory счётчик). Это архитектурное разделение, а не предпочтение.

Node.js RED middleware

const client = require('prom-client');
const reqs = new client.Counter({
  name: 'http_requests_total',
  help: 'Total HTTP requests',
  labelNames: ['method', 'route', 'status_class'],
});
const errs = new client.Counter({
  name: 'http_request_errors_total',
  help: 'Failed HTTP requests (5xx)',
  labelNames: ['method', 'route'],
});
const dur = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Request duration',
  labelNames: ['method', 'route', 'status_class'],
  buckets: [0.01, 0.025, 0.05, 0.1, 0.2, 0.4, 0.8, 1.6, 3.2, 10],
});

app.use((req, res, next) => {
  const start = process.hrtime.bigint();
  res.on('finish', () => {
    const seconds = Number(process.hrtime.bigint() - start) / 1e9;
    const sclass = `${Math.floor(res.statusCode / 100)}xx`;
    const route = req.route?.path || 'unknown';
    reqs.inc({ method: req.method, route, status_class: sclass });
    dur.observe({ method: req.method, route, status_class: sclass }, seconds);
    if (res.statusCode >= 500) errs.inc({ method: req.method, route });
  });
  next();
});

req.route.path даёт совпавший шаблон (/cart), а не req.url с query string. Эта одна строка предотвращает cardinality explosion.

Викторина

Команда настраивает алерт на СРЕДНЮЮ задержку запросов по всем репликам. Почему это опасно?

Викторина

Сервис эмитирует счётчик Errors с label по точному тексту error_message. После багованного релиза, бросающего уникальные stack trace, счёт в hosted-бэкенде утраивается за ночь. Почему?

Викторина

Senior-инженер утверждает, что histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m]))) без 'by (le)' даёт fleet-wide p99. Почему это неверно?

Вспомните перед уходом
  1. 01
    Почему RED Duration обязана быть гистограммой, а не sum/count (средним)?
  2. 02
    Что делает клауза 'by (le)' в запросе histogram_quantile и что происходит без неё?
  3. 03
    Назови три значения label, запрещённые на RED-метриках, и одно всегда разрешённое.
Итог

RED в Prometheus — три группы метрик: http_requests_total (счётчик для Rate), http_request_errors_total (счётчик для Errors) и http_request_duration_seconds (гистограмма для Duration). Duration обязана быть гистограммой, потому что среднее маскирует хвостовое поведение, которое ощущают пользователи — histogram_quantile читает per-bucket счётчики и интерполирует перцентиль, но только когда sum by (le) сохраняет label границы бакета. Выбор бакетов определяет точность p99: выбирай 10–15 бакетов с наибольшей плотностью вокруг SLO-цели, соседние отличаются не более чем в 2× вблизи SLO. Дисциплина label — вторая половина: используй шаблоны роутов, HTTP-метод и класс статуса — никаких user ID, request ID или точных текстов ошибок. Каждая уникальная комбинация label — отдельный временной ряд, биллируемый отдельно и хранимый в RAM на Prometheus-сервере.

Связанные уроки
встречается в167
Продолжить восхождение ↑USE на Linux: CPU, память, диск, сеть и PSI
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.