✻ Урок 7.1 · Тема 7: Метрики, логи, трейсы и алерты
Метрики и Prometheus: типы метрик, сбор, /metrics
Содержание урока
Зачем это нужно
Ты пришёл в команду интернет-магазина, и впереди большая распродажа. В прошлом году сайт на ней упал, а на разборе сказали: «Мы не знали почему». Мы поставим приборы, чтобы в этом году знать. Я буду рядом как опытный коллега и объясню то, что мне самому когда-то пришлось выяснять на ошибках.
Начну с истории. Мы запустили мой первый нагрузочный тест. Генератор нагрузки (программа, которая засыпает сайт запросами) честно сообщил: на третьей минуте ответы стали медленными, потом пошли ошибки. Полдня мы гадали: база, процессор или сеть? Каждый защищал свою версию, а проверить никто не мог, потому что внутрь сервиса мы не заглядывали. Назавтра мы поставили мониторинг и повторили тест. На графике было видно: свободные соединения с базой закончились на второй минуте, и запросы выстроились в очередь. Версию проверили за пять минут вместо полудня. С тех пор я не запускаю тест, пока не вижу приборы.
Генератор видит сервис только снаружи, а причина сидит внутри. Увидеть её можно, если сервис сам рассказывает о себе, а кто-то это записывает.
То, что сервис рассказывает о себе, называется метрикой: мы встречали её в уроке 2.3 как число вроде «сколько запросов обработано». Записанные во времени числа превращаются в график, а по графику видно, что и когда сломалось. Записывает их Prometheus: программа, которая сама обходит сервисы, собирает числа, хранит и отвечает на вопросы вроде «сколько запросов в секунду было вчера в три часа». На работе «глянь метрики» почти всегда значит «открой Prometheus».
Шаг проекта: ты включаешь профиль мониторинга (--profile monitoring) и проверяешь, что Prometheus видит все восемь «целей» (адресов, откуда он собирает числа). Потом составляешь в ~/perf-lab/07-monitoring/metrics-catalog.md каталог метрик «Магазина»: имя, тип, метки (уточнения к числу), смысл.
Что нужно знать
- Терминал,
curlиgrep: урок 1.4. Для разбора JSON-ответов используемjq, он установлен в уроке 1.1. - HTTP: метод, путь, код ответа 200 и 5xx, из чего состоит запрос: урок 2.1.
- Стенд «Магазин» запущен и отвечает на
localhost:8000: урок 2.1, как он собран из контейнеров: тема 5, командыdocker compose up -dиdown: урок 5.3. - Ничего о задержке и перцентилях знать заранее не нужно: ниже каждый термин объяснён с нуля, а подробный разбор будет в уроке 8.1.
Картина целиком
Возьми приборную панель автомобиля. Спидометр показывает скорость прямо сейчас, одометр складывает пройденные километры. Теперь добавь штурмана, который каждые пять секунд заглядывает на панель и записывает показания в блокнот с временем. Вечером по блокноту рисуется график поездки.
В мониторинге те же роли. Датчики встроены в сервис: он считает свои запросы и ошибки и отдаёт числа обычной текстовой страницей по адресу /metrics. Штурман это Prometheus: по расписанию заходит на эту страницу каждого сервиса и пишет числа в блокнот (базу, где числа хранятся вместе со временем). График рисует Grafana, тревогу (алерт) дежурному поднимает Alertmanager, когда числа выглядят опасно. Всю цепочку ты соберёшь по ходу темы.
flowchart TD
A["Магазин<br>/metrics на :8000"] --> P["Prometheus<br>забирает числа сам"]
B["Оплата<br>/metrics на :8001"] --> P
P --> D[("База с временем<br>блокнот штурмана")]
D --> G["Grafana<br>графики"]
D --> R["Правила алертов<br>→ Alertmanager"]
Стрелки от сервисов к Prometheus читай как «Prometheus приходит и забирает», а не «сервис отправляет». Почему именно так, увидишь в теории.
Теория
Метрики, логи и трассы: три вида данных
Сервис упал. Откуда узнать почему? Ответ зависит от того, что он о себе записывал. Способность понять по данным сервиса, что у него внутри, называют наблюдаемостью (observability).
Представь больницу. У кровати стоят приборы: пульс, давление, температура, одно число каждого вида раз в минуту. Это метрики: видно, что температура растёт третий час, но не видно, что съел пациент. Медсестра ведёт записи: «14:05 дали лекарство, жалуется на головокружение». Это логи (logs): каждое событие отдельной строкой, с подробностями. А маршрутный лист «приёмная, рентген, палата» с временем на каждом этапе это трассы (traces). Аналогия ломается в одном: метрика складывает события тысяч пользователей в одно число, судьбу одного запроса по ней не разобрать.
В «Магазине» метрика это http_requests_total, сколько запросов обработано. Лог это JSON-строка «запрос завершён, статус 502, 2014 мс». Трасса: «заказ занял 2100 мс, из них оплата 2050». Логи разберём в уроке 7.5, трассы в уроке 7.7. Начнём с метрик: на тесте идут тысячи запросов в секунду, читать их по одному нельзя.
Осторожно: метрика не лог. Ста тысячам запросов соответствует сто тысяч строк лога, а метрика остаётся одним числом, которое выросло. Поэтому метрики дешёвые, но детали в них теряются.
Главное: метрики показывают, что и сколько, логи объясняют конкретное событие, трассы показывают, где потерялось время.
Посмотрим, из чего устроено одно число метрики.
Имя, метки, значение: из чего сделана метрика
Ты видишь «ошибок 17». Ошибок где, каких, за какой период? Метрику нужно описать так, чтобы по ней можно было задавать вопросы. Вот одна запись в Prometheus:
http_requests_total{method="GET", route="/api/products", status="200"} 1542 в момент 14:05:05
Сначала имя: что считаем. В скобках метки (labels), пары «ключ=значение»: они уточняют число. Свой счёт у GET (чтение страницы) на /api/products с кодом 200, свой у POST на /api/orders с кодом 502. Дальше значение, 1542. Время замера Prometheus ставит сам.
Имя плюс полный набор меток называют временным рядом (time series, «серия»). У каждой серии копится список пар «время, значение»: (14:05:00, 1500), (14:05:05, 1542), (14:05:10, 1577). Его хранит Prometheus и по нему строит графики. Хранилище называют TSDB (time series database, база временных рядов): оно быстро дописывает точки в конец серий и быстро читает их за период.
Прикинь сам: сервис получал запросы
GET /api/productsс кодами 200 и 500 иPOST /api/ordersс кодом 201. Сколько рядов уhttp_requests_total?
Три: {GET, /api/products, 200}, {GET, /api/products, 500} и {POST, /api/orders, 201}. Любая отличающаяся метка даёт новую серию, а считаются только комбинации, которые реально встречались.
Осторожно: метка это часть идентификатора, а не комментарий. Добавишь завтра version="2", и старая и новая серии станут разными: у старой история оборвётся, у новой начнётся с нуля, и график «разорвётся».
Главное: метрика это имя, метки и значение, а имя вместе с метками задаёт серию, у которой Prometheus хранит историю.
Как эти числа попадают в Prometheus?
Pull: Prometheus сам приходит за числами
Если сервис молчит, он мёртв или ему нечего сказать? От способа доставки чисел зависит, сможешь ли ты это отличить. Есть два способа. Push («толкать»): сервис сам отправляет числа в хранилище, когда захочет. Pull («тянуть»): хранилище само приходит и забирает. Prometheus выбрал pull.
В больнице это санитар с градусником: обходит палаты и, если пациент не отвечает, записывает «не отвечает». А если бы пациент сам шёл в процедурную, не поймёшь, здоров он или потерял сознание.
Один заход Prometheus за числами называют скрейпом (scrape, «соскоб»). Каждые 5 секунд он делает запрос GET /metrics на адрес сервиса, получает текст «имя{метки} число», ставит на все числа одно время и дописывает их в серии. Заодно записывает метрику up для этого адреса: 1, если скрейп удался, 0, если нет.
sequenceDiagram
participant П as Prometheus
participant М as Магазин
loop каждые 5 с
П->>М: GET /metrics
М-->>П: текст: имя{метки} число
Note over П: ставит время и записывает в базу
end
П->>М: GET /metrics
М--xП: нет ответа
Note over П: up = 0, цель DOWN
Сервис не знает, кто и когда придёт. Нет ответа? Prometheus пишет up = 0: молчание тоже сигнал, по нему строят алерт «сервис недоступен».
Адрес, с которого собирают числа (например, shop:8000), называют целью (target). Группа однотипных целей это job («задание»): все экземпляры «Магазина» образуют job shop. Конкретный адрес внутри job это instance. Обе метки Prometheus добавляет к каждой серии сам, поэтому можно писать up{job="shop"}.
Теперь важное: между скрейпами Prometheus слепой, он смотрит снимками. Посмотри, как это влияет на два вида чисел:
Виджет показывает по одним событиям два вида чисел: счёт всех запросов с начала и число запросов «в работе» сейчас. Счёт не теряет ничего: разница двух соседних замеров точно говорит, сколько запросов прошло между ними. Число запросов «в работе» показывает только момент замера, и короткий всплеск между замерами не виден. Короткие события ловят накопленные счётчики, а текущим уровням доверяй с оглядкой.
Осторожно: Prometheus видит сервис не непрерывно, а раз в scrape_interval.
Главное: Prometheus сам ходит за числами каждые несколько секунд, а недоступный сервис записывается как
up = 0.
Теперь посмотрим глазами на то, что именно отдаёт сервис.
Как выглядит /metrics
Открой в браузере адрес сервиса с /metrics, и увидишь обычный текст: формат простой, чтобы любой сервис мог его выдать. «Магазин» делает это библиотекой prometheus_client. Вот выдержка из curl -s localhost:8000/metrics после немного нагрузки:
# HELP http_requests_total Запросы HTTP
# TYPE http_requests_total counter
http_requests_total{method="GET",route="/api/products",status="200"} 1542.0
http_requests_total{method="GET",route="/api/products/{id}",status="200"} 318.0
http_requests_total{method="GET",route="/api/products/{id}",status="404"} 6.0
# HELP shop_db_pool_available Свободные соединения
# TYPE shop_db_pool_available gauge
shop_db_pool_available 3.0
Строка # HELP это человеческое описание, его пишет автор сервиса. Строка # TYPE объявляет тип метрики: здесь counter и gauge, всего их четыре. Остальные строки это данные имя{метки} значение. Точка в 1542.0 это манера Python.
Главное:
/metricsэто обычный HTTP-путь с текстом, его можно открыть в браузере или вызвать черезcurl, а в строке# TYPEнаписан тип метрики.
Для любопытных: чего нет в /metrics «Магазина»
Данные в TSDB живут 15 дней, потом удаляются. Метрики нескольких воркеров «Магазина» (число задаёт WEB_CONCURRENCY) библиотека суммирует в один ответ.
Пути /healthz, /readyz и сам /metrics в HTTP-метрики не попадают: код стенда исключает их, чтобы проверки здоровья и скрейп не искажали число запросов в секунду.
Типов четыре, начнём со счётчика.
Counter: счётчик, который только растёт
Ты построил график http_requests_total и видишь лестницу вверх. Нагрузка растёт? Нет. Метрика устроена как одометр: показывает километры за всю жизнь машины, назад не открутить. Скорость узнают, сравнив два показания и поделив на время между ними.
Такой тип называется counter: одно число, которое только растёт (при перезапуске сервиса оно обнуляется). Большое значение само по себе ничего не говорит, полезна скорость роста.
Прикинь сам: в 14:05:00 счётчик показывал 1500, в 14:05:05 уже 1542. Сколько запросов в секунду пришло?
За 5 секунд прибавилось 1542 - 1500 = 42 запроса, значит 42 / 5 = 8,4 запроса в секунду. Это делает функция rate() из следующего урока. Имена счётчиков заканчиваются на _total: http_requests_total{method,route,status}, shop_orders_created_total, shop_cache_requests_total{result}, shop_payment_requests_total{result}.
Осторожно: после перезапуска счётчик падает в ноль. Это нормально, rate() такой сброс учитывает.
Главное: счётчик хранит накопленный итог, поэтому смотрят не на значение, а на скорость роста.
Но не всё только растёт.
Gauge: стрелка, которая ходит вверх и вниз
Сколько запросов ждут соединение с базой прямо сейчас? Три, потом ноль, потом пять. Это не итог, а текущее состояние, как у спидометра: показывает «сколько сейчас» и ходит в обе стороны.
Такой тип называется gauge, и на графике смотрят само значение. В «Магазине» это http_requests_in_progress (запросов в работе), shop_db_pool_size (соединений в пуле), shop_db_pool_available (из них свободных) и shop_db_pool_waiting (запросов, ждущих соединение). Пул соединений мы разбирали в уроке 2.3: заранее открытые соединения с базой, выдаваемые запросам по очереди. Свободных нет, запрос ждёт: это и показывает shop_db_pool_waiting.
Осторожно: для счёта событий («сколько заказов было») новички берут gauge. Тогда при перезапуске история теряется, а «сколько за последнюю минуту» не посчитать. Правило: считаешь события, нужен counter. Меряешь текущее состояние, нужен gauge.
Главное: gauge показывает состояние сейчас и ходит в обе стороны, а Prometheus видит его только в моменты скрейпа.
Остался самый хитрый случай: время ответа.
Histogram: распределение по корзинам
Сколько длится запрос? Одно число не годится, и среднее врёт (подробно в уроке 8.1): надо знать, сколько запросов ответили быстро, сколько медленно. Хранить время каждого запроса нельзя: их миллионы. Поэтому тип histogram заранее делит время на корзины (buckets) и считает, сколько запросов в каждую уложилось. Это как сортировка писем по районам: точный адрес не нужен, а распределение видно.
Границы корзин у «Магазина» заданы в коде: от 0.005 до 10 секунд (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.3, 0.5, 1, 2.5, 5, 10). У каждой корзины свой счётчик с меткой le («меньше или равно», less or equal): le="0.1" считает всех, кто ответил за 100 мс или быстрее. Корзины накопительные: каждая следующая включает предыдущие. Последняя, le="+Inf" («до бесконечности»), считает вообще все запросы. Рядом лежат _count (всего запросов) и _sum (суммарное время всех запросов в секундах):
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.005",method="GET",route="/api/products"} 12.0
http_request_duration_seconds_bucket{le="0.01",method="GET",route="/api/products"} 233.0
http_request_duration_seconds_bucket{le="0.025",method="GET",route="/api/products"} 1301.0
http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/api/products"} 1542.0
http_request_duration_seconds_sum{method="GET",route="/api/products"} 21.84
http_request_duration_seconds_count{method="GET",route="/api/products"} 1542.0
Показаны не все 12 корзин. Читаем: 12 запросов уложились в 5 мс, 233 в 10 мс, значит между 5 и 10 мс их 233 - 12 = 221. Всего запросов 1542, и _count совпадает с le="+Inf".
Прикинь сам: какое среднее время ответа и какая доля запросов медленнее 25 мс?
Среднее это суммарное время, делённое на число запросов: 21,84 / 1542 = 0,0142 с, то есть 14,2 мс. Не медленнее 25 мс ответили 1301 запрос, это 1301 / 1542 = 84%. Значит, 1542 - 1301 = 241 запрос были медленнее, при среднем в 14 мс. Такие хвосты прячет среднее. Как получить из корзин перцентили (p95, p99), ты узнаешь в уроке 7.2.
Осторожно: корзины накопительные, а не «сколько в каждом диапазоне». Границы задаёт код: если все запросы медленнее 10 секунд, перцентиль выйдет «больше 10 с» и точнее не скажешь. И каждая корзина это отдельная серия.
Четвёртый тип, summary, похож на histogram, но перцентили вычисляет сам сервис. Их нельзя объединять: p95 двух серверов не равен p95 всего кластера. В «Магазине» summary нет, но на собеседовании про него спросят.
| Тип | Что хранит | Что смотреть | Пример в «Магазине» |
|---|---|---|---|
| counter | Накопленное число событий | Скорость роста: rate() |
http_requests_total |
| gauge | Текущее значение | Само значение | shop_db_pool_available |
| histogram | Счётчики по корзинам времени | Перцентили: histogram_quantile() |
http_request_duration_seconds |
| summary | Готовые перцентили | Само значение, объединять нельзя | нет |
Проверь понимание:
shop_orders_created_totalпоказывает 840, после перезапуска 0, потом 25. Это ошибка? Аshop_db_pool_waitingвдруг упал с 5 до 0?
Ответ
Для счётчика падение до нуля при перезапуске нормально: процесс начал считать заново, а rate() не считает такой сброс отрицательным ростом. Для gauge падение с 5 до 0 тоже нормально: очередь за соединениями рассосалась. Gauge и должен ходить вверх и вниз.
Главное: histogram хранит накопительные счётчики по корзинам времени, из них считают среднее (
_sum / _count) и перцентили.
С типами разобрались. Но метки, которые мы навешиваем на метрики, имеют цену.
Метки и кардинальность
Я видел, как мониторинг упал от одной метки с идентификатором пользователя. Посчитаем, как так выходит. Каждая комбинация значений меток это отдельная серия, и Prometheus хранит её отдельно: память, диск, время. Число серий метрики называют кардинальностью (cardinality), и оно растёт перемножением.
У http_requests_total три метки. Метод: GET, POST, DELETE, три значения. Маршрут: десять шаблонов и other, одиннадцать. Код ответа: 200, 201, 204, 400, 401, 404, 409, 422, 500, 502, 503, 504, двенадцать. Максимум 3 × 11 × 12 = 396 серий. Реально меньше: серия рождается при первом событии, и пока ни один запрос не вернул 503, серии с status="503" нет. Поэтому на чистом стенде http_requests_total может быть пустой, и это не поломка.
С гистограммой серьёзнее. У http_request_duration_seconds две метки, но каждая комбинация даёт 12 корзин, плюс +Inf, _sum и _count: 15 серий. При десяти живых парах «метод и маршрут» это 150 серий только на задержку:
Числа примерные: у счётчика запросов в этом тесте было 12 живых комбинаций, а у гистограммы каждая даёт 15 серий. Она весит примерно в двенадцать раз больше.
Прикинь сам: разработчик добавил в
http_request_duration_secondsметкуuser_id. В «Магазине» тысяча пользователей. Сколько серий получится?
На каждого пользователя 15 серий для каждой из десяти пар: 1000 × 10 × 15 = 150 000 вместо 150. С миллионом пользователей было бы 150 миллионов.
Отсюда правило: у метки должно быть небольшое, ограниченное число значений. Метод, шаблон маршрута, код ответа, результат оплаты (ok, error, timeout). Нельзя класть user_id, order_id, request_id, полный адрес с числами, текст ошибки. Поэтому в метке route стоит не путь /api/products/42, а шаблон /api/products/{id}: все десять тысяч товаров попадают в одну серию. Неизвестные пути вроде /wp-admin.php собраны под значением other, иначе бот, перебирающий случайные адреса, создал бы миллионы серий. Рост числа серий без границы называют взрывом кардинальности (cardinality explosion): память кончается, и мониторинг падает именно тогда, когда нужнее всего. Подробности про пользователя или заказ живут в логах, где есть user_id и request_id (урок 7.5).
Осторожно: одной меткой тоже не обойтись, по статусу график потом не разделишь. Нужны метки, по которым ты делишь график и у которых мало значений.
Главное: серий столько, сколько разных комбинаций меток, поэтому в метку кладут только значения с ограниченным набором, а идентификаторы оставляют логам.
Метки выбрали, осталось договориться об именах.
Имена и единицы
В запросе стоит число 0.25. Четверть секунды или 250 миллисекунд? Prometheus не знает, в чём измерено число, поэтому есть договорённость, которой следует «Магазин». Имена пишут через подчёркивание, с префиксом приложения для собственных метрик (shop_, payment_). Единицы всегда базовые: секунды и байты, не миллисекунды и мегабайты, чтобы числа разных сервисов можно было сравнивать без пересчёта, так что http_request_duration_seconds, а не _ms. В удобные единицы переводят в Grafana. Счётчики заканчиваются на _total, гистограммы на единицу измерения.
Главное: единицу измерения читай в имени метрики: Prometheus хранит только числа.
Осталось посмотреть, как настроен сам Prometheus.
Как настроен Prometheus на стенде
Prometheus настраивается одним YAML-файлом. Вот выдержка из ~/learning/load-tester/project/shop/monitoring/prometheus/prometheus.yml:
global:
scrape_interval: 5s
evaluation_interval: 5s
rule_files:
- /etc/prometheus/rules/alerts.yml
- /etc/prometheus/rules/slo.yml
alerting:
alertmanagers:
- static_configs:
- targets: ["alertmanager:9093"]
scrape_configs:
- job_name: shop
static_configs: [{targets: ["shop:8000"]}]
- job_name: payment
static_configs: [{targets: ["payment:8001"]}]
# ... node, cadvisor, postgres, prometheus, alertmanager, alloy
scrape_interval: 5s задаёт, как часто ходить за метриками (по умолчанию минута, боевые системы ставят 15–30 секунд, у стенда 5, чтобы графики быстро реагировали на тест). Остальное сверху (evaluation_interval, rule_files, alerting) относится к алертам (правилам «когда звать дежурного»): урок 7.6. Для сегодня важно scrape_configs: job_name: shop и targets: ["shop:8000"] значат «ходи на http://shop:8000/metrics» (путь /metrics подразумевается по умолчанию). shop здесь это имя контейнера в сети Docker Compose: внутри сети контейнеры видят друг друга по именам. Задания node, cadvisor и postgres подключают экспортеры: программы, которые превращают состояние хоста, контейнеров и PostgreSQL в метрики (урок 7.4). Ещё три цели, prometheus, alertmanager и alloy, отдают метрики самих инструментов.
Всего у стенда восемь целей: это число проверяет CI курса и его ты проверишь в практике. Помнишь полдня гаданий? Теперь каждая версия проверяется одним графиком.
Главное: стенд собирает метрики с восьми целей каждые 5 секунд, а список целей лежит в
scrape_configs.
Практика
Все файлы урока складывай в ~/perf-lab/07-monitoring/. Нагрузку даём только на свой стенд.
1. Включи профиль мониторинга
Стенд из темы 2 включал только сам «Магазин» с базой. Мониторинг оформлен как профиль (profile) Docker Compose: набор дополнительных сервисов, которые не стартуют без явной просьбы. Включаются они флагом --profile monitoring.
mkdir -p ~/perf-lab/07-monitoring
cd ~/learning/load-tester/project/shop
docker compose --profile monitoring up -d --wait --wait-timeout 300
Разбор команды:
docker composeэто управление набором контейнеров, описанных вcompose.yaml(урок 5.3);--profile monitoringдобавляет к стартующим сервисам те, что помеченыprofiles: [monitoring]: Prometheus, Alertmanager, Grafana, Loki, Tempo, Alloy и экспортеры;up -dзапускает в фоне,--waitждёт, пока у всех контейнеров пройдёт проверка здоровья (healthcheck),--wait-timeout 300не ждёт дольше пяти минут.
Одна оговорка про настройки самого Prometheus: в compose.yaml ему передан флаг --web.enable-remote-write-receiver. Он разрешает принимать метрики по запросу извне (remote write), а не только забирать их скрейпом. Флаг нужен в теме 10: k6 будет присылать свои метрики теста именно так. По умолчанию порт 9090 привязан к 127.0.0.1, то есть виден только на этой машине; на все интерфейсы он открывается при BIND_ADDR=0.0.0.0, и тогда в чужой сети не оставляй стенд включённым.
Первый запуск скачает образы и займёт несколько минут. Когда команда вернула управление, проверь:
docker compose --profile monitoring ps --format 'table {{.Service}}\t{{.Status}}'
SERVICE STATUS
alertmanager Up 40 seconds
alloy Up 40 seconds
cadvisor Up 40 seconds (healthy)
grafana Up 40 seconds
loki Up 40 seconds
node-exporter Up 40 seconds
payment Up 55 seconds (healthy)
postgres Up 55 seconds (healthy)
postgres-exporter Up 40 seconds
prometheus Up 40 seconds
redis Up 55 seconds (healthy)
shop Up 50 seconds (healthy)
tempo Up 40 seconds
Как читать вывод: в колонке STATUS ищи слова Up. У части сервисов нет (healthy): у них не задана проверка здоровья, это нормально. Если какого-то сервиса нет в списке или написано Restarting или Exited, открой логи: docker compose --profile monitoring logs prometheus.
Типичные ошибки:
port is already allocated(порт занят): на машине уже что-то слушает 9090 или 3000. Найди владельца:ss -ltnp | grep -E ':9090|:3000'и останови его.no configuration file provided: ты не в каталоге~/learning/load-tester/project/shop.- Prometheus запустился, но в цели
shopошибка: подожди 30 секунд, покаshopстанет здоровым.
2. Прочитай /metrics глазами
Сначала создай немного событий, чтобы метрики не были пустыми:
for i in $(seq 1 30); do curl -s -o /dev/null localhost:8000/api/products; done
curl -s -o /dev/null localhost:8000/api/products/42
curl -s -o /dev/null localhost:8000/api/products/999999
curl -s -o /dev/null localhost:8000/api/no-such-path
Разбор: цикл for i in $(seq 1 30) повторяет curl тридцать раз; -s убирает индикатор загрузки, -o /dev/null выбрасывает тело ответа, нам важен только факт запроса. Последние три команды создают успешный запрос, 404 на несуществующий товар и запрос на неизвестный путь (он попадёт в route="other").
curl -s localhost:8000/metrics | grep '^http_requests_total'
http_requests_total{method="GET",route="/api/products",status="200"} 30.0
http_requests_total{method="GET",route="/api/products/{id}",status="200"} 1.0
http_requests_total{method="GET",route="/api/products/{id}",status="404"} 1.0
http_requests_total{method="GET",route="other",status="404"} 1.0
Разбор: grep '^http_requests_total' оставляет строки, которые начинаются с имени метрики (^ означает «начало строки»), и отбрасывает # HELP и # TYPE.
Как читать вывод: посмотри на путь /api/products/42 и /api/products/999999: оба превратились в один и тот же route="/api/products/{id}", это шаблон, о котором шла речь в разделе про кардинальность. Единица в конце строки с status="404" это тот самый запрос на несуществующий товар. А запрос на /api/no-such-path ушёл в route="other". Если у тебя числа другие (ты запускал тесты раньше), главное, чтобы набор меток совпадал.
Теперь посмотри объявления типов:
curl -s localhost:8000/metrics | grep '^# TYPE'
# TYPE http_requests_total counter
# TYPE http_request_duration_seconds histogram
# TYPE http_requests_in_progress gauge
# TYPE shop_db_pool_size gauge
# TYPE shop_db_pool_available gauge
# TYPE shop_db_pool_waiting gauge
# TYPE shop_db_connection_wait_seconds histogram
# TYPE shop_cache_requests_total counter
# TYPE shop_orders_created_total counter
# TYPE shop_payment_requests_total counter
# TYPE shop_payment_duration_seconds histogram
Порядок строк у тебя может отличаться. У метрик с метками (shop_payment_requests_total, shop_cache_requests_total) строк TYPE может не быть, пока не случилось первое событие.
3. Найди цели в UI Prometheus
Открой в браузере http://localhost:9090/targets, или в меню Prometheus «Status → Target health». Ты увидишь таблицу, сгруппированную по job. Если окно показывает строку «State: UP» зелёным у всех восьми целей, всё хорошо.
То же самое в терминале, чтобы не зависеть от интерфейса:
curl -s localhost:9090/api/v1/targets \
| jq -r '.data.activeTargets[] | "\(.labels.job)\t\(.health)\t\(.scrapeUrl)"' | sort
Разбор: Prometheus отвечает на /api/v1/targets JSON-ом; jq -r вытаскивает из каждой активной цели название job, здоровье (up или down) и адрес скрейпа; sort упорядочивает по алфавиту.
alertmanager up http://alertmanager:9093/metrics
alloy up http://alloy:12345/metrics
cadvisor up http://cadvisor:8080/metrics
node up http://node-exporter:9100/metrics
payment up http://payment:8001/metrics
postgres up http://postgres-exporter:9187/metrics
prometheus up http://prometheus:9090/metrics
shop up http://shop:8000/metrics
Как читать вывод: восемь строк, все up. Адреса используют имена контейнеров, а не localhost, потому что Prometheus сам работает внутри сети Docker. Правило: «localhost» внутри контейнера это сам контейнер, а не твой ноутбук. Если цель down, смотри колонку «Error»: там будет причина.
4. Первые запросы в Prometheus
Открой http://localhost:9090/query. В строку запроса вводи по очереди выражения и нажимай Execute. Результат показывается во вкладке Table (значения «сейчас») или Graph (во времени).
up
Вернёт 8 серий: по одной на цель, у каждой метки instance и job, значение 1. Это «сердцебиение» мониторинга.
http_requests_total
Список серий счётчика, как в /metrics, только с добавленными метками instance="shop:8000" и job="shop".
http_requests_in_progress
Единственное число (серия без меток, кроме instance и job). Если генератора нагрузки сейчас нет, значение близко к нулю.
count({job="shop"})
Сколько всего серий у Prometheus от «Магазина». Это общее число серий твоей кардинальности. {job="shop"} без имени метрики выбирает все серии этого job, count(...) считает их.
count by (__name__) ({job="shop"})
Сколько серий у каждой метрики: __name__ это служебная метка, в которой хранится имя метрики. Должно быть видно, что у гистограммы http_request_duration_seconds_bucket рядов в разы больше, чем у остальных. Посмотри на график в таблице и сверь с графиком в теории выше.
Если на вкладке Graph ничего нет, поставь диапазон «Last 15m» и подожди минуту: сначала нужны хотя бы две точки.
Типичные ошибки:
parse error: unexpected ...: в выражении опечатка, например забыта закрывающая скобка или кавычка. Prometheus показывает позицию ошибки.Empty query resultдляhttp_requests_total: ни один запрос ещё не обработан. Выполни команды циклаcurlиз шага 2.- Для
shop_payment_requests_totalилиshop_cache_requests_totalпустой результат тоже нормален: событий с такими метками ещё не было.
Не понимаешь строку из
/metrics? Скопируй её вместе с соседними строками# HELPи# TYPEи спроси нейросеть, какого типа метрика и что значит каждая метка. Проверь ответ по строке# TYPEв самом выводе и по таблице типов в уроке.
5. Составь каталог метрик
Теперь собери то, что узнал, в файл. Заготовка ~/perf-lab/07-monitoring/metrics-catalog.md:
# Каталог метрик «Магазина»
| Метрика | Тип | Метки | Что значит | Единица |
|---|---|---|---|---|
| http_requests_total | counter | method, route, status | Обработано HTTP-запросов | штук |
| http_requests_in_progress | gauge | (нет) | Запросов в работе сейчас | штук |
| http_request_duration_seconds | histogram | method, route | Время обработки запроса | секунды |
| shop_db_pool_size | gauge | | Соединений в пуле БД | штук |
| shop_db_pool_available | gauge | | Свободных соединений | штук |
| shop_db_pool_waiting | gauge | | Запросов ждёт соединение | штук |
| shop_db_connection_wait_seconds | histogram | | | секунды |
| shop_cache_requests_total | counter | result (hit, miss) | | |
| shop_orders_created_total | counter | | | |
| shop_payment_requests_total | counter | result (ok, error, timeout) | | |
| shop_payment_duration_seconds | histogram | | | |
Первые строки заполнены, остальные допиши сам. Чтобы не гадать, как работает shop_db_connection_wait_seconds или shop_payment_duration_seconds, найди их в /metrics, посмотри # HELP и тип. В колонке «Что значит» формулируй своими словами, опираясь на HELP и на разбор в этом уроке. Дополнительно допиши в конец файла раздел:
## Что я проверил руками
- Количество целей Prometheus: 8, все up.
- `count({job="shop"})` вернул: ____ серий.
- Какая метрика занимает больше всего серий и почему: ____
Подставь реальные числа из своего стенда. Если ты уже прошёл тему 3, закоммить:
cd ~/perf-lab && git add 07-monitoring && git commit -m "7.1: каталог метрик Магазина" && git push
Сломай и почини
Поломка 1. Магазин пропал. Остановим сервис и посмотрим, как это увидит мониторинг.
cd ~/learning/load-tester/project/shop
docker compose stop shop
Подожди 10–15 секунд, потом в Prometheus выполни:
up{job="shop"}
Задача: без подсказок выясни, что показывает мониторинг, и найди причину в http://localhost:9090/targets.
Разбор
up{job="shop"}стал 0. Prometheus пришёл наshop:8000/metricsи не получил ответа. Это тот самый случай «молчание тоже сигнал».- На странице targets цель
shopв состоянииDOWN, в колонке ошибки текст видаGet "http://shop:8000/metrics": dial tcp ...: lookup shop ...: no such host. Для остановленного контейнера в сети compose это типичный текст: имяshopперестало разрешаться. Текст «connection refused» означал бы другое: контейнер жив, до адреса добрались, но на порту никто не слушает. - Остальные семь целей продолжают быть
UP: проблема только в «Магазине». curl localhost:8000/healthzзавершится ошибкойFailed to connect,docker compose psне покажетshop.
Почини:
docker compose start shop
Через 15–20 секунд (контейнер ждёт проверку здоровья) up{job="shop"} снова 1. Теперь посмотри на http_requests_total в Graph за последние 15 минут: там дырка на время остановки, а значения после перезапуска начались с нуля, потому что счётчики живут в памяти сервиса. Это живой пример сброса counter из теории. Запомни этот график, в следующем уроке ты увидишь, как rate() справляется с таким обрывом.
Поломка 2. Метрика не появилась. Проверь два запроса:
shop_orders_created_total
shop_payment_requests_total
Первая метрика без меток: она есть сразу, со значением 0. Вторая с меткой result: если заказов ещё не было, результат пустой. Оформи заказ и повтори запросы:
TOKEN=$(curl -s localhost:8000/api/login -H 'Content-Type: application/json' \
-d '{"email":"user0001@shop.lab","password":"password"}' | jq -r .token)
curl -s -X POST localhost:8000/api/cart/items -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"product_id":1,"qty":1}' > /dev/null
curl -s -X POST localhost:8000/api/orders -H "Authorization: Bearer $TOKEN" | jq '{id, status, total}'
Через пять-десять секунд (следующий скрейп) shop_orders_created_total вырастет с 0 до 1, а у shop_payment_requests_total появится серия result="ok". Вывод: серия с метками рождается при первом событии, и пустой результат не всегда означает поломку. Поэтому в алертах и на дашбордах, где нужны нули, потом используют приём or vector(0) (ты увидишь его в готовом дашборде в уроке 7.3).
ИИ в помощь
Нейросеть хорошо объясняет формат /metrics и помогает понять, чем счётчик отличается от гистограммы, но названий метрик твоего стенда она не знает. Общие правила: ИИ-помощник.
Задача: разобрать фрагмент вывода /metrics.
Я учу Prometheus. Вот кусок вывода http://localhost:8000/metrics:
<вставь 15-20 строк с http_request_duration_seconds_bucket, _sum, _count>.
Объясни по строкам: что такое le, почему бакеты накопительные, что значат _sum и _count,
и как из них получить среднее время запроса. Приведи числовой пример на моих значениях.
Проверь ответ: сверь с таблицей типов метрик из урока и пересчитай среднее сам: _sum / _count в Prometheus. Типичная ошибка: нейросеть выдумывает метрики, которых у «Магазина» нет (например, http_request_latency_ms). Сверяй имена с реальным /metrics и с каталогом, который ты ведёшь.
Задача: выбрать тип метрики для нового измерения.
Для сервиса интернет-магазина нужно измерять: <вставь, что именно: число заказов, время оплаты, размер корзины>.
Для каждого предложи тип метрики Prometheus (counter, gauge, histogram), имя в стиле
Prometheus и метки. Объясни, какой метки не стоит заводить из-за большого числа значений.
Проверь ответ: сравни с готовыми метриками стенда (shop_orders_created_total, shop_payment_duration_seconds). Типичная ошибка: метка с user_id или request_id, которая разрывает число рядов; в уроке это названо кардинальностью.
Словарик урока
| Термин | Простыми словами |
|---|---|
| Метрика (metric) | Число, которое сервис сообщает о себе и которое записывается во времени |
| Метка (label) | Пара «ключ=значение» у метрики, которая уточняет, о чём именно число (status="200") |
| Временной ряд, серия (time series) | Имя метрики с одним набором меток и список пар «время, значение» |
| Prometheus | Программа, которая собирает, хранит и отдаёт метрики |
| TSDB | База временных рядов внутри Prometheus: быстро дописывает и читает точки по времени |
/metrics |
HTTP-адрес сервиса, который отдаёт все его метрики текстом |
| Скрейп (scrape) | Один заход Prometheus на /metrics за числами |
| Pull / push | Pull: хранилище само забирает данные. Push: сервис сам отправляет их |
| Цель (target), job, instance | Адрес, с которого собирают метрики; задание-группа таких адресов; один конкретный адрес |
up |
Служебная метрика Prometheus: 1, если скрейп цели удался, 0, если нет |
| Counter | Счётчик, который только растёт; смотрят на скорость роста |
| Gauge | Значение, которое ходит вверх и вниз; смотрят на само значение |
| Histogram | Набор накопительных счётчиков по корзинам времени, из них считают перцентили |
Корзина, бакет (bucket), le |
Счётчик запросов не медленнее заданной границы; le значит «меньше или равно» |
| Summary | Метрика с готовыми перцентилями, посчитанными в сервисе; нельзя объединять |
| Кардинальность (cardinality) | Сколько разных серий у метрики; растёт перемножением значений меток |
| Взрыв кардинальности | Число серий растёт без границы из-за метки с неограниченными значениями |
| Пул соединений (connection pool) | Заранее открытые соединения с базой, которые выдаются запросам по очереди |
| Профиль Compose (profile) | Набор сервисов, которые стартуют только при явной просьбе |
Вопросы с собеседований
Раздел для повторения: ответь вслух, потом открой ответ. Короткие вопросы с пометкой [на скорость] тренируй на время: ответ за 30 секунд.
1. [junior] [часто] Что такое метрика и чем она отличается от лога?
Ответ
Метрика это число (с именем и метками), которое записывается во времени, например количество запросов в секунду. Лог это запись о конкретном событии с подробностями. Метрика хранит агрегат: её размер не растёт с числом запросов, зато нельзя разобрать судьбу одного запроса. Лог хранит каждое событие, поэтому растёт вместе с нагрузкой, зато даёт детали. Обычно метрика показывает, что что-то не так, а лог объясняет, почему.
Что хотят услышать: агрегат против событий, размер, роль каждого (обнаружить против разобрать).
Красный флаг: «метрика это просто лог в другом формате».
2. [junior] [часто] Чем pull-модель Prometheus отличается от push и в чём плюсы pull?
Ответ
При pull Prometheus сам регулярно приходит за числами на /metrics сервиса, при push сервис сам отправляет их. Плюсы pull: если сервис не отвечает, это видно сразу (up = 0), сервису не нужно знать адрес хранилища, легко проверить метрики руками через curl, нагрузкой на сбор управляет Prometheus. Минус: для коротких задач, которые успевают завершиться между скрейпами (batch-задания), pull не подходит, для них есть Pushgateway.
Что хотят услышать: up как сердцебиение, простота проверки, и минус для короткоживущих процессов.
Красный флаг: «Prometheus получает метрики, которые сервис ему присылает».
3. [junior] [часто] Назови четыре типа метрик Prometheus и приведи пример каждого.
Ответ
Counter: только растёт, например http_requests_total. Gauge: ходит вверх и вниз, например http_requests_in_progress или число свободных соединений в пуле. Histogram: счётчики по корзинам, например длительность запросов http_request_duration_seconds, из него считают перцентили. Summary: готовые перцентили, посчитанные в самом сервисе, объединять между экземплярами нельзя.
Что хотят услышать: все четыре и правильные примеры, плюс как с каждым работают (rate для counter, значение для gauge, histogram_quantile для histogram).
Красный флаг: перепутать counter и gauge («счётчик запросов это gauge»).
4. [junior] Почему нельзя просто нарисовать график counter как есть?
Ответ
Counter накапливает итог с момента запуска и поэтому только растёт: график это лестница вверх, из которой не видно ни нагрузки, ни пика. Полезна скорость роста: сколько запросов добавилось за секунду. Её даёт функция rate(). Ещё одна причина: при перезапуске сервиса counter обнуляется, и «сырой» график падает в ноль, а rate() умеет такие сбросы учитывать.
Что хотят услышать: скорость вместо накопленного итога, упоминание сброса.
Красный флаг: «график растёт, значит нагрузка растёт».
5. [junior] Что такое job и instance в Prometheus?
Ответ
job это название группы однотипных целей, например все экземпляры «Магазина». instance это адрес конкретной цели внутри группы, например shop:8000. Prometheus сам добавляет обе метки к каждой серии, поэтому можно писать up{job="shop"} или смотреть, какой именно экземпляр отвечает медленнее.
Что хотят услышать: группа против конкретного экземпляра, что метки добавляются автоматически.
Красный флаг: «это названия, которые указал разработчик в коде сервиса».
6. [junior] Что делает метка le у histogram и почему корзины накопительные?
Ответ
le значит «меньше или равно» и задаёт верхнюю границу корзины: le="0.1" считает запросы, ответившие за 100 мс или быстрее. Корзины накопительные, то есть каждая включает все предыдущие, а le="+Inf" равна общему числу запросов. Так устроено, чтобы можно было рассчитать, какая доля запросов уложилась в любую границу, простым делением двух счётчиков, и чтобы по ним считать перцентили.
Что хотят услышать: «меньше или равно», накопительность, +Inf равно _count.
Красный флаг: «в корзине те запросы, что попали именно в этот диапазон».
7. [middle] Что такое кардинальность и почему нельзя класть user_id в метку?
Ответ
Кардинальность это число разных серий метрики, оно растёт перемножением значений всех меток. Метка user_id имеет столько значений, сколько пользователей: для миллиона пользователей и 10 маршрутов получится 10 миллионов серий, у гистограммы ещё в 15 раз больше. Память и диск Prometheus кончатся, запросы станут медленными, мониторинг упадёт в самый нужный момент. Идентификаторы конкретных запросов и пользователей принадлежат логам. В метках должны быть значения с ограниченным набором: метод, шаблон маршрута, код ответа.
Что хотят услышать: перемножение, расчёт на примере, куда вместо этого девать детали, понятие «взрыв кардинальности».
Красный флаг: «метки ничего не стоят, их можно добавлять сколько угодно».
8. [middle] Почему в метке route у «Магазина» шаблон /api/products/{id}, а не реальный путь?
Ответ
Реальных путей десять тысяч (по одному на товар), каждый создал бы отдельную серию, и для гистограммы ещё в 15 раз больше. Шаблон объединяет все товары в одну серию и сохраняет полезную информацию: какой тип запроса. Неизвестные пути объединяются в other, чтобы случайные или вредоносные запросы не создавали бесконечное число серий.
Что хотят услышать: ограничение числа серий, принцип «шаблон вместо значения», защита от мусорных путей.
Красный флаг: «так проще читать».
9. [middle] Prometheus видит gauge http_requests_in_progress раз в 15 секунд. Что можно пропустить?
Ответ
Любой всплеск короче интервала скрейпа, который не совпал с моментом замера: например, пять секунд, когда в работе было 50 запросов, при интервале 15 с могут не попасть ни в один замер. Counter такой проблемы не имеет: разница двух замеров покажет все события между ними. Поэтому для событий выбирают counter и считают rate, а для текущих уровней смотрят на gauge с запасом по интервалу, плюс подкрепляют гистограммами и счётчиками.
Что хотят услышать: снимки против накопления, практический вывод про выбор типа.
Красный флаг: «Prometheus видит всё, что происходит в сервисе».
10. [middle] Метрика http_requests_total в Prometheus пустая, хотя сервис работает. Какие причины?
Ответ
Идти надо по цепочке. 1) Цель down: посмотреть up и страницу targets, прочитать текст ошибки. 2) Prometheus собирает не тот адрес или порт: сверить prometheus.yml. 3) Метрика не объявлена в этом процессе или ещё не было событий, значит серия не создана: сделать запрос и проверить через curl на /metrics. 4) Запрос ищет не то имя или метку: проверить имя в /metrics. 5) Пусто не сам селектор, а rate(...): окно слишком маленькое, внутри него нужны хотя бы две точки.
Что хотят услышать: последовательность от up к /metrics, знание, что серия появляется после первого события.
Красный флаг: «перезапущу Prometheus и посмотрю».
11. [middle] Как из histogram получить среднее время запроса, и почему этого недостаточно?
Ответ
Среднее равно _sum / _count (суммарное время делить на число запросов), в PromQL rate(..._sum[5m]) / rate(..._count[5m]). Но среднее прячет хвост: несколько очень медленных запросов почти не меняют его, а пользователи страдают. Поэтому по корзинам считают перцентили p95 и p99 функцией histogram_quantile.
Что хотят услышать: формулу через _sum и _count, причину перехода к перцентилям.
Красный флаг: «среднего достаточно, оно показывает типичное время».
12. [junior] [на скорость] Назови три вида данных наблюдаемости.
Ответ
Метрики (числа во времени), логи (записи о событиях), трассы (путь запроса через сервисы). Метрики показывают, что и сколько, логи объясняют конкретное событие, трассы показывают, где потерялось время.
13. [junior] [на скорость] Что значит up == 0?
Ответ
Последний скрейп цели не удался: сервис недоступен, ответил ошибкой или не уложился в таймаут. Prometheus сам записывает up для каждой цели, 1 если всё хорошо.
14. [junior] [на скорость] Какой тип у «число свободных соединений в пуле»?
Ответ
Gauge: значение ходит вверх и вниз. Counter не подходит, он только растёт.
15. [middle] [на скорость] Что произойдёт, если положить request_id в метку?
Ответ
Каждый запрос создаст новую серию. Число серий вырастет без границы, память Prometheus кончится. Идентификаторы запросов хранят в логах, не в метках.
Проверено на версиях
Ubuntu 24.04, Docker Engine с Compose v2, стенд «Магазин» из project/shop (Python 3.14, prometheus_client в режиме multiprocess), Prometheus 3.15, Grafana 13.2. Октябрь 2026.
Итог урока: ты умеешь
- Объяснить, чем метрики отличаются от логов и трасс и чем pull-модель отличается от push.
- Включить профиль мониторинга и убедиться, что все восемь целей Prometheus живые.
- Прочитать вывод
/metrics: имя, метки, значение,# TYPE, корзиныle. - Назвать тип любой метрики «Магазина» и объяснить, почему он именно такой.
- Посчитать среднее и долю запросов по корзинам гистограммы руками.
- Объяснить, что такое кардинальность и почему
user_idнельзя класть в метку. - Найти упавшую цель по
upи странице targets и понять причину по тексту ошибки. - Вести каталог метрик в
~/perf-lab/07-monitoring/metrics-catalog.md.
Дальше: урок 7.2. PromQL: rate, sum by, histogram_quantile, где ты научишься задавать Prometheus вопросы: сколько запросов в секунду, какая доля ошибок и чему равен p95.
Глубже: устройство Prometheus и форматы метрик подробнее в курсе DevOps, если захочется заглянуть дальше учебного стенда.
Проверь себя
Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.
Тест работает с включённым JavaScript.