DarkRiDDeR14 мин

Лейблы Prometheus: где заканчивается полезная размерность

НаблюдаемостьPrometheusРазбор механизма

Симптом появляется не в коде обработчика, а после добавления «ещё одного полезного label». Метрика по-прежнему показывает запросы, но одинаковый график превращается в множество почти уникальных series, а простой запрос по операции начинает возвращать лишние строки. Цена — не только память и диск сервера метрик. В разборе становится неясно, какое измерение является частью вопроса, а какое случайно сохранило одну пользовательскую историю вместо агрегированного сигнала.

Для августа 2020 года мне достаточно разобрать один механизм: каждая уникальная комбинация name и labels образует отдельную time series. Ниже нет фактического размера Prometheus, нагрузки или claim о production инциденте. Есть учебный расчёт, короткий allowlist и fixture, который отвергает неограниченные значения. Цель не в том, чтобы запретить labels, а в том, чтобы различать размерность вопроса и идентификатор конкретного события.

Series — результат выбора, а не побочный эффект строки

Представим один counter store_http_requests_total. Без labels у него один ряд для target. С operation="catalog" и outcome="success" появляется ряд для этой пары. Если добавить request_id, новый запрос почти наверняка принесёт новое значение. Такая строка может выглядеть информативно, но это уже не ответ на вопрос «как меняется число ошибок checkout», а попытка поместить журнал события в storage метрик. Для поиска единичного случая есть correlation ID в логе; для агрегирования — короткий набор измерений.

Проверка здесь простая: можно ли заранее перечислить значения label и останется ли осмысленным sum by (...), если часть dimensions убрать? Для operation ответ обычно да: проект заранее знает небольшой набор логических обработчиков. Для outcome тоже да, если договор ограничен success и error. Для email, UUID, сырого path с ID товара, stack trace и текста ошибки ответ нет: новые значения приходят извне или растут вместе с числом запросов.

Граница labels в учебном HTTP-сигнале
КандидатЗначения известны заранее?Какой вопрос поддерживаетРешение в этом контракте
operationда: catalog, checkout, profileВ какой логической операции выросло число завершений или ошибок?оставить; проверять allowlist
outcomeда: success/errorЕсть ли различие между удачными и ошибочными завершениями?оставить; не хранить текст ошибки
status_codeпочти ограничен, но для первого вопроса избыточенНужны ли отдельные HTTP-классы?добавить только после нового вопроса; пока outcome достаточно
request_id, user ID, emailнет: новое значение почти на каждую операциюНайти один конкретный запросне добавлять; оставить в журнале
Полный URL /orders/12345нет: ID и query string растутПонять маршрутнормализовать в operation или route template до метрики

Эта таблица не означает, что status_code всегда плох. Он может быть полезным, когда действительно нужен вопрос о 404 и 500. Но нельзя добавлять его по привычке, если следующий шаг всё равно одинаков для всех error. Контракт должен быть небольшим: одна добавленная dimension меняет не только текст exposition, но и число рядов, агрегации, правила и стоимость хранения. Если ей не соответствует отдельное действие, она пока не проходит ревью.

Кардинальность можно оценить до первого scrape

У учебного counter есть три операции и два исхода. Для одного target верхняя граница — шесть комбинаций, если все они встретятся. Если такой же application exporter запускается на двух target, Prometheus добавляет target labels на стороне scrape, и в простом мысленном расчёте получится до двенадцати наблюдаемых рядов. Это не замер сервера и не предел всех связанных metric families: histogram создаёт дополнительные bucket, sum и count series. Расчёт нужен для другого — увидеть мультипликацию до того, как в неё попадёт неограниченный input.

Добавим в этот же контракт user ID со ста учебными значениями. Уже для counter получаем не шесть, а до шестисот комбинаций на target; с двумя target — до тысячи двухсот. Цифры специально синтетические. Они не описывают настоящих пользователей, RAM или пропускную способность Prometheus, но показывают форму ошибки: значение, которое растёт вместе с пользователями, умножает каждый уже выбранный label. Поэтому полезнее спросить «можно ли получить это из лога по request ID?» до того, как переносить поле в метрику.

Проверяем boundary в коде, а не глазами на dashboard

Ниже псевдокод обёртки над client library. Его можно реализовать на выбранном клиенте, но в этой статье не создаётся HTTP endpoint и не вызывается внешняя библиотека. Важна граница перед инструментированием: operation и outcome проходят allowlist, а произвольные поля не имеют места в label object. Такое правило полезнее комментария «не использовать high cardinality», потому что новый endpoint или ошибка перестают незаметно менять форму metric family.

const allowedOperations = new Set(["catalog", "checkout", "profile"]);
const allowedOutcomes = new Set(["success", "error"]);

function recordFinishedRequest(event) {
  if (!allowedOperations.has(event.operation)) throw new Error("unknown operation");
  if (!allowedOutcomes.has(event.outcome)) throw new Error("unknown outcome");

  // Нет user_id, email, request_id, полного URL или текста ошибки.
  requestTotal.inc({ operation: event.operation, outcome: event.outcome });
  requestDuration.observe({ operation: event.operation }, event.durationSeconds);
}

// requestTotal и requestDuration — объекты выбранной client library.
// Псевдокод задаёт контракт labels; library и server не запускаются в статье.

Слово «псевдокод» здесь важно. Имена методов inc и observe часто похожи в client libraries, но конкретный API, регистрация и exposition зависят от выбранной версии. Нельзя копировать фрагмент и считать, что он создал metrics endpoint. Однако контракт входа проверяем без инфраструктуры: функция renderFixtureExposition в этом module принимает только тот же набор bounded values. Unknown operation завершает fixture ошибкой, а выходной текст не может получить request_id или user_id.

Вертикальная схема границы labels: событие запроса проходит через allowlist operation и outcome, попадает в агрегированную time series; request ID, email и полный URL направлены в журнал и не становятся labels
Один signal хранит ограниченные dimensions. Идентификатор отдельного события остаётся ключом поиска в журнале, а не генератором новой series.

Запрос должен агрегировать тот же договор

Если metric family содержит operation и outcome, запрос может явно сохранить оба измерения. В примере ниже sum by группирует изменение counter за учебные десять минут. Это полезно именно потому, что labels заранее ограничены: результат имеет шесть или меньше понятных строк, а не одну строку на пользователя. Если выражение приходится постоянно фильтровать по уникальным идентификаторам, проблема не в синтаксисе PromQL, а в том, что журнал и метрика получили одну и ту же работу.

# Раскладываем завершённые запросы только по двум заранее ограниченным labels.
sum by (operation, outcome) (
  increase(store_http_requests_total[10m])
)

# Не делаем так: один request_id создаёт новую series для каждого запроса.
store_http_requests_total{request_id="<unique value>"}

У counter есть ещё одна граница: процесс может перезапуститься и значение станет меньше. Prometheus предназначен для работы с такими series, но вручную вычитать две точки и называть результатом increase() нельзя. Контролируемая fixture ниже намеренно выбрасывает ошибку на убывающем наборе, потому что она тестирует лишь договор порога без модели reset. После подключения реального сервера нужно проверить scrape interval, фактическую экспозицию и поведение выражения на используемой версии Prometheus.

Когда новый label всё-таки оправдан

Новый label появляется не потому, что поле уже есть в request object. Сначала появляется новый вопрос и другое действие. Например, команда действительно готова разбирать 4xx отдельно от 5xx; тогда можно договориться о bounded status_class и добавить его после теста, что все значения нормализуются в известные классы. Или один exporter измеряет две заранее перечисленные внешние базы; тогда dependency может быть допустимой размерностью. Но строка SQL, hostname от пользователя и путь с UUID остаются данными для лога или отдельного хранилища.

Перед добавлением полезно сделать маленький расчёт: сколько значений есть сейчас, какое верхнее значение допускает код, с чем оно перемножится и какой запрос станет возможным. Если на любой строке ответ «не знаю, значения приходят из входа», действие откладывают. Нельзя компенсировать неопределённость надеждой, что график потом подскажет. График уже создан из series, и потерянная граница будет дорого стоить следующему расследованию.

Маршрут ревью labels

  1. Записать один operational question и назвать действие после его ответа. Вопрос «собрать всё на будущее» для labels не подходит.
  2. Выписать каждый candidate label, источник его значения и максимальное число значений. Отдельно отметить поля из URL, пользователя, исключения и request context.
  3. Оставить только измерения, которые можно заранее перечислить и по которым действительно будет агрегация. Для первого контракта выбрать operation и outcome.
  4. Посчитать учебную верхнюю границу combinations с уже существующими labels и target. Для histogram отдельно учесть, что buckets создают дополнительные series.
  5. Поставить allowlist или нормализацию на границе instrumentation и прогнать controlled fixture с допустимым и недопустимым событием.
  6. Сформировать PromQL-запрос с явным sum by. Перед выпуском на реальный сервер отдельно проверить scrape, reset, storage и ответственность за действие.

Что этот механизм не решает

Bounded labels не заменяют нормальный журнал, trace context или доступ к исходному событию. Они также не дают ответ на вопрос, почему запрос завершился ошибкой: для этого после срабатывания нужны связанный request ID и контекст кода. В статье не оценивается память Prometheus, не запускается exporter и не сравниваются latency на живой системе. Упомянутые числа — простая арифметика учебной модели, а не production telemetry.

Это и есть уровень М3 для 2020 года: автор начинает видеть, что наблюдаемый сигнал зависит от границы данных, а не от цвета dashboard. Он умеет сказать «эта dimension принадлежит журналу, а эта — ограниченному metric contract», но не заявляет опыт управления общей платформой наблюдаемости, SLO или error budget. Следующий проверяемый шаг после такого текста — один изолированный endpoint и один запрос, а не массовое тиражирование labels по приложению.

Проверяемые источники

  • Prometheus: Metric and label naming — официальные правила: одна величина и единица на имя метрики, базовые единицы и отдельная осторожность с label dimensions
  • Prometheus: Instrumentation — официальные рекомендации для online-serving систем: считать завершённые запросы, ошибки и latency; не раздувать labels и держать счётчик ошибок рядом с числом попыток
  • Prometheus: Querying basics — официальная справка по selectors, range vectors, агрегации и выражениям PromQL; запросы ниже показаны как учебные формулировки
  • Prometheus: Metric types — официальное описание counter, gauge, histogram и summary; выбор типа должен следовать форме наблюдаемой величины