Files
progcode/editorial/agent-rewrites/267.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

2 lines
20 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":267,"slug":"editorial-2020-08-practice-metrics-basics","title":"Метрики приложения: как превратить сбой в проверяемый сигнал","excerpt":"Общий график запросов не отвечает, где возникла ошибка и что проверять дальше. Разбираем небольшой контракт для HTTP-операции: тип метрики, ограниченные labels, учебный запрос и явное действие после порога.","contentHtml":"<p>Симптом выглядит безобидно: график HTTP-запросов растёт, но из него нельзя понять, в какой операции появились ошибки. Команда открывает несколько дашбордов, сверяет несвязанные пики и вручную ищет нужные строки в логах. За это время растёт очередь разбора, задерживается релиз, а исправление опирается на догадки. Если в метрику добавить полный URL, пользователя и текст исключения, сигнал станет ещё дороже: Prometheus получит множество почти уникальных рядов, но причина сбоя не станет яснее.</p><p>Первая метрика должна отвечать на один операционный вопрос и вести к следующей проверке. Для HTTP-операции достаточно зафиксировать завершение, стабильное имя операции и исход <code>success</code> или <code>error</code>. Затем можно посчитать ошибки за окно и решить, открыть ли связанный лог или повторить сценарий. Такая метрика не заменяет трассировку, журнал и проверку клиента. Она сужает поиск и делает его воспроизводимым.</p><h2>Сначала вопрос, потом имя</h2><p>Запишите вопрос до кода: «Были ли ошибки завершения checkout за последние десять минут и какую проверку выполнить, если их не меньше двух?» В нём есть операция, исход, окно и действие. Общее число запросов отвечает только на вопрос «сколько раз обработчик завершился». Оно не разделяет каталог, checkout и профиль. Поэтому счётчик нужно привязать к месту, где обработчик уже знает результат.</p><p>Увеличивайте счётчик после завершения операции. Если поставить его в начале обработчика, он будет считать попытки. Это тоже полезная величина, но её нельзя молча называть числом успешных запросов. Ошибку учитывайте в той же границе, где определяете исход. Тогда число попыток, число ошибок и длительность относятся к одному событию. Если исход приходит из внешней системы позже, границу надо описать отдельно.</p><p>В этой статье разбирается только серверная HTTP-граница. Доступность браузера, DNS, сеть, база, очередь и ручное действие оператора остаются отдельными сигналами. Один counter не доказывает, что пользователь увидел корректный экран. Он показывает, что выбранная операция завершилась с указанным исходом.</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 или summary, секунды</td><td><code>operation</code></td><td>Как распределяется длительность?</td><td>Причину медленного ответа</td></tr><tr><td>Учебное условие <code>&gt;= 2</code></td><td><code>increase()</code> за 10 минут</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 редко отвечает на вопрос о недавнем окне. Для количества событий за интервал применяют функцию над изменением счётчика. В учебном выражении ниже используется <code>increase()</code>; для скорости событий обычно применяют <code>rate()</code>.</p><p>Gauge подходит для состояния, которое может расти и уменьшаться: текущего числа запросов в работе, свободной памяти или температуры. Ставить gauge для числа ошибок за весь срок работы процесса неправильно: обновление может затереть накопленную историю. Выбирайте тип по форме величины, а не по тому, как удобнее вызвать метод библиотеки.</p><p>Для длительности храните секунды и собирайте распределение наблюдений. Histogram даёт buckets, сумму и количество наблюдений. Summary также считает сумму и количество, но его квантили имеют другую семантику и требуют отдельного выбора. В первом сигнале не нужно обещать p95 или SLO. Сначала договоритесь, что операция имеет стабильное имя и что единица времени одинакова везде.</p><p>Суффикс <code>_total</code> показывает накопительную природу counter. В имени длительности есть <code>_seconds</code>. Не смешивайте миллисекунды и секунды под одним именем. Иначе запросы будут синтаксически корректными, но сравнение значений станет ложным.</p><h2>Labels должны отвечать на вопрос</h2><p>Каждая уникальная комбинация имени метрики и labels образует отдельный time series. Поэтому <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 нельзя перечислить заранее, остановитесь и проверьте его смысл. Сколько рядов оно добавит? Какой запрос использует его? Можно ли нормализовать маршрут до шаблона, например <code>/orders/:id</code>, вместо полного URL с идентификатором заказа? Ответы должны быть в контракте до публикации метрики.</p><h2>Учебный пример: четыре события и два snapshots</h2><p>Ниже не клиент Prometheus и не результат нагрузки. Это учебный набор с выдуманными значениями. Он проверяет разрешённые labels, суммирование ошибок и поведение при неизвестной операции. В реальном сервисе инкремент выполняет выбранная библиотека, а результат нужно проверить на нужной версии сервера и с реальным интервалом scrape.</p><pre><code>const allowedOperations = new Set(['catalog', 'checkout', 'profile']);&lt;br&gt;const allowedOutcomes = new Set(['success', 'error']);&lt;br&gt;&lt;br&gt;const events = [{ operation: 'checkout', outcome: 'success', durationSeconds: 0.42 }, { operation: 'checkout', outcome: 'error', durationSeconds: 0.90 }, { operation: 'catalog', outcome: 'success', durationSeconds: 0.18 }, { operation: 'checkout', outcome: 'error', durationSeconds: 1.10 }];&lt;br&gt;&lt;br&gt;for (const event of events) {&lt;br&gt; if (!allowedOperations.has(event.operation)) throw new Error('unknown operation');&lt;br&gt; if (!allowedOutcomes.has(event.outcome)) throw new Error('unknown outcome');&lt;br&gt; if (event.durationSeconds &amp;lt; 0) throw new Error('invalid duration');&lt;br&gt;}&lt;br&gt;&lt;br&gt;// Учебный результат: checkout/error = 2.&lt;br&gt;// Это не production-данные и не готовый alert rule.</code></pre><p>Проверка отбрасывает неизвестную операцию до публикации значения. Это отрицательный путь, и он важнее красивой строки exposition. Если новый endpoint молча создаёт 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>Запрос зависит от точек scrape и от обработки reset counter. Локальная разность двух чисел в памяти не доказывает, что production PromQL вернёт такое же значение. При перезапуске процесса, задержке scrape или отсутствии ряда результат требует отдельной проверки. Нулевое значение также не всегда означает «ошибок не было»: ряд мог ещё не появиться.</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>Нет bounded label <code>operation</code></td><td>Посмотреть series и словарь операций</td><td>Добавить короткий словарь и тест неизвестного значения</td></tr><tr><td>График растёт числом рядов</td><td>В label попал ID, URL или другой unbounded value</td><td>Найти label с почти уникальными значениями</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>Добавьте проверки неизвестной операции, исхода и отрицательной длительности. Убедитесь, что labels не содержат пользовательских значений.</li><li>Прогоните контролируемую серию и сравните ожидаемое число ошибок с exposition или API библиотеки.</li><li>Проверьте PromQL на тестовом Prometheus. Смоделируйте reset, отсутствие ряда и задержку scrape.</li><li>Привяжите каждую ветку порога к действию: открыть лог, повторить сценарий, проверить релиз или ничего не делать при учебном нуле.</li></ol><h2>Ограничения</h2><p>Эта схема не показывает причину ошибки. Для неё понадобятся логи, трассы, код ответа, версия релиза и контекст внешних зависимостей. Она не измеряет пользовательский опыт, если запрос не дошёл до сервера или ответ испортился в браузере. Она не выбирает за команду SLO и не говорит, сколько ложных срабатываний допустимо.</p><p>Порог нельзя назначать по удобному числу. Нужны период наблюдения, стоимость ошибки, ожидаемый трафик и владелец реакции. Если трафик почти нулевой, две ошибки и две тысячи ошибок имеют одинаковое значение в абсолютном counter, но разный смысл для продукта. Для сравнения сервисов нужна доля ошибок или другой согласованный показатель, а не копирование окна.</p><p>Учебные операции, события, длительности и порог выдуманы. Здесь нет production-нагрузки, измеренного уменьшения инцидентов или готового alert. Проверяемый результат скромнее: один вопрос превращён в контракт, а контракт можно прогнать, запросить и связать с конкретным следующим действием.</p><h2>Критерий готовности</h2><p>Сигнал готов к первой проверке, если другой инженер без устного объяснения может назвать границу события, перечислить допустимые labels, воспроизвести учебные значения, получить ожидаемый результат запроса и пройти отрицательный путь с неизвестным label. После reset и пропущенного ряда команда понимает, что означает «нет данных», а что — «ошибок не было». Если пункт не выполняется, уточняйте контракт и проверку, а не добавляйте новые labels.</p><h2>Проверяемые источники</h2><ul><li><a href=\"https://prometheus.io/docs/practices/instrumentation/\" target=\"_blank\" rel=\"noopener noreferrer\">Prometheus: Instrumentation</a> — рекомендации по запросам, ошибкам, latency и labels.</li><li><a href=\"https://prometheus.io/docs/concepts/metric_types/\" target=\"_blank\" rel=\"noopener noreferrer\">Prometheus: Metric types</a> — определения counter, gauge, histogram и summary.</li><li><a href=\"https://prometheus.io/docs/practices/naming/\" target=\"_blank\" rel=\"noopener noreferrer\">Prometheus: Metric and label naming</a> — правила имён, единиц и label dimensions.</li></ul>"}