8 lines
19 KiB
JSON
8 lines
19 KiB
JSON
{
|
||
"index": 266,
|
||
"slug": "editorial-2020-08-mechanism-metrics-basics",
|
||
"title": "Лейблы Prometheus: где заканчивается полезная размерность",
|
||
"excerpt": "Метрика помогает сравнивать операции, пока её labels имеют ограниченный набор значений. Разбираем cardinality, безопасный контракт для кода, запрос по counter и проверку случая, когда сигнал становится слишком дорогим.",
|
||
"contentHtml": "<p>На графике растёт число временных рядов, запросы к Prometheus начинают отвечать медленнее, а полезный разрез всё равно не находится. Частая причина — в label попало значение, которое меняется почти на каждый запрос: полный URL, идентификатор пользователя или request ID. Каждое новое сочетание label создаёт отдельный time series. Цена ошибки — память, CPU, место на диске и потеря доверия к мониторингу. Команда видит много данных, но не получает короткий ответ на операционный вопрос.</p>\n<p>Тезис простой: label должен описывать небольшой и заранее понятный набор вариантов. Если значение растёт вместе с числом пользователей или запросов, это не dimension для метрики. Такой контекст нужно искать в логах или трассировке. В Prometheus полезная размерность заканчивается там, где число комбинаций становится непредсказуемым или не связано с действием оператора.</p>\n<h2>Как label превращается в time series</h2>\n<p>Имя метрики само по себе не определяет ряд. Ряд задаёт имя плюс полный набор label. Записи <code>store_http_requests_total{operation=\"checkout\",outcome=\"success\"}</code> и <code>store_http_requests_total{operation=\"checkout\",outcome=\"error\"}</code> — два разных ряда. Если добавить <code>user_id</code>, каждый пользователь создаст новый ряд. Если добавить <code>request_id</code>, почти каждый запрос создаст новый ряд.</p>\n<p>Для учебного сигнала допустимы три операции и два исхода. Верхняя граница равна 3 × 2 = 6 комбинациям до учёта других labels, например instance и job. Это число можно проверить заранее. Для полного URL такой расчёт невозможен: число значений следует из трафика, параметров и маршрутов. Для email или UUID граница также отсутствует. Поэтому проблема возникает не в синтаксисе метрики, а в контракте данных.</p>\n<figure><img src=\"/assets/editorial/2020/metrics-label-boundary-2020.svg\" alt=\"Граница полезной размерности labels Prometheus: ограниченные operation и outcome образуют небольшой набор рядов, а request ID и полный URL раздувают cardinality\" loading=\"lazy\" /><figcaption>Учебная схема: стабильные значения оставляют число рядов ограниченным, уникальные значения переносят контекст в другой сигнал.</figcaption></figure>\n<h2>Сначала операционный вопрос</h2>\n<p>Метрика должна помогать принять решение. Например: «В какой операции за последние десять минут появились ошибки?» Для этого нужны завершённые запросы, стабильное имя операции и нормализованный исход. Не нужны пользователь, текст исключения или URL с query-параметрами. Если вопрос другой, меняется и контракт. «Сколько задач сейчас выполняется?» требует gauge. «Сколько запросов завершилось?» требует counter. «Как распределилась длительность?» требует наблюдений длительности, обычно histogram или summary.</p>\n<p>Counter накапливает события и может сброситься при перезапуске процесса. Сырым значением удобно проверять экспорт, но для окна обычно используют функцию над counter. В учебном запросе ниже <code>increase()</code> отвечает на вопрос о числе ошибок за интервал. Это не готовый alert и не production-порог. Число два выбрано только для того, чтобы граница была видна на короткой серии.</p>\n<table><caption>Контракт учебной метрики и границы применения</caption><thead><tr><th scope=\"col\">Элемент</th><th scope=\"col\">Решение</th><th scope=\"col\">Проверка</th><th scope=\"col\">Ограничение</th></tr></thead><tbody><tr><td>Метрика запросов</td><td><code>store_http_requests_total</code>, counter</td><td>Считаем только завершённые операции</td><td>Не объясняет причину ошибки</td></tr><tr><td><code>operation</code></td><td><code>catalog</code>, <code>checkout</code>, <code>profile</code></td><td>Проверяем allowlist до записи</td><td>Новый маршрут требует изменения контракта</td></tr><tr><td><code>outcome</code></td><td><code>success</code> или <code>error</code></td><td>Нормализуем исход в одном месте</td><td>Не заменяет код HTTP или тип ошибки</td></tr><tr><td>Контекст запроса</td><td>Лог или trace с request ID</td><td>Связываем событие по trace ID</td><td>Не добавляем уникальный ID в metric label</td></tr></tbody></table>\n<h2>Минимальный контракт в коде</h2>\n<p>Контракт лучше закрепить рядом с точкой записи. Псевдокод ниже не зависит от конкретной client library. Он показывает порядок проверки: сначала нормализуем значения, затем обновляем счётчики. Все значения синтетические. Код не запускает Prometheus и не сообщает ничего о реальной нагрузке.</p>\n<pre><code>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// или произвольный текст ошибки.</code></pre>\n<p>Ошибку неизвестного значения нельзя молча превращать в новый label. Иначе опечатка или новый маршрут незаметно расширит набор рядов. В одном проекте допустимо вернуть событие в общий обработчик ошибок, в другом — записать его в лог и не обновлять эту метрику. Выбор зависит от контракта сервиса. Важно, чтобы отказ был видимым и не создавал бесконечный словарь значений.</p>\n<h2>Запрос и отрицательный путь</h2>\n<p>После экспорта можно собрать число завершённых ошибок по операции:</p>\n<pre><code>sum by (operation) (\n increase(store_http_requests_total{outcome=\"error\"}[10m])\n)\n\n# Учебный запрос: ищем серию с уникальным label.\nstore_http_requests_total{request_id=\"any-value\"}</code></pre>\n<p>Первое выражение агрегирует ограниченные серии и оставляет операцию для сравнения. Второе выражение — не рекомендация, а отрицательный пример. Запрос по <code>request_id</code> может найти отдельный ряд, но сам способ записи создаёт новый ряд для каждого ID. Через короткое время поиск конкретного запроса станет дороже, а сборщик будет хранить данные, которые лучше подходят логам или trace. Метрика отвечает на вопрос «сколько и где», лог или trace — «какой именно запрос и почему».</p>\n<p>Есть и менее очевидный отрицательный путь. Разработчик заменяет стабильное имя маршрута на полный URL, чтобы увидеть параметры. В итоге <code>/orders/1</code> и <code>/orders/2</code> получают разные значения. Нормализованный маршрут решает только часть задачи: список маршрутов всё равно должен быть ограничен, а редкие динамические значения не должны проходить в label без оценки cardinality.</p>\n<h2>Симптомы и действия</h2>\n<table><caption>Диагностическая таблица для label cardinality</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Вероятная причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Число рядов быстро растёт после релиза</td><td>В label попало уникальное или почти уникальное значение</td><td>Посчитать distinct values по новым labels и сравнить с diff кода</td><td>Удалить label, нормализовать значение или перенести контекст в лог</td></tr><tr><td>График показывает слишком много линий</td><td>Запрос сохранил лишние labels и не агрегирует их</td><td>Открыть табличный результат и проверить число series до агрегации</td><td>Оставить только dimension, по которой принимается действие; добавить <code>sum by</code></td></tr><tr><td>Нельзя отличить ошибку одной операции от другой</td><td>Счётчик не содержит стабильного operation label</td><td>Сравнить экспорт и вопрос, который должен поддержать dashboard</td><td>Добавить ограниченный allowlist operation</td></tr><tr><td>По метрике ищут конкретный запрос</td><td>Metric используют вместо логов или трассировки</td><td>Проверить наличие request ID и текста ошибки в label</td><td>Оставить агрегат в Prometheus, связать его с trace ID в другом сигнале</td></tr><tr><td>Новые значения появляются без изменения схемы</td><td>Label принимает пользовательский ввод или свободный текст</td><td>Проверить источник каждого label и список допустимых значений</td><td>Ввести нормализацию и явный отказ для неизвестного значения</td></tr></tbody></table>\n<h2>Порядок проверки</h2>\n<ol><li>Сформулируйте один вопрос, на который должна ответить метрика. Запишите, какое действие последует после ответа.</li><li>Назначьте границу события: например, завершение HTTP-операции. Не смешивайте попытку, ответ клиента и результат фоновой очереди.</li><li>Перечислите labels и для каждого укажите источник и полный список допустимых значений.</li><li>Посчитайте верхнюю границу комбинаций. Умножьте размеры ограниченных наборов и отдельно учтите labels, которые добавляет окружение.</li><li>Проверьте кодовую точку записи. Не допускайте URL, UUID, email, request ID и текста ошибки в label.</li><li>Экспортируйте несколько учебных событий и убедитесь, что одинаковые значения дают один ряд, а неизвестные значения не проходят молча.</li><li>Проверьте запрос в табличном режиме. Сначала посмотрите число возвращённых series, затем добавьте агрегацию и проверьте оставшиеся labels.</li><li>Отдельно проверьте отрицательный путь: неизвестная операция, динамический URL и запрос с уникальным ID должны приводить к явному отказу или к другому сигналу.</li><li>Только после этих проверок выбирайте правило или dashboard. Учебный порог нельзя переносить в production без данных о норме, окне, пропусках и стоимости ошибки.</li></ol>\n<h2>Ограничения</h2>\n<p>Cardinality — не единственный риск. Даже ограниченный label может быть бесполезным, если команда не знает, какое действие следует из его значения. Метрика не показывает стек ошибки, тело запроса, пользователя или порядок событий. Для этого нужны логи и трассировка. Она также не гарантирует, что scrape не пропустил точку, что все экземпляры используют одну версию схемы или что выбранный порог отражает SLO.</p>\n<p>У Prometheus нет универсального безопасного числа для любой системы. Официальные рекомендации предлагают держать cardinality низкой и отдельно расследовать метрики, которые могут вырасти до больших значений. Фактический предел зависит от числа targets, scrape interval, retention, количества метрик и ресурсов. Поэтому число «шесть комбинаций» выше относится только к учебному сигналу; это не расчёт ёмкости конкретного кластера.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Схема готова к следующему этапу, если команда может показать четыре результата: список labels с источниками и верхней границей значений; экспорт учебных событий без уникальных labels; запрос, который оставляет только нужную dimension; и отрицательный тест, в котором неизвестное или уникальное значение не создаёт новый ряд молча. После этого отдельно проверяют нагрузку, правила хранения и production-порог. До этих измерений материал остаётся проверкой контракта, а не доказательством эксплуатационного результата.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://prometheus.io/docs/practices/instrumentation/\" target=\"_blank\" rel=\"noopener noreferrer\">Prometheus: Instrumentation</a> — официальные рекомендации по labels, cardinality, counter и измерениям для online-serving систем.</li><li><a href=\"https://prometheus.io/docs/practices/naming/\" target=\"_blank\" rel=\"noopener noreferrer\">Prometheus: Metric and label naming</a> — официальные правила имён метрик, единиц и label dimensions.</li><li><a href=\"https://prometheus.io/docs/prometheus/latest/querying/functions/\" target=\"_blank\" rel=\"noopener noreferrer\">Prometheus: Query functions</a> — официальное описание <code>increase()</code> и его применения к counter.</li></ul>"
|
||
}
|