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. Записи 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. В учебном запросе ниже 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\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. Через короткое время поиск конкретного запроса станет дороже, а сборщик будет хранить данные, которые лучше подходят логам или 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 нет универсального безопасного числа для любой системы. Официальные рекомендации предлагают держать cardinality низкой и отдельно расследовать метрики, которые могут вырасти до больших значений. Фактический предел зависит от числа targets, scrape interval, retention, количества метрик и ресурсов. Поэтому число «шесть комбинаций» выше относится только к учебному сигналу; это не расчёт ёмкости конкретного кластера.
\nСхема готова к следующему этапу, если команда может показать четыре результата: список labels с источниками и верхней границей значений; экспорт учебных событий без уникальных labels; запрос, который оставляет только нужную dimension; и отрицательный тест, в котором неизвестное или уникальное значение не создаёт новый ряд молча. После этого отдельно проверяют нагрузку, правила хранения и production-порог. До этих измерений материал остаётся проверкой контракта, а не доказательством эксплуатационного результата.
\nincrease() и его применения к counter.В учебном сценарии дежурный открывает график в понедельник утром после релиза: число временных рядов растёт, запросы к 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 и экстраполяции на окно.