2 lines
22 KiB
JSON
2 lines
22 KiB
JSON
{"index":267,"slug":"editorial-2020-08-practice-metrics-basics","title":"Метрики приложения: как превратить сбой в проверяемый сигнал","excerpt":"Общий график запросов не отвечает, где возникла ошибка и что проверять дальше. Разбираем небольшой контракт для HTTP-операции: границу события, тип метрики, ограниченные labels, учебный запрос и действие после порога.","contentHtml":"<p>Представим рабочую проверку в августе 2020 года. Дежурный инженер видит, что график HTTP-запросов растёт, но не может определить, в какой операции появились ошибки. Он открывает несколько дашбордов, сверяет несвязанные пики и вручную ищет строки в журнале. За это время задерживается разбор инцидента, а первое исправление строится на догадке.</p><p>Быстрый способ ухудшить сигнал — добавить в label полный URL, идентификатор пользователя или текст исключения. Prometheus получит множество почти уникальных временных рядов, но следующий шаг расследования не станет яснее. В этой заметке соберём небольшой учебный контракт: одна серверная HTTP-граница, стабильное имя операции, два исхода и отдельный способ искать контекст в логах или трассировке.</p><h2>Сценарий и вопрос</h2><p>Начните не с названия метрики, а с вопроса, на который должен ответить запрос. Например: «Были ли ошибки завершения checkout за последние десять минут и какую проверку выполнить, если их не меньше двух?» Здесь явно заданы операция, исход, окно и действие. Такой вопрос ограничивает модель: мы измеряем завершённую серверную операцию, а не весь пользовательский путь.</p><p>Граница события проходит там, где обработчик уже знает результат. Инкремент в начале обработчика считает попытки, но не доказывает успех. Для ошибок нужен тот же момент завершения и тот же словарь операций. Если внешний платёжный провайдер отвечает позже, это уже другая граница, которую следует измерять отдельным сигналом.</p><div class='table-scroll'><table><caption>Контракт сигнала для учебного сценария</caption><thead><tr><th scope='col'>Величина</th><th scope='col'>Тип и единица</th><th scope='col'>Labels</th><th scope='col'>Вопрос</th><th scope='col'>Не доказывает</th></tr></thead><tbody><tr><td><code>store_http_requests_total</code></td><td>counter, завершённые запросы</td><td><code>operation</code>, <code>outcome</code></td><td>Какая операция и с каким исходом завершилась?</td><td>Причину ошибки и путь до пользователя</td></tr><tr><td><code>store_http_request_duration_seconds</code></td><td>histogram, секунды</td><td><code>operation</code></td><td>Как распределяется длительность?</td><td>Причину медленного ответа</td></tr><tr><td><code>increase(...)</code> за 10 минут</td><td>запрос к counter</td><td><code>operation=checkout</code>, <code>outcome=error</code></td><td>Пересекла ли фикстура учебный порог?</td><td>Production-порог и SLO</td></tr></tbody></table></div><h2>Тип метрики следует из состояния</h2><p><code>store_http_requests_total</code> — накопительный counter. Он увеличивается при событии и может сброситься к нулю после перезапуска процесса. Сырое значение редко отвечает на вопрос о недавнем окне; для количества событий за интервал применяют функцию над изменением counter. В учебном запросе ниже используется <code>increase()</code>, а для скорости событий обычно применяют <code>rate()</code>.</p><p>Gauge описывает состояние, которое может расти и уменьшаться: число запросов в работе, свободную память или температуру. Нельзя использовать gauge для числа ошибок за всё время работы процесса: новое значение затрёт предыдущий снимок. Тип выбирают по форме величины, а не по удобству вызова библиотеки.</p><p>Длительность лучше хранить в секундах и собирать как распределение. Классическая histogram представляет наблюдения bucket-счётчиками, а также суммой и количеством; по bucket-данным можно вычислять квантили и агрегировать экземпляры при согласованных границах. Summary тоже публикует сумму и количество, но его заранее рассчитанные квантили нельзя пересчитать для другого окна и нельзя корректно агрегировать между репликами. Поддержку native histograms и точный API конкретной библиотеки нужно проверять отдельно: учебный контракт ниже её не предполагает.</p><p>Имя метрики должно показывать величину и базовую единицу: суффикс <code>_total</code> у накопительного счётчика и <code>_seconds</code> у длительности. Не смешивайте миллисекунды и секунды под одним именем. Иначе запросы останутся синтаксически правильными, но сравнение будет ложным.</p><div class='table-scroll'><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>Counter</td><td>Накопленные события, с возможным reset</td><td>Завершённые HTTP-запросы</td><td>Значение не должно уменьшаться в обычной работе</td></tr><tr><td>Gauge</td><td>Текущий снимок, который меняется в обе стороны</td><td>Запросы в работе</td><td>Не применять <code>rate()</code> к gauge</td></tr><tr><td>Classic histogram</td><td>Распределение в заданных bucket, сумма и количество</td><td>Длительность запроса</td><td>Проверить границы bucket и возможность агрегации</td></tr><tr><td>Summary</td><td>Сумму, количество и заданные квантили окна</td><td>Локальная оценка latency</td><td>Не складывать квантили разных экземпляров</td></tr></tbody></table></div><h2>Labels должны отвечать на вопрос</h2><p>Каждая уникальная комбинация имени метрики и значений labels образует отдельный временной ряд. Поэтому <code>operation</code> должен брать значения из короткого словаря: например, <code>catalog</code>, <code>checkout</code>, <code>profile</code>. Для <code>outcome</code> достаточно <code>success</code> и <code>error</code>. Такой набор можно перечислить заранее, проверить тестом и использовать в запросах.</p><p>Не добавляйте в этот counter <code>user_id</code>, email, полный URL, request ID, текст исключения или произвольный статус из запроса. Это контекст отдельного события, а не агрегированное измерение. Поместите его в журнал или трассу и свяжите с метрикой через correlation ID. Label должен разделять небольшое число веток, а не хранить историю каждого пользователя.</p><p>Сначала зафиксируйте словарь значений label и вопрос, который он должен отвечать. Затем проверьте, сколько рядов создаст каждая комбинация и какой запрос использует это измерение. После этого можно публиковать сигнал. Если маршрут содержит ID заказа, нормализуйте его до шаблона вроде <code>/orders/:id</code>. Если и шаблонов слишком много, оставьте в метрике только операцию, а конкретный адрес ищите по логу.</p><h2>Воспроизводимый учебный пример</h2><p>Следующий фрагмент не подключает клиент Prometheus и не выдаёт результат production-нагрузки. Это маленькая проверка контракта, которую можно запустить в Node.js: она принимает четыре события, отбрасывает неизвестные значения и считает ошибки только после проверки их границы.</p><pre><code>const allowedOperations = new Set(['catalog', 'checkout', 'profile']);\nconst allowedOutcomes = new Set(['success', 'error']);\nconst events = [\n { operation: 'checkout', outcome: 'success', durationSeconds: 0.42 },\n { operation: 'checkout', outcome: 'error', durationSeconds: 0.90 },\n { operation: 'catalog', outcome: 'success', durationSeconds: 0.18 },\n { operation: 'checkout', outcome: 'error', durationSeconds: 1.10 },\n];\n\nfor (const event of events) {\n if (!allowedOperations.has(event.operation)) throw new Error('unknown operation');\n if (!allowedOutcomes.has(event.outcome)) throw new Error('unknown outcome');\n if (event.durationSeconds < 0) throw new Error('invalid duration');\n}\n\nconst checkoutErrors = events.filter((event) =>\n event.operation === 'checkout' && event.outcome === 'error',\n).length;\nconsole.log(checkoutErrors); // 2</code></pre><p>Ожидаемый вывод — <code>2</code>. Это не значение Prometheus и не готовое правило оповещения. В настоящем обработчике после такой проверки библиотека увеличит counter с теми же labels и передаст длительность в histogram. Точный вызов зависит от языка и клиента, поэтому его нельзя выдавать за универсальный API.</p><p>Отрицательный путь здесь обязателен. Если новый endpoint молча создаёт неизвестное значение <code>operation</code>, график может продолжить работать, а стоимость хранения и смысл агрегации изменятся. В рабочем коде ошибку нужно вернуть вызывающему слою или записать в отдельный технический сигнал, а не продолжать публикацию с неподтверждённым label.</p><figure><img src='/assets/editorial/2020/metrics-signal-contract-2020.svg' alt='Схема выбора метрики: вопрос об операции, counter с operation и outcome, длительность в секундах, учебный запрос и действие после порога' loading='lazy' /><figcaption>Сигнал отделяет факт завершения операции от следующего шага расследования.</figcaption></figure><h2>Запрос и границы порога</h2><p>Для учебной серии запрос может выглядеть так:</p><pre><code>sum(increase(store_http_requests_total{operation=\"checkout\",outcome=\"error\"}[10m])) >= 2</code></pre><p>Выражение суммирует изменение counter за десять минут и проверяет условие. Число <code>2</code> выбрано для упражнения: одна ошибка показывает, что label работает, две переводят пример в следующую ветку. Оно не получено из трафика, не является допустимой долей ошибок и не должно копироваться в production alert.</p><p><code>increase()</code> работает по диапазону наблюдений и учитывает reset counter, но результат зависит от точек scrape и наличия ряда. Локальная разность двух чисел в памяти не доказывает, что PromQL вернёт такое же значение. После перезапуска процесса, задержки scrape или пропуска ряда нужна отдельная проверка. Отсутствие ряда также не следует автоматически читать как ноль ошибок.</p><p>Если сервис запущен в нескольких экземплярах, <code>sum()</code> объединяет их только при совместимом наборе labels. До запроса проверьте, не добавляет ли инфраструктура собственные измерения, и решите, нужно ли сохранить разрез по экземпляру для диагностики. Для пользовательского опыта метрику надо связать с логом, трассировкой и клиентской проверкой: серверный counter не доказывает, что браузер отобразил правильный экран.</p><h2>Симптом → причина → проверка → действие</h2><div class='table-scroll'><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>Нет ограниченного label <code>operation</code></td><td>Посмотреть series и словарь операций</td><td>Добавить короткий словарь и тест неизвестного значения</td></tr><tr><td>График быстро растёт числом рядов</td><td>В label попал ID, URL или другой unbounded value</td><td>Найти значения с высокой уникальностью</td><td>Перенести контекст в лог или trace</td></tr><tr><td>Ошибки считают попытки</td><td>Counter увеличивается до определения исхода</td><td>Сопоставить точку инкремента с завершением</td><td>Разделить attempts и outcomes или перенести инкремент</td></tr><tr><td>Порог ведёт себя неверно после перезапуска</td><td>Сырые значения counter сравнивают как gauge</td><td>Проверить reset и range vector</td><td>Использовать <code>rate()</code> или <code>increase()</code></td></tr><tr><td>Нет записи до первой ошибки</td><td>Ряд появляется только после события</td><td>Запросить известный labelset заранее</td><td>Инициализировать нулевую серию, если это оправдано</td></tr></tbody></table></div><h2>Порядок внедрения</h2><ol><li>Запишите операционный вопрос: операция, исход, окно и действие после порога.</li><li>Выберите границу завершения и определите, что считается успехом и ошибкой.</li><li>Составьте allowlist для labels и оставьте только измерения, которые можно перечислить и агрегировать.</li><li>Назовите метрики по одной величине и базовой единице; проверьте <code>_total</code> и <code>_seconds</code>.</li><li>Добавьте проверки неизвестной операции, исхода и отрицательной длительности.</li><li>Прогоните контролируемую серию и сравните ожидаемое число ошибок с exposition или API выбранной библиотеки.</li><li>Проверьте PromQL на тестовом Prometheus; смоделируйте reset, отсутствие ряда и задержку scrape.</li><li>Свяжите каждую ветку порога с действием: открыть лог, повторить сценарий, проверить релиз или ничего не делать при учебном нуле.</li></ol><h2>Ограничения</h2><p>Эта схема показывает контракт сигнала, но не причину ошибки. Для причины понадобятся логи, трассы, код ответа, версия релиза и контекст внешних зависимостей. Она не измеряет путь браузера, DNS, сеть и базу, если запрос не дошёл до сервера. Она также не выбирает за команду SLO и не говорит, сколько ложных срабатываний допустимо.</p><p>Точный синтаксис клиента, поддержка native histograms и формат exposition зависят от версии и языка. Для старой установки сначала сверяйте версию Prometheus и документацию используемой библиотеки. Нельзя переносить учебный порог <code>2</code>, словарь операций или bucket-границы в production без наблюдения и отдельного решения владельца сервиса.</p><p>Учебные операции, события, длительности и порог выдуманы. Здесь нет production-нагрузки, измеренного уменьшения инцидентов или готового alert. Проверяемый результат скромнее: один вопрос превращён в контракт, который можно прогнать, запросить и связать с конкретным следующим действием.</p><h2>Критерий готовности</h2><p>Сигнал готов к первой проверке, если другой инженер без устного объяснения может назвать границу события, перечислить допустимые labels, воспроизвести учебный вывод <code>2</code>, выполнить запрос и пройти отрицательный путь с неизвестным label. После reset и пропущенного ряда команда понимает, что означает «нет данных», а что — «ошибок не было». Если пункт не выполняется, уточняйте контракт и проверку, а не добавляйте новые labels.</p><h2>Проверяемые источники</h2><ul><li><a href='https://prometheus.io/docs/practices/instrumentation/' target='_blank' rel='noopener noreferrer'>Prometheus: Instrumentation</a> — выбор counter/gauge, осторожность с labels, rate и отсутствующими рядами.</li><li><a href='https://prometheus.io/docs/concepts/metric_types/' target='_blank' rel='noopener noreferrer'>Prometheus: Metric types</a> — определения counter, gauge, classic/native histogram и summary.</li><li><a href='https://prometheus.io/docs/practices/naming/' target='_blank' rel='noopener noreferrer'>Prometheus: Metric and label naming</a> — единицы, суффиксы, измерения и риск высокой cardinality.</li><li><a href='https://prometheus.io/docs/practices/histograms/' target='_blank' rel='noopener noreferrer'>Prometheus: Histograms and summaries</a> — различия bucket-распределения и заранее рассчитанных quantiles.</li></ul>"}
|