Files

2 lines
22 KiB
JSON
Raw Permalink 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":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 &lt; 0) throw new Error('invalid duration');\n}\n\nconst checkoutErrors = events.filter((event) =&gt;\n event.operation === 'checkout' &amp;&amp; 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])) &gt;= 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>"}