Files
progcode/editorial/agent-rewrites/266.json
T

8 lines
19 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 266,
"slug": "editorial-2020-08-mechanism-metrics-basics",
"title": "Лейблы Prometheus: где заканчивается полезная размерность",
"excerpt": "На примере дежурства разбираем, почему label должен иметь ограниченный набор значений, как cardinality превращается в стоимость и каким запросом проверить counter, не превращая Prometheus в хранилище контекста.",
"contentHtml": "<p>В учебном сценарии дежурный открывает график в понедельник утром после релиза: число временных рядов растёт, запросы к Prometheus отвечают медленнее, а нужный разрез не находится. Сначала он подозревает нехватку ресурсов и хочет увеличить retention. Цена такого шага — дополнительные CPU, RAM и диск без гарантии, что оператор увидит нужный сигнал.</p>\n<p>Он проверяет не только сервер, но и diff instrumentation. В новом счётчике рядом со стабильной <code>operation</code> появилась <code>request_id</code>; почти каждый запрос принёс новое значение. После этого команда возвращается к вопросу: какие операции ошибаются за последние десять минут? В этой сцене label должен иметь небольшой заранее понятный набор значений, а уникальный контекст должен остаться в логе или трассировке.</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 полезно проверить при экспорте, но для окна обычно используют функцию над 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 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// или произвольный текст ошибки.</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. Через короткое время поиск конкретного запроса станет дороже, а Prometheus будет хранить данные, которые лучше подходят логам или 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 нет универсального безопасного числа для любой системы. В официальных рекомендациях rule of thumb — держать cardinality ниже 10; для метрики, которая уже превысила 100 или может вырасти до такого уровня, предлагают уменьшить число dimensions либо перенести анализ в систему общего назначения. Это ориентиры, а не расчёт ёмкости: фактический предел зависит от числа 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>"
}