{ "index": 266, "slug": "editorial-2020-08-mechanism-metrics-basics", "title": "Лейблы Prometheus: где заканчивается полезная размерность", "excerpt": "На примере дежурства разбираем, почему label должен иметь ограниченный набор значений, как cardinality превращается в стоимость и каким запросом проверить counter, не превращая Prometheus в хранилище контекста.", "contentHtml": "
В учебном сценарии дежурный открывает график в понедельник утром после релиза: число временных рядов растёт, запросы к Prometheus отвечают медленнее, а нужный разрез не находится. Сначала он подозревает нехватку ресурсов и хочет увеличить retention. Цена такого шага — дополнительные CPU, RAM и диск без гарантии, что оператор увидит нужный сигнал.
\nОн проверяет не только сервер, но и diff instrumentation. В новом счётчике рядом со стабильной operation появилась request_id; почти каждый запрос принёс новое значение. После этого команда возвращается к вопросу: какие операции ошибаются за последние десять минут? В этой сцене label должен иметь небольшой заранее понятный набор значений, а уникальный контекст должен остаться в логе или трассировке.
Имя метрики само по себе не определяет ряд. Ряд задаёт имя плюс полный набор label. Записи store_http_requests_total{operation=\"checkout\",outcome=\"success\"} и store_http_requests_total{operation=\"checkout\",outcome=\"error\"} — два разных ряда. Если добавить user_id, каждый пользователь создаст новый ряд. Если добавить request_id, почти каждый запрос создаст новый ряд.
Для учебного сигнала допустимы три операции и два исхода. Верхняя граница равна 3 × 2 = 6 комбинациям до учёта других labels, например instance и job. Это число можно проверить заранее. Для полного URL граница заранее не задана: число значений следует из трафика, параметров и маршрутов. Для email или UUID набор значений не ограничен контрактом. Поэтому проблема возникает не в синтаксисе метрики, а в контракте данных.
\nМетрика должна помогать принять решение. Например: «В какой операции за последние десять минут появились ошибки?» Для этого нужны завершённые запросы, стабильное имя операции и нормализованный исход. Не нужны пользователь, текст исключения или URL с query-параметрами. Если вопрос другой, меняется и контракт. «Сколько задач сейчас выполняется?» требует gauge. «Сколько запросов завершилось?» требует counter. «Как распределилась длительность?» требует наблюдений длительности, обычно histogram или summary.
\nCounter накапливает события и может сброситься при перезапуске процесса. Само значение counter полезно проверить при экспорте, но для окна обычно используют функцию над counter. В учебном запросе ниже increase() считает изменение за интервал, учитывает сброс при перезапуске и экстраполирует его на окно, поэтому результат в общем случае может быть дробным. Это не готовый alert и не production-порог. Число два выбрано только для того, чтобы граница была видна на короткой серии.
| Элемент | Решение | Проверка | Ограничение |
|---|---|---|---|
| Метрика запросов | store_http_requests_total, counter | Считаем только завершённые операции | Не объясняет причину ошибки |
operation | catalog, checkout, profile | Проверяем allowlist до записи | Новый маршрут требует изменения контракта |
outcome | success или error | Нормализуем исход в одном месте | Не заменяет код HTTP или тип ошибки |
| Контекст запроса | Лог или trace с request ID | Связываем событие по trace ID | Не добавляем уникальный ID в metric label |
Контракт лучше закрепить рядом с точкой записи. Псевдокод ниже не зависит от конкретной client library. Он показывает порядок проверки: сначала нормализуем значения, затем проверяем длительность и обновляем счётчики. Все значения синтетические. Код не запускает Prometheus и не сообщает ничего о реальной нагрузке.
\nconst allowedOperations = new Set([\"catalog\", \"checkout\", \"profile\"]);\nconst allowedOutcomes = new Set([\"success\", \"error\"]);\n\nfunction recordFinishedRequest(event) {\n if (!allowedOperations.has(event.operation)) {\n throw new Error(\"unknown operation\");\n }\n if (!allowedOutcomes.has(event.outcome)) {\n throw new Error(\"unknown outcome\");\n }\n if (!Number.isFinite(event.durationSeconds) || !(event.durationSeconds >= 0)) {\n throw new Error(\"invalid duration\");\n }\n\n requestsTotal.inc({\n operation: event.operation,\n outcome: event.outcome,\n });\n requestDuration.observe(\n { operation: event.operation },\n event.durationSeconds,\n );\n}\n\n// Не добавляем сюда user_id, request_id, email, полный URL\n// или произвольный текст ошибки.\nОшибку неизвестного значения нельзя молча превращать в новый label. Иначе опечатка или новый маршрут незаметно расширит набор рядов. В одном проекте допустимо вернуть событие в общий обработчик ошибок, в другом — записать его в лог и не обновлять эту метрику. Выбор зависит от контракта сервиса. Важно, чтобы отказ был видимым и не создавал бесконечный словарь значений.
\nПосле экспорта можно собрать число завершённых ошибок по операции:
\nsum by (operation) (\n increase(store_http_requests_total{outcome=\"error\"}[10m])\n)\n\n# Учебный запрос: ищем серию с уникальным label.\nstore_http_requests_total{request_id=\"any-value\"}\nПервое выражение агрегирует ограниченные серии и оставляет операцию для сравнения. Второе выражение — не рекомендация, а отрицательный пример. Запрос по request_id может найти отдельный ряд, но сам способ записи создаёт новый ряд для каждого ID. Через короткое время поиск конкретного запроса станет дороже, а Prometheus будет хранить данные, которые лучше подходят логам или trace. Метрика отвечает на вопрос «сколько и где», лог или trace — «какой именно запрос и почему».
Есть и менее очевидный отрицательный путь. Разработчик заменяет стабильное имя маршрута на полный URL, чтобы увидеть параметры. В итоге /orders/1 и /orders/2 получают разные значения. Нормализованный маршрут решает только часть задачи: список маршрутов всё равно должен быть ограничен, а редкие динамические значения не должны проходить в label без оценки cardinality.
| Симптом | Вероятная причина | Проверка | Действие |
|---|---|---|---|
| Число рядов быстро растёт после релиза | В label попало уникальное или почти уникальное значение | Посчитать distinct values по новым labels и сравнить с diff кода | Удалить label, нормализовать значение или перенести контекст в лог |
| График показывает слишком много линий | Запрос сохранил лишние labels и не агрегирует их | Открыть табличный результат и проверить число series до агрегации | Оставить только dimension, по которой принимается действие; добавить sum by |
| Нельзя отличить ошибку одной операции от другой | Счётчик не содержит стабильного operation label | Сравнить экспорт и вопрос, который должен поддержать dashboard | Добавить ограниченный allowlist operation |
| По метрике ищут конкретный запрос | Metric используют вместо логов или трассировки | Проверить наличие request ID и текста ошибки в label | Оставить агрегат в Prometheus, связать его с trace ID в другом сигнале |
| Новые значения появляются без изменения схемы | Label принимает пользовательский ввод или свободный текст | Проверить источник каждого label и список допустимых значений | Ввести нормализацию и явный отказ для неизвестного значения |
Cardinality — не единственный риск. Даже ограниченный label может быть бесполезным, если команда не знает, какое действие следует из его значения. Метрика не показывает стек ошибки, тело запроса, пользователя или порядок событий. Для этого нужны логи и трассировка. Она также не гарантирует, что scrape не пропустил точку, что все экземпляры используют одну версию схемы или что выбранный порог отражает SLO.
\nУ Prometheus нет универсального безопасного числа для любой системы. В официальных рекомендациях rule of thumb — держать cardinality ниже 10; для метрики, которая уже превысила 100 или может вырасти до такого уровня, предлагают уменьшить число dimensions либо перенести анализ в систему общего назначения. Это ориентиры, а не расчёт ёмкости: фактический предел зависит от числа targets, scrape interval, retention, количества метрик и ресурсов. Поэтому число «шесть комбинаций» выше относится только к учебному сигналу.
\nПроверка завершена, если команда может показать четыре результата: список labels с источниками и верхней границей значений; экспорт учебных событий без уникальных labels; запрос, который оставляет только нужную dimension; и отрицательный тест, в котором неизвестное или уникальное значение не создаёт новый ряд молча. После этого отдельно проверяют нагрузку, правила хранения и production-порог. До этих измерений материал остаётся проверкой контракта, а не доказательством эксплуатационного результата.
\nincrease(), сбросов counter и экстраполяции на окно.