diff --git a/editorial/agent-rewrites/266.json b/editorial/agent-rewrites/266.json index 65f54ba..67d44b5 100644 --- a/editorial/agent-rewrites/266.json +++ b/editorial/agent-rewrites/266.json @@ -2,6 +2,6 @@ "index": 266, "slug": "editorial-2020-08-mechanism-metrics-basics", "title": "Лейблы Prometheus: где заканчивается полезная размерность", - "excerpt": "Метрика помогает сравнивать операции, пока её labels имеют ограниченный набор значений. Разбираем cardinality, безопасный контракт для кода, запрос по counter и проверку случая, когда сигнал становится слишком дорогим.", - "contentHtml": "

На графике растёт число временных рядов, запросы к Prometheus начинают отвечать медленнее, а полезный разрез всё равно не находится. Частая причина — в label попало значение, которое меняется почти на каждый запрос: полный URL, идентификатор пользователя или request ID. Каждое новое сочетание label создаёт отдельный time series. Цена ошибки — память, CPU, место на диске и потеря доверия к мониторингу. Команда видит много данных, но не получает короткий ответ на операционный вопрос.

\n

Тезис простой: label должен описывать небольшой и заранее понятный набор вариантов. Если значение растёт вместе с числом пользователей или запросов, это не dimension для метрики. Такой контекст нужно искать в логах или трассировке. В Prometheus полезная размерность заканчивается там, где число комбинаций становится непредсказуемым или не связано с действием оператора.

\n

Как label превращается в time series

\n

Имя метрики само по себе не определяет ряд. Ряд задаёт имя плюс полный набор label. Записи store_http_requests_total{operation=\"checkout\",outcome=\"success\"} и store_http_requests_total{operation=\"checkout\",outcome=\"error\"} — два разных ряда. Если добавить user_id, каждый пользователь создаст новый ряд. Если добавить request_id, почти каждый запрос создаст новый ряд.

\n

Для учебного сигнала допустимы три операции и два исхода. Верхняя граница равна 3 × 2 = 6 комбинациям до учёта других labels, например instance и job. Это число можно проверить заранее. Для полного URL такой расчёт невозможен: число значений следует из трафика, параметров и маршрутов. Для email или UUID граница также отсутствует. Поэтому проблема возникает не в синтаксисе метрики, а в контракте данных.

\n
\"Граница
Учебная схема: стабильные значения оставляют число рядов ограниченным, уникальные значения переносят контекст в другой сигнал.
\n

Сначала операционный вопрос

\n

Метрика должна помогать принять решение. Например: «В какой операции за последние десять минут появились ошибки?» Для этого нужны завершённые запросы, стабильное имя операции и нормализованный исход. Не нужны пользователь, текст исключения или URL с query-параметрами. Если вопрос другой, меняется и контракт. «Сколько задач сейчас выполняется?» требует gauge. «Сколько запросов завершилось?» требует counter. «Как распределилась длительность?» требует наблюдений длительности, обычно histogram или summary.

\n

Counter накапливает события и может сброситься при перезапуске процесса. Сырым значением удобно проверять экспорт, но для окна обычно используют функцию над counter. В учебном запросе ниже increase() отвечает на вопрос о числе ошибок за интервал. Это не готовый alert и не production-порог. Число два выбрано только для того, чтобы граница была видна на короткой серии.

\n
Контракт учебной метрики и границы применения
ЭлементРешениеПроверкаОграничение
Метрика запросовstore_http_requests_total, counterСчитаем только завершённые операцииНе объясняет причину ошибки
operationcatalog, checkout, profileПроверяем allowlist до записиНовый маршрут требует изменения контракта
outcomesuccess или errorНормализуем исход в одном местеНе заменяет код HTTP или тип ошибки
Контекст запросаЛог или trace с request IDСвязываем событие по trace IDНе добавляем уникальный ID в metric label
\n

Минимальный контракт в коде

\n

Контракт лучше закрепить рядом с точкой записи. Псевдокод ниже не зависит от конкретной client library. Он показывает порядок проверки: сначала нормализуем значения, затем обновляем счётчики. Все значения синтетические. Код не запускает Prometheus и не сообщает ничего о реальной нагрузке.

\n
const 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\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

Запрос и отрицательный путь

\n

После экспорта можно собрать число завершённых ошибок по операции:

\n
sum 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. Через короткое время поиск конкретного запроса станет дороже, а сборщик будет хранить данные, которые лучше подходят логам или trace. Метрика отвечает на вопрос «сколько и где», лог или trace — «какой именно запрос и почему».

\n

Есть и менее очевидный отрицательный путь. Разработчик заменяет стабильное имя маршрута на полный URL, чтобы увидеть параметры. В итоге /orders/1 и /orders/2 получают разные значения. Нормализованный маршрут решает только часть задачи: список маршрутов всё равно должен быть ограничен, а редкие динамические значения не должны проходить в label без оценки cardinality.

\n

Симптомы и действия

\n
Диагностическая таблица для 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 и список допустимых значенийВвести нормализацию и явный отказ для неизвестного значения
\n

Порядок проверки

\n
  1. Сформулируйте один вопрос, на который должна ответить метрика. Запишите, какое действие последует после ответа.
  2. Назначьте границу события: например, завершение HTTP-операции. Не смешивайте попытку, ответ клиента и результат фоновой очереди.
  3. Перечислите labels и для каждого укажите источник и полный список допустимых значений.
  4. Посчитайте верхнюю границу комбинаций. Умножьте размеры ограниченных наборов и отдельно учтите labels, которые добавляет окружение.
  5. Проверьте кодовую точку записи. Не допускайте URL, UUID, email, request ID и текста ошибки в label.
  6. Экспортируйте несколько учебных событий и убедитесь, что одинаковые значения дают один ряд, а неизвестные значения не проходят молча.
  7. Проверьте запрос в табличном режиме. Сначала посмотрите число возвращённых series, затем добавьте агрегацию и проверьте оставшиеся labels.
  8. Отдельно проверьте отрицательный путь: неизвестная операция, динамический URL и запрос с уникальным ID должны приводить к явному отказу или к другому сигналу.
  9. Только после этих проверок выбирайте правило или dashboard. Учебный порог нельзя переносить в production без данных о норме, окне, пропусках и стоимости ошибки.
\n

Ограничения

\n

Cardinality — не единственный риск. Даже ограниченный label может быть бесполезным, если команда не знает, какое действие следует из его значения. Метрика не показывает стек ошибки, тело запроса, пользователя или порядок событий. Для этого нужны логи и трассировка. Она также не гарантирует, что scrape не пропустил точку, что все экземпляры используют одну версию схемы или что выбранный порог отражает SLO.

\n

У Prometheus нет универсального безопасного числа для любой системы. Официальные рекомендации предлагают держать cardinality низкой и отдельно расследовать метрики, которые могут вырасти до больших значений. Фактический предел зависит от числа targets, scrape interval, retention, количества метрик и ресурсов. Поэтому число «шесть комбинаций» выше относится только к учебному сигналу; это не расчёт ёмкости конкретного кластера.

\n

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

\n

Схема готова к следующему этапу, если команда может показать четыре результата: список labels с источниками и верхней границей значений; экспорт учебных событий без уникальных labels; запрос, который оставляет только нужную dimension; и отрицательный тест, в котором неизвестное или уникальное значение не создаёт новый ряд молча. После этого отдельно проверяют нагрузку, правила хранения и production-порог. До этих измерений материал остаётся проверкой контракта, а не доказательством эксплуатационного результата.

\n

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

" + "excerpt": "На примере дежурства разбираем, почему label должен иметь ограниченный набор значений, как cardinality превращается в стоимость и каким запросом проверить counter, не превращая Prometheus в хранилище контекста.", + "contentHtml": "

В учебном сценарии дежурный открывает график в понедельник утром после релиза: число временных рядов растёт, запросы к Prometheus отвечают медленнее, а нужный разрез не находится. Сначала он подозревает нехватку ресурсов и хочет увеличить retention. Цена такого шага — дополнительные CPU, RAM и диск без гарантии, что оператор увидит нужный сигнал.

\n

Он проверяет не только сервер, но и diff instrumentation. В новом счётчике рядом со стабильной operation появилась request_id; почти каждый запрос принёс новое значение. После этого команда возвращается к вопросу: какие операции ошибаются за последние десять минут? В этой сцене label должен иметь небольшой заранее понятный набор значений, а уникальный контекст должен остаться в логе или трассировке.

\n

Поворот расследования: как label превращается в time series

\n

Имя метрики само по себе не определяет ряд. Ряд задаёт имя плюс полный набор label. Записи store_http_requests_total{operation=\"checkout\",outcome=\"success\"} и store_http_requests_total{operation=\"checkout\",outcome=\"error\"} — два разных ряда. Если добавить user_id, каждый пользователь создаст новый ряд. Если добавить request_id, почти каждый запрос создаст новый ряд.

\n

Для учебного сигнала допустимы три операции и два исхода. Верхняя граница равна 3 × 2 = 6 комбинациям до учёта других labels, например instance и job. Это число можно проверить заранее. Для полного URL граница заранее не задана: число значений следует из трафика, параметров и маршрутов. Для email или UUID набор значений не ограничен контрактом. Поэтому проблема возникает не в синтаксисе метрики, а в контракте данных.

\n
\"Граница
Учебная схема: стабильные значения оставляют число рядов ограниченным, уникальные значения переносят контекст в другой сигнал.
\n

Сначала операционный вопрос

\n

Метрика должна помогать принять решение. Например: «В какой операции за последние десять минут появились ошибки?» Для этого нужны завершённые запросы, стабильное имя операции и нормализованный исход. Не нужны пользователь, текст исключения или URL с query-параметрами. Если вопрос другой, меняется и контракт. «Сколько задач сейчас выполняется?» требует gauge. «Сколько запросов завершилось?» требует counter. «Как распределилась длительность?» требует наблюдений длительности, обычно histogram или summary.

\n

Counter накапливает события и может сброситься при перезапуске процесса. Само значение counter полезно проверить при экспорте, но для окна обычно используют функцию над counter. В учебном запросе ниже increase() считает изменение за интервал, учитывает сброс при перезапуске и экстраполирует его на окно, поэтому результат в общем случае может быть дробным. Это не готовый alert и не production-порог. Число два выбрано только для того, чтобы граница была видна на короткой серии.

\n
Контракт учебной метрики и границы применения
ЭлементРешениеПроверкаОграничение
Метрика запросовstore_http_requests_total, counterСчитаем только завершённые операцииНе объясняет причину ошибки
operationcatalog, checkout, profileПроверяем allowlist до записиНовый маршрут требует изменения контракта
outcomesuccess или errorНормализуем исход в одном местеНе заменяет код HTTP или тип ошибки
Контекст запросаЛог или trace с request IDСвязываем событие по trace IDНе добавляем уникальный ID в metric label
\n

Минимальный контракт в коде

\n

Контракт лучше закрепить рядом с точкой записи. Псевдокод ниже не зависит от конкретной client library. Он показывает порядок проверки: сначала нормализуем значения, затем проверяем длительность и обновляем счётчики. Все значения синтетические. Код не запускает Prometheus и не сообщает ничего о реальной нагрузке.

\n
const 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

Запрос и отрицательный путь

\n

После экспорта можно собрать число завершённых ошибок по операции:

\n
sum 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 — «какой именно запрос и почему».

\n

Есть и менее очевидный отрицательный путь. Разработчик заменяет стабильное имя маршрута на полный URL, чтобы увидеть параметры. В итоге /orders/1 и /orders/2 получают разные значения. Нормализованный маршрут решает только часть задачи: список маршрутов всё равно должен быть ограничен, а редкие динамические значения не должны проходить в label без оценки cardinality.

\n

Симптомы и действия

\n
Диагностическая таблица для 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 и список допустимых значенийВвести нормализацию и явный отказ для неизвестного значения
\n

Порядок проверки

\n
  1. Сформулируйте один вопрос, на который должна ответить метрика. Запишите, какое действие последует после ответа.
  2. Назначьте границу события: например, завершение HTTP-операции. Не смешивайте попытку, ответ клиента и результат фоновой очереди.
  3. Перечислите labels и для каждого укажите источник и полный список допустимых значений.
  4. Посчитайте верхнюю границу комбинаций. Умножьте размеры ограниченных наборов и отдельно учтите labels, которые добавляет окружение.
  5. Проверьте кодовую точку записи. Не допускайте URL, UUID, email, request ID и текста ошибки в label.
  6. Экспортируйте несколько учебных событий и убедитесь, что одинаковые значения дают один ряд, а неизвестные значения не проходят молча.
  7. Проверьте запрос в табличном режиме. Сначала посмотрите число возвращённых series, затем добавьте агрегацию и проверьте оставшиеся labels.
  8. Отдельно проверьте отрицательный путь: неизвестная операция, динамический URL и запрос с уникальным ID должны приводить к явному отказу или к другому сигналу.
  9. Только после этих проверок выбирайте правило или dashboard. Учебный порог нельзя переносить в production без данных о норме, окне, пропусках и стоимости ошибки.
\n

Ограничения

\n

Cardinality — не единственный риск. Даже ограниченный label может быть бесполезным, если команда не знает, какое действие следует из его значения. Метрика не показывает стек ошибки, тело запроса, пользователя или порядок событий. Для этого нужны логи и трассировка. Она также не гарантирует, что scrape не пропустил точку, что все экземпляры используют одну версию схемы или что выбранный порог отражает SLO.

\n

У Prometheus нет универсального безопасного числа для любой системы. В официальных рекомендациях rule of thumb — держать cardinality ниже 10; для метрики, которая уже превысила 100 или может вырасти до такого уровня, предлагают уменьшить число dimensions либо перенести анализ в систему общего назначения. Это ориентиры, а не расчёт ёмкости: фактический предел зависит от числа targets, scrape interval, retention, количества метрик и ресурсов. Поэтому число «шесть комбинаций» выше относится только к учебному сигналу.

\n

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

\n

Проверка завершена, если команда может показать четыре результата: список labels с источниками и верхней границей значений; экспорт учебных событий без уникальных labels; запрос, который оставляет только нужную dimension; и отрицательный тест, в котором неизвестное или уникальное значение не создаёт новый ряд молча. После этого отдельно проверяют нагрузку, правила хранения и production-порог. До этих измерений материал остаётся проверкой контракта, а не доказательством эксплуатационного результата.

\n

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

" }