✻ Урок 8.2 · Тема 8: Наблюдаемость
Prometheus: сбор метрик и /metrics в «Заметках»
Содержание урока
Зачем это нужно
В 8.1 ты решил, что считать надёжностью: SLI и SLO. Чтобы их посчитать, нужны числа: сколько запросов пришло, сколько закончилось ошибкой, сколько они длились, сколько памяти съел контейнер. Prometheus это программа, которая собирает эти числа, хранит их вместе со временем и отвечает на вопросы про прошлое. Представь тетрадь, куда каждые 15 секунд записывают показания приборов: по ней потом можно спросить «что было вчера в три часа ночи». Без такой тетради остаётся помнить по ощущениям.
Ночью «сервис тормозит» без метрик означает угадывание по логам. С метриками видно: ошибки выросли в 02:14, память растёт с релиза, диск закончится через шесть часов. Prometheus есть почти в каждом Kubernetes-кластере (Kubernetes это система, которая запускает и следит за множеством контейнеров на нескольких машинах; ты встретишь её в теме 5), на собеседованиях по нему спрашивают всегда.
Шаг проекта: «Заметки» получают адрес GET /metrics (app.py v5, образ 0.5.0), появляется каталог monitoring/ со стеком Prometheus, node_exporter и cAdvisor, и Prometheus начинает собирать метрики приложения, хоста и контейнеров. Стек это набор программ, которые работают вместе. node_exporter это маленькая программа-«переводчик», которая читает состояние самой машины (процессор, память, диск) и выкладывает его страницей, понятной Prometheus, как датчик на щитке, к которому можно подключить прибор. cAdvisor делает то же для контейнеров: сколько памяти и процессора ест каждый. Без них Prometheus знал бы только то, что приложение само о себе рассказало, и ничего о машине под ним. Подробно разберём в теории.
Что нужно знать
- Урок 8.1: наблюдаемость, SLI и SLO: три сигнала (метрики, логи, трейсы) и какие числа нам нужны. Метрика это число, которое меняется со временем.
- Урок 4.5: Compose с PostgreSQL: Compose это файл
compose.yml, который описывает несколько контейнеров, их тома (постоянные диски) и общую сеть. - Урок 4.6: nginx и TLS перед «Заметками»: сеть
notes-netи запросы черезhttps://notes.lab. - Урок 4.7: образы и теги: сборка образа с семантическим тегом (
0.5.0, а неlatest). - Урок 2.4: HTTP: коды ответов,
curl, демонстрационные пути/errorи/slow. - Урок 1.4: процессы и сигналы: что происходит с процессом при остановке и почему всё, что было у него в памяти, пропадает.
- Если хочешь разобрать основы на другом стенде, см. Prometheus в курсе «Мониторинг и SRE».
Картина целиком
Представь учителя, который каждые 15 секунд обходит класс с журналом. Ученики (наши сервисы) ничего не рассылают сами: у каждого на парте лежит листок с текущими цифрами («решено 12 задач, 3 с ошибкой»). Учитель подходит, списывает цифры в журнал вместе с временем и идёт дальше. Если ученик не отвечает, учитель ставит отметку «нет на месте». По журналу потом видно, как менялись цифры в течение урока.
Учитель это Prometheus, листок на парте это страница /metrics (обычный адрес в сети, по которому программа отдаёт текст с текущими цифрами), ученики это цели (targets), а журнал это база временных рядов (time series database, TSDB): хранилище, которое устроено под записи вида «в такое-то время было такое-то число». Обычная таблица с этим справляется хуже, а журнал по датам как раз то, что нужно.
flowchart TD
P["Prometheus<br>раз в 15 с обходит цели"] -->|"GET /metrics"| N["notes:8080<br>приложение"]
P -->|"GET /metrics"| NE["node_exporter :9100<br>хост"]
P -->|"GET /metrics"| CA["cadvisor :8080<br>контейнеры"]
N -->|текст с числами| P
NE -->|текст с числами| P
CA -->|текст с числами| P
P --> DB[("TSDB<br>числа с меткой времени")]
DB --> U["Ты: браузер, curl, Grafana"]
Стрелки к целям читай как «Prometheus приходит и забирает». Цели не рассылают ничего сами, а результат обхода попадает в базу и оттуда к тебе. Про каждый обход Prometheus сам записывает метрику up: 1 если цель ответила, 0 если нет.
Не у каждой программы есть /metrics. Для базы данных, операционной системы и контейнеров существуют небольшие программы-«переводчики» (exporters, экспортёры): они читают состояние чужой системы и выкладывают его в том же формате. Сегодня ты подключишь два (node_exporter для хоста, cAdvisor для контейнеров) и научишь свои «Заметки» отдавать /metrics.
Теория
Pull-модель: Prometheus сам ходит за метриками
Зачем это устроено именно так. Есть два способа передать числа мониторингу. Push (от слова «толкать»): приложение само отправляет цифры на сервер мониторинга. Pull («тянуть»): сервер мониторинга сам приходит и забирает. В Prometheus выбран pull. Причина в том, что при push мониторинг не отличает «приложение молчит, потому что всё хорошо» от «приложение молчит, потому что умерло». При pull это различие получается само собой: пришёл за цифрами, никто не ответил, значит проблема.
Учитель с журналом из «Картины целиком». Если бы ученики сами приходили к учителю со своими цифрами, то отсутствие ученика никак бы не бросалось в глаза. Когда учитель обходит парты, пустое место видно сразу. Аналогия ломается для очень коротких дел: ученик, который успел решить задачу и уйти между двумя обходами, останется незамеченным (об этом ниже, в разделе про Pushgateway).
- Раз в
scrape_interval(интервал сбора, у нас 15 секунд) Prometheus берёт список целей из конфига. - К каждой цели он делает обычный HTTP-запрос
GET /metrics(то же, что делаетcurlв уроке 2.4). Сбор одной цели называется «scrape» (от англ. соскрести). - В ответе приходит текст с числами. Prometheus его разбирает.
- Каждое число он записывает в свою базу вместе с текущим временем.
- Отдельно он записывает в базу метрику
upпро саму цель: 1, если запрос удался и текст разобрался, 0 в остальных случаях (таймаут, отказ соединения, код ответа не 200, битый текст).
Три слова, которые путают чаще всего:
- цель (target): один адрес, например
notes:8080; - задача (job): группа однотипных целей, например все копии сервиса
notes; - экземпляр (instance): конкретный адрес внутри задачи.
Prometheus сам добавляет к каждому числу метки job и instance, чтобы потом было видно, откуда оно.
Разберём на примере. Допустим, в 12:00:00 Prometheus запросил notes:8080/metrics и получил ответ 200. В базу лягут два числа с меткой времени 12:00:00: up{job="notes",instance="notes:8080"} = 1 и все метрики приложения. В 12:00:15 сервис завис и не ответил за 10 секунд (это scrape_timeout, по умолчанию 10 секунд). В базу ляжет только up{...} = 0. Больше от приложения не придёт ничего, и это видно на графике up сразу, приложение не пришлось учить «слать пульс».
Смотри на разницу между накопленным счётчиком и текущим уровнем: между двумя обходами уровень слепой, а счётчик ничего не теряет.
Прикинь сам: сервис перестал отвечать. Что в Prometheus покажет это быстрее всего и почему это не надо программировать в самом сервисе?
Метрика up{job="notes"} станет 0. Её создаёт сам Prometheus по итогам каждого сбора, приложению ничего дописывать не нужно. Если приложение зависло, оно просто не ответит на запрос, и это тоже даст up = 0.
Осторожно, тут часто путают. Если цель исчезла, ряд не «застывает» на последнем значении навсегда. Когда цель исчезла или сбор не удался, Prometheus сразу помечает её ряды устаревшими (staleness), и запрос перестаёт их возвращать. Если маркера нет (ряд пришёл через push или федерацию), запрос ещё до 5 минут отдаёт последнюю точку: это окно lookback delta. Отсюда следствие для алертов: условие «метрика больше порога» не сработает, если метрики совсем нет, потому что сравнивать нечего. Поэтому рядом с любым алертом по значению нужен алерт на up == 0 (разберём в 8.5).
Главное: Prometheus сам обходит цели, а недоступная цель записывается как
up = 0без участия приложения.
Что именно он забирает с каждой цели?
Формат exposition: как выглядит /metrics
Prometheus и приложение должны договориться, как записывать числа. Договорились о простом тексте, который читает и человек: его можно открыть через curl и увидеть глазами. Это главный инструмент отладки: когда что-то не так, ты первым делом смотришь, что реально отдаёт /metrics.
Листок на парте: заголовок («Решено задач»), подсказка («число, всегда растёт») и сама цифра.
Ответ состоит из строк трёх видов:
# HELP demo_requests_total Сколько запросов обработано <- описание (для людей)
# TYPE demo_requests_total counter <- тип метрики
demo_requests_total{status="200"} 3.0 <- имя{метки} значение
demo_requests_total{status="500"} 1.0
# HELPи# TYPEначинаются с решётки и отвечают на вопросы «что это» и «какого типа».- Строка с числом состоит из имени (
demo_requests_total), необязательных меток в фигурных скобках (парыимя="значение"через запятую) и значения (число). - Одно имя с разными наборами меток это разные временные ряды (time series):
{status="200"}и{status="500"}считаются отдельно. Временной ряд это последовательность пар «момент времени, число» для одного имени и одного набора меток.
Разберём на примере. Строка demo_requests_total{status="500"} 1.0 говорит: «счётчик demo_requests_total для запросов со статусом 500 сейчас равен 1». Prometheus запишет это число в ряд и приложит время. Через 15 секунд запишет следующее значение (например, 4.0), и по двум точкам можно будет посчитать скорость роста: 3 запроса за 15 секунд.
Осторожно, тут часто путают. Что в /metrics лежит история. Нет: там только текущее состояние. Историю строит Prometheus, записывая эти текущие числа раз в 15 секунд. Поэтому если Prometheus не работал час, часа данных не будет, даже когда приложение всё это время исправно считало.
Главное:
/metricsэто обычный текст с текущими значениями, а историю строит Prometheus.
Проверь понимание: в
/metricsдве строки:notes_http_requests_total{path="/notes",status="200"} 10иnotes_http_requests_total{path="/notes",status="500"} 2. Сколько тут временных рядов?
Ответ
Два: имя одно, но наборы меток разные (status="200" и status="500"), а ряд определяется именем и всеми метками вместе.
У чисел бывают разные типы. Какие?
Четыре типа метрик
«Число» бывает разным по смыслу. Число запросов только растёт. Число заметок сейчас может и уменьшиться. Время ответа это не одно число, а целое распределение. Prometheus хочет знать тип, чтобы правильно с ним работать.
Три вида приборов в машине. Счётчик (counter) это одометр: только вперёд. Датчик (gauge) это спидометр или уровень топлива: вверх и вниз, важно текущее значение. Гистограмма (histogram) это таблица «сколько поездок заняли до 10 минут, до 30, до часа»: она не даёт точного времени каждой поездки, но показывает распределение.
| Тип | Что это | Пример |
|---|---|---|
| counter (счётчик) | только растёт; при перезапуске процесса сбрасывается в 0 | notes_http_requests_total |
| gauge (датчик) | значение идёт вверх и вниз | notes_notes_total, память процесса |
| histogram (гистограмма) | считает наблюдения по корзинам (bucket) с верхней границей le |
notes_http_request_duration_seconds |
| summary (сводка) | квантили (p95 и подобные, см. 8.1) считаются в самом приложении; их нельзя складывать между копиями сервиса | редко, в новом коде обычно histogram |
По счётчику интересно не само число, а скорость роста. «Всего запросов 1 000 000» ничего не говорит: это за день или за год? «Сейчас 12 запросов в секунду» говорит. Скорость считает функция rate(), она в уроке 8.3. Ей же не страшен сброс счётчика после перезапуска процесса: она замечает, что число упало, и понимает, что это начало нового отсчёта, а не «-1000 запросов».
Гистограмма подробно. Она нужна, чтобы считать задержки (p95 из 8.1). Ты заранее выбираешь границы корзин, например 0.1, 0.5 и 1 секунда. Каждый ответ приложения «кладётся» в корзины. Гистограмма даёт три вида рядов:
имя_bucket{le="0.5"}: сколько наблюдений были не больше 0.5 секунды (leзначит «less or equal», меньше или равно);имя_sum: сумма всех значений;имя_count: сколько было наблюдений.
Корзины кумулятивные: в le="0.5" входит всё, что уже попало в le="0.1". Последняя корзина le="+Inf" (бесконечность) вмещает вообще всё и потому равна _count.
Разберём на примере. Мы записали четыре времени ответа: 0.05, 0.3, 0.3 и 2.0 секунды, границы корзин 0.1, 0.5 и 1.
| Значение | Попало в le="0.1" |
le="0.5" |
le="1" |
le="+Inf" |
|---|---|---|---|---|
| 0.05 | да | да | да | да |
| 0.3 | нет | да | да | да |
| 0.3 | нет | да | да | да |
| 2.0 | нет | нет | нет | да |
| Итого | 1 | 3 | 3 | 4 |
_count = 4, _sum = 0.05 + 0.3 + 0.3 + 2.0 = 2.65. Среднее время = _sum / _count = 2.65 / 4 = 0.6625 секунды. Ты увидишь ровно эти числа в задании 1.
Столбики только растут слева направо: так устроены корзины. Разность соседних столбиков и есть число запросов в своём диапазоне.
Прикинь сам: в гистограмме
_bucket{le="0.5"} 30,_bucket{le="1"} 30,_count 34. Что можно сказать о четырёх запросах?
30 запросов уложились в 0.5 секунды, а четыре оказались медленнее секунды (34 минус 30, они попали только в корзину +Inf). Корзины кумулятивные, поэтому le="1" не отличается от le="0.5": между 0.5 и 1 секундой запросов не было.
Осторожно, тут часто путают. Counter и gauge. Правило простое: если число может уменьшиться при нормальной работе (заметок стало меньше, память освободилась), это gauge. Если оно только накапливается («всего случилось»), это counter. Ещё путают гистограмму с готовыми квантилями: гистограмма хранит корзины, а p95 из них оценивают позже. Из-за этого корзины надо выбирать под свои задержки: если первая корзина 100 мс, а все ответы по 20 мс, то p95 окажется «где-то внутри первой корзины», то есть оценка будет грубой.
Главное: counter только растёт, gauge ходит вверх и вниз, histogram считает наблюдения по накопительным корзинам.
Метки делят метрику на ряды. Сколько их допустимо?
Метки и кардинальность
Одной цифры «запросов всего 1000» мало: нужно уметь разрезать по путям, методам и статусам. Метки (labels) как раз делят одну метрику на группы.
Складской учёт. «На складе 500 коробок» скучно. «Красных 200, синих 300» уже полезнее. «Красных маленьких 50, красных больших 150…» ещё подробнее, но и строк в таблице больше. Каждая новая колонка умножает число строк.
Каждая уникальная комбинация значений всех меток это отдельный ряд в памяти и на диске. Число рядов называется кардинальностью (cardinality, «мощность множества»). Если метка принимает 5 значений, а вторая 3, а третья 4, то рядов до 5 * 3 * 4 = 60. Это безопасно. Если положить в метку что-то неограниченное (номер пользователя, номер заметки, сырой URL), рядов станет миллионы, память Prometheus раздуется, и он упадёт по OOM (нехватка памяти, процесс убивает система, урок 1.5).
Разберём на примере. В «Заметках» метрика notes_http_requests_total имеет метки method (GET, POST…), path (11 известных маршрутов плюс other) и status (несколько кодов). Рядов не больше нескольких сотен. Теперь представь, что вместо path="/notes" в метку попадает сырой адрес запроса: /notes/17, /notes/18… Каждая заметка добавит новые ряды для каждой пары method и status. После 10 000 заметок это десятки тысяч рядов, а если кто-то просканирует сайт случайными URL, то миллионы. Поэтому в приложении есть функция route_label: всё, что не входит в список известных маршрутов, превращается в other.
Осторожно, тут часто путают. Что подробности всегда хорошо. Для метрик подробности вредны, для логов (8.7) и трейсов (8.8) полезны. Метки метрик должны быть скучными: короткий конечный список значений.
Главное: каждая комбинация меток это отдельный ряд, поэтому в метках держат короткий конечный список значений.
Проверь понимание: разработчик хочет добавить в
notes_http_requests_totalметкуnote_id, чтобы видеть популярные заметки. Что ответишь?
Ответ
Нельзя: значений столько же, сколько заметок, и каждое даёт новый ряд для каждой комбинации method/status. Это взрыв кардинальности. Популярные заметки смотрят в логах или трейсах, а метрика остаётся с ограниченным набором меток.
Где всё это хранится?
Хранение: TSDB, retention, том
Числа надо где-то держать, и быстро находить по имени, меткам и времени. Обычная база данных с таблицами для этого плохо подходит, поэтому в Prometheus своя база временных рядов (TSDB, time series database).
Дневник погоды: запись по дням, старые страницы через какое-то время выбрасывают. Искать удобно по дате.
Prometheus пишет данные в каталог, который задаёт флаг --storage.tsdb.path. Сначала свежие числа лежат в памяти (и в журнале на диске на случай перезапуска), потом порциями по два часа сбрасываются в блоки на диске. Блоки старше срока хранения удаляются. Срок хранения (retention) задаёт --storage.tsdb.retention.time, по умолчанию 15 дней. Каталог мы выносим в том Docker (prom-data), иначе при удалении контейнера пропадёт вся история.
Разберём на примере. У нас 15 секунд между сборами. В сутках 86 400 секунд, значит один ряд получает 86 400 / 15 = 5 760 значений в сутки. Если рядов 2 000, то за сутки записей 11 520 000. Prometheus сжимает их до 1-2 байт на значение, так что сутки занимают порядка 20 МБ. Поэтому начинают с одного Prometheus с разумным retention.
Прикинь сам: почему каталог с данными Prometheus кладём в том, а не оставляем внутри контейнера?
Файловая система контейнера исчезает вместе с контейнером (урок 4.1). При пересоздании контейнера без тома пропала бы вся история метрик. Том живёт отдельно и переживает пересоздание.
Осторожно, тут часто путают. Что Prometheus это долговременное хранилище и кластер. Нет: данные лежат на одном узле. Для долгого хранения и нескольких кластеров есть Thanos, Mimir и VictoriaMetrics, но это надстройки, до которых надо дорасти.
Главное: данные лежат в TSDB на одном узле, срок хранения 15 дней, а каталог выносят в том.
Не всё умеет отдавать /metrics само. Кто помогает?
Экспортёры, Pushgateway и откуда берутся цели
Не всё умеет отдавать /metrics само: у Linux, PostgreSQL, nginx его нет. И не всё живёт достаточно долго, чтобы его успели опросить.
Переводчик: человек говорит на языке, который учитель не понимает, и переводчик пересказывает его слова на языке журнала.
Экспортёр (exporter) это небольшая программа, которая читает состояние чужой системы и выдаёт его в формате /metrics. Мы подключаем два:
- node_exporter (порт 9100) отдаёт метрики хоста: процессор, память, диски, сеть. Он читает их из каталогов
/procи/sys(урок 1.5). - cAdvisor (порт 8080 внутри сети, у нас проброшен как 8081) отдаёт метрики контейнеров: сколько памяти и процессора съел каждый.
Остальные экспортёры (PostgreSQL, nginx, проверки снаружи через blackbox) разберём в 8.4.
Pushgateway (шлюз для «пуша») нужен для задач, которые живут секунды: CronJob, скрипт бэкапа. Такая задача в конце работы отправляет итог в Pushgateway, а Prometheus забирает его оттуда как с обычной цели. Это исключение, а не основной путь.
Как Prometheus узнаёт, куда ходить. У нас список статический: адреса записаны в prometheus.yml. В Kubernetes цели находятся автоматически через service discovery (обнаружение служб), разберём в 8.9.
Разберём на примере. Скрипт бэкапа работает 40 секунд раз в час. Prometheus ходит раз в 15 секунд, но 59 минут из 60 процесса нет. Скрипт в конце вызывает Pushgateway и записывает gauge с временем успеха (например, backup_last_success_timestamp_seconds 1790000000). Шлюз живёт всегда, Prometheus снимает эту цифру каждые 15 секунд. Алерт строят на возрасте: «сейчас минус время последнего успеха больше 25 часов».
Осторожно, тут часто путают. Что Pushgateway это «push вместо pull для всего». Нет, и у него подводный камень: он хранит последнее значение вечно, даже если задача давно не запускалась, а up показывает живость шлюза, а не задачи.
Главное: экспортёр переводит чужую систему в формат Prometheus, а Pushgateway нужен только для коротких задач.
Проверь понимание: скрипт бэкапа работает 40 секунд раз в час. Как получить по нему метрику «время последнего успешного бэкапа»?
Ответ
Скрипт в конце отправляет gauge (например, Unix-время успеха) в Pushgateway, а Prometheus собирает Pushgateway как обычную цель. Прямой pull бесполезен: 15 секунд опроса почти всегда попадут в момент, когда процесса нет.
Как понять, что цель вообще видна?
Состояния цели: как читать страницу Targets
Первый вопрос при любой проблеме с метриками: «Prometheus вообще видит эту цель?». Ответ даёт страница /targets в его веб-интерфейсе. Без умения её читать ты будешь гадать, почему график пустой.
Табло вылетов в аэропорту. Рядом с каждым рейсом статус и, если что-то не так, причина: «задержан, погода». Статус отвечает «что», причина отвечает «почему».
У каждой цели два главных поля. Состояние (health): up, если последний сбор удался, и down, если нет. Последняя ошибка (lastError): текст, который Prometheus получил при неудаче. Состояние говорит, что плохо, а текст ошибки почти всегда сразу указывает, где искать.
Что написано в lastError |
Что это значит | Что проверить |
|---|---|---|
connection refused |
по адресу есть машина, но порт никто не слушает | запущено ли приложение, верный ли порт |
no such host |
имя не превратилось в адрес | есть ли контейнер в той же сети, нет ли опечатки |
context deadline exceeded |
цель не успела ответить за scrape_timeout |
не завис ли процесс, не слишком ли тяжёлая страница |
server returned HTTP status 404 |
сервер ответил, но такого пути нет | верен ли metrics_path, есть ли в приложении /metrics |
Разберём на примере. Представь строку «notes down, Get "http://notes:8081/metrics": dial tcp 172.18.0.4:8081: connect: connection refused». Читаем слева направо. Get и адрес: Prometheus пытался сделать обычный запрос. notes:8081: порт 8081, хотя приложение слушает 8080. 172.18.0.4 это адрес контейнера: имя notes найдено успешно, значит проблема не в имени. connection refused: на этом адресе порт закрыт. Вывод: в конфиге опечатка в порту. Подсказка уже в тексте.
Прикинь сам: цель
down, ошибкаserver returned HTTP status 404 Not Found. Приложение упало?
Нет. Сервер отвечает, иначе была бы connection refused или таймаут. Но по пути /metrics ничего нет: либо в приложении ещё не добавили этот адрес, либо в конфиге Prometheus указан другой metrics_path.
Осторожно, тут часто путают. Что down значит «приложение упало». Нет: оно может быть живо и обслуживать пользователей, но /metrics недоступен (404, закрыт порт, блокирует firewall). Поэтому после down сначала читай текст ошибки, а потом решай, куда идти.
Главное:
downне значит «приложение упало»: смотри текст ошибки и решай по нему.
Давай разберёмся со словами «метрика», «ряд» и «сэмпл».
Сэмпл, ряд и метрика: три слова про одни данные
В документации и в разговорах эти слова используют вперемешку, и новичок думает, что речь о разных вещах. Разберись один раз, и дальше текст читается легче.
Журнал температуры. Вся тетрадь по одному пациенту это ряд. Отдельная запись «12:00, 36.6» это сэмпл. Название показателя («температура») это метрика, а пациент это метки: у разных пациентов свои тетради для одного и того же показателя.
Метрика (metric) это имя показателя: notes_http_requests_total. Ряд (series) это метрика вместе с конкретными метками: notes_http_requests_total{status="500"}. Сэмпл (sample, «проба») это одна точка ряда: пара «момент времени, число». Раз в 15 секунд Prometheus добавляет в каждый ряд по одному сэмплу.
Разберём на примере. Метрика notes_http_requests_total с метками status (200 и 500) даёт два ряда. За минуту в каждом ряду накопится четыре сэмпла (15 секунд между ними), всего восемь. Поэтому scrape_samples_scraped из следующего раздела считает именно ряды, которые вернула цель за один сбор: по одному новому сэмплу на каждый.
Осторожно, тут часто путают. Сэмпл и запрос пользователя. Они не связаны: один сэмпл мог появиться из миллиона запросов, потому что это всего лишь число «на сейчас».
Главное: метрика это имя, ряд это имя с метками, сэмпл это одна точка ряда.
Проверь понимание: метрика с двумя метками:
method(3 значения) иstatus(4 значения). Сколько сэмплов добавится за один сбор, если встречались все комбинации?
Ответ
3 * 4 = 12 рядов, в каждый добавляется по одному сэмплу, значит 12 сэмплов за сбор.
А как часто делать обход?
Интервал сбора: почему 15 секунд
Интервал нельзя выбрать наугад: от него зависит, насколько быстро ты заметишь поломку, сколько места займёт база и насколько нагрузишь сами цели.
Как часто мерить температуру больному. Раз в сутки: пропустишь скачок. Каждую секунду: медсестра не успевает делать ничего другого, а записей тонны. Раз в 15 минут подходит для серьёзного случая. Для сервисов такой «серьёзный» ритм это 10-60 секунд.
Чем меньше интервал, тем точнее график и тем быстрее срабатывает алерт, но тем больше сэмплов, места и нагрузки на цель. Второе число, scrape_timeout, это сколько Prometheus ждёт ответа. Оно обязано быть не больше интервала (иначе новый сбор начнётся, пока старый не закончился). У нас 15 секунд и 10 секунд таймаут.
Разберём на примере. Если поломка случилась сразу после сбора, Prometheus заметит её только на следующем: в худшем случае через 15 секунд. К этому прибавляется время условия алерта (например, «5 минут подряд», см. 8.5). Интервал в минуту сделает реакцию в худшем случае вдвое-вчетверо медленнее и сократит объём данных в четыре раза, а для краткого всплеска в 20 секунд он вообще сделает картину неточной: всплеск может не попасть ни в один сбор.
Прикинь сам: всплеск ошибок длился 10 секунд, интервал сбора 60 секунд. Гарантирует ли Prometheus, что он его увидит?
Нет. Всплеск мог целиком оказаться между двумя сборами. Счётчики ошибок в этом случае всё равно покажут разницу между сборами (они накопительные), но точную форму всплеска ты не восстановишь: видно будет «ошибок стало больше», а не «когда и как резко».
Осторожно, тут часто путают. Что интервал «чем меньше, тем лучше». Мелкий интервал нужен не везде: для метрики «свободно диска» хватит минуты. Начинай с 15-30 секунд и меняй по необходимости, а не по привычке.
Главное: интервал это компромисс между точностью, местом и нагрузкой, а
scrape_timeoutне больше интервала.
Откуда берутся метрики хоста и контейнеров?
Что именно читают node_exporter и cAdvisor
Метрики приложения отвечают на вопрос «что делает программа». Но когда приложение тормозит, причина часто под ним: не хватает памяти, диск забит, контейнер упёрся в лимит процессора. Без метрик хоста и контейнеров ты видишь симптом, но не причину.
Врач смотрит не только на жалобы пациента, но и на анализы крови. Приложение рассказывает о себе само (жалобы), а экспортёры снимают «анализы» независимо, не спрашивая программу.
Linux сам ведёт учёт ресурсов и выкладывает его как обычные файлы в каталогах /proc и /sys (урок 1.5). node_exporter каждый раз при запросе /metrics читает эти файлы и переводит цифры в формат Prometheus. Например, node_memory_MemAvailable_bytes это байты памяти, которые можно ещё использовать. cAdvisor делает то же, но для каждого контейнера отдельно: читает учёт расхода ресурсов, который ведёт ядро для каждой группы процессов, и отдаёт метрики с префиксом container_ (например, container_memory_usage_bytes).
Разберём на примере. У node_exporter метрика node_load1 это средняя нагрузка за последнюю минуту (число процессов, которые хотят процессор). На машине с двумя ядрами значение 2 означает «заняты оба ядра, очереди нет», а 6 означает «в очереди стоят ещё четыре». Важное отличие от приложения: экспортёр ничего не считает сам, он только переводит. Поэтому его страница /metrics всегда свежая, а при его остановке пропадёт только картина хоста, приложение останется видимым.
Осторожно, тут часто путают. Что экспортёр «собирает» метрики и хранит их. Нет: он отвечает только в момент запроса, хранит и собирает историю по-прежнему Prometheus.
Главное: экспортёры переводят состояние
/procи/sysи ничего не хранят, история остаётся у Prometheus.
Проверь понимание: node_exporter остановили, приложение работает. Какие метрики пропадут из Prometheus?
Ответ
Пропадут метрики хоста (память, диск, процессор), цель node станет down. Метрики приложения продолжат собираться, потому что это отдельная цель.
А как считает метрики само приложение?
Как приложение считает метрики: путь одного запроса
Метрики сами не появятся: кто-то должен в нужный момент прибавить единицу к счётчику и записать длительность. Этим занимается библиотека-клиент внутри приложения. В Python это prometheus_client.
Турникет в метро. Каждый проход добавляет единицу на счётчике, а камера рядом засекает, сколько времени человек шёл по коридору. Турникету не нужно знать, кто и когда прочитает эти цифры.
Библиотека хранит все метрики в одном месте, реестре (registry), в памяти процесса. Когда приходит запрос:
- Приложение запоминает время начала.
- Обрабатывает запрос и отправляет ответ.
- В момент отправки статуса вызывается наш код: он прибавляет 1 к нужному ряду счётчика (по метке метода, пути и статуса) и кладёт длительность в гистограмму.
- Когда Prometheus приходит на
/metrics, функцияgenerate_latest()просто печатает весь реестр текстом. Ничего не считается заново: страница это снимок текущих чисел.
sequenceDiagram
participant К as Клиент
participant А as app.py
participant Р as Реестр метрик
participant П as Prometheus
К->>А: GET /notes
Note over А: запомнил время начала
А-->>К: ответ 200
А->>Р: +1 к requests_total{GET,/notes,200}
А->>Р: длительность в корзины гистограммы
П->>А: GET /metrics
А-->>П: generate_latest(): текст всего реестра
Счёт ведётся в момент ответа, а при запросе /metrics реестр просто печатается как есть.
Разберём на примере. Пришли три запроса: GET /notes за 0.03 с, GET /notes за 0.4 с и GET /nope. После них в реестре: requests_total{GET,/notes,200} = 2, requests_total{GET,other,404} = 1 (неизвестный путь стал other, см. предыдущий раздел). В гистограмме для /notes две записи: 0.03 попало в корзины от le="0.05" и выше, 0.4 попало в корзины от le="0.5" и выше. Счётчики лежат в памяти процесса, поэтому после перезапуска app.py всё начнётся с нуля (теория про сброс выше).
Прикинь сам: сервис обработал 1 000 000 запросов на
/notesсо статусом 200. Сколько рядов у счётчика запросов из-за этого?
Один: {method="GET",path="/notes",status="200"}. Число запросов это значение ряда, а не количество рядов. Растёт число рядов только с новыми комбинациями меток.
Осторожно, тут часто путают. Что метрики «весят» столько же, сколько запросов. Нет: память занимает число рядов, а не число запросов. Миллион запросов в один и тот же ряд стоит те же байты, что один.
Главное: приложение прибавляет счётчики в момент ответа, а
/metricsпечатает реестр как снимок.
Как называть то, что считаем?
Как называть метрики
Через полгода метрик будут сотни, и по имени должно быть понятно, что это и в чём измерено. В Prometheus есть договорённости, которые помогают не путаться и не ошибиться в единицах.
- Имя начинается с префикса проекта:
notes_.... Так видно, чья это метрика. - Счётчики заканчиваются на
_total:notes_http_requests_total. - Единицу измерения пишут в имени и берут базовую: секунды (не миллисекунды), байты (не мегабайты):
notes_http_request_duration_seconds. - Имя описывает, что измеряют, а не как это будут считать. Значения, которые можно получить складыванием или вычитанием, лучше не хранить отдельно (например, «доля ошибок» не метрика, её считают запросом из двух счётчиков).
Разберём на примере. Метрика request_time_ms нарушает два правила: нет префикса проекта и единица не базовая (миллисекунды). Правильно: notes_http_request_duration_seconds. Разработчик, который увидит в Grafana значение 0.25 и не знает единицы, всё равно поймёт, что это секунды, а не миллисекунды.
Осторожно, тут часто путают. Что название не важно, «главное, чтобы работало». Смешанные единицы (где-то секунды, где-то миллисекунды) приводят к ошибкам в расчётах на порядок, и заметить их трудно.
Главное: префикс проекта,
_totalу счётчиков и базовые единицы (секунды, байты) спасают от ошибок на порядок.
Проверь понимание: как назвать счётчик числа отправленных писем?
Ответ
notes_emails_sent_total: префикс проекта, что считаем, суффикс _total для счётчика.
Какие ряды появляются у каждой цели без нашей помощи?
Метрики, которые Prometheus пишет сам
Кроме метрик приложения, Prometheus сам записывает несколько служебных рядов про каждый сбор. Они показывают, здорова ли сама система сбора.
К каждой цели автоматически добавляются:
| Метрика | Что означает |
|---|---|
up |
1 если сбор удался, 0 если нет |
scrape_duration_seconds |
сколько секунд занял сбор |
scrape_samples_scraped |
сколько значений вернула цель за один сбор |
scrape_samples_post_metric_relabeling |
сколько значений осталось после фильтрации |
Разберём на примере. У «Заметок» scrape_samples_scraped будет в районе нескольких сотен: наши метрики плюс стандартные метрики процесса Python (использование памяти, число сборок мусора), которые prometheus_client добавляет сам. Если завтра это число выросло до 20 000, значит, у цели появились лишние ряды. Это самый быстрый способ заметить взрыв кардинальности, ещё до того как Prometheus упадёт.
Прикинь сам:
up{job="notes"} = 1, а пользователи получают 500. Противоречие?
Нет. up относится только к сбору метрик: /metrics отвечает, значит up = 1. Ошибки пользователей видны в счётчике notes_http_requests_total со статусом 500, а не в up.
Осторожно, тут часто путают. Что up = 1 значит «сервис здоров». Нет: он значит только «/metrics ответил корректно». Приложение может отвечать на /metrics и при этом падать на реальных запросах. Здоровье для пользователя измеряют ошибками и задержкой (RED из 8.1), а внешняя проверка, ходит ли реальный клиент, это тема 8.4.
Главное:
up = 1значит только «/metricsответил», а здоровье для пользователя измеряют ошибками и задержкой.
Как всё это связано с целями из урока 8.1?
Как метрики связаны с SLI из урока 8.1
Мы собираем метрики не ради метрик, а чтобы посчитать SLI и SLO из 8.1. Полезно сразу увидеть, какие ряды для чего нужны.
- Доступность: «хорошие запросы делить на все». Все запросы это сумма
notes_http_requests_total, плохие это те, у которыхstatusначинается с 5. Меткаstatusв счётчике нужна именно для этого. - Задержка: доля запросов быстрее порога. Её даёт гистограмма: корзина
le="0.5"делится на_count. Порог в SLI должен совпадать с границей корзины: в нашем списке0.25и0.5, поэтому порог 300 мс из 8.1 мы в 8.3 заменим ближайшей границей.
Разберём на примере. За час: всего запросов на /notes 1 000, из них со статусом 5xx 12. Доступность = (1 000 - 12) / 1 000 = 98.8%. В гистограмме _count = 1 000, _bucket{le="0.5"} = 950. Доля быстрее 0.5 секунды = 950 / 1 000 = 95%.
Осторожно, тут часто путают. Что порог SLI можно выбирать любой. Раз квантили и доли считаются по корзинам, порог, не совпадающий с границей, даст приблизительный ответ. Корзины планируют вместе с SLO.
Главное: доступность считают по счётчику со статусом, задержку по корзине гистограммы, а порог SLI совпадает с границей корзины.
Проверь понимание: в гистограмме
_count= 2 000,_bucket{le="0.25"}= 1 700. Какая доля запросов быстрее 250 мс?
Ответ
1 700 / 2 000 = 0.85, то есть 85%.
Осталось прочитать конфиг, который всё это запускает.
Конфиг prometheus.yml: из чего он состоит
Список целей и интервалы Prometheus берёт из одного файла, поэтому его нужно уметь читать.
YAML-файл (см. урок 4.5: отступы пробелами значат вложенность). Два главных блока:
global:
scrape_interval: 15s # как часто ходить за метриками по умолчанию
scrape_configs: # список задач сбора
- job_name: notes # имя задачи: станет меткой job="notes"
metrics_path: /metrics # путь, по умолчанию именно он (можно не писать)
static_configs: # статический список целей
- targets: ["notes:8080"] # host:port; имя резолвит сеть Docker
Разберём на примере. Адрес цели notes:8080 использует имя контейнера в сети Docker (урок 4.5), поэтому Prometheus, живущий в той же сети notes-net, находит его по имени. localhost:8080 не подошёл бы: для Prometheus localhost это он сам, а не соседний контейнер.
Прикинь сам: почему в
targetsнельзя написатьlocalhost:8080для приложения, которое работает в соседнем контейнере?
Внутри контейнера Prometheus localhost указывает на сам контейнер Prometheus, а порт 8080 в нём никто не слушает. Соседний контейнер доступен по имени сервиса в общей сети: notes:8080.
Осторожно, тут часто путают. Что Prometheus сразу увидит правку файла. Он читает конфиг при старте и по сигналу перечитывания (в нашем стенде проще перезапустить контейнер). Перед этим конфиг проверяют утилитой promtool check config: она находит опечатки в отступах и ключах до запуска.
Главное: в конфиге глобальный интервал и список задач с целями, а адреса соседей пишут по имени контейнера, не
localhost.
Практика
Все команды в заданиях 1 и 2 запускай на машине, где стоит Docker и есть ~/notes из темы 4; в /etc/hosts должна быть строка 127.0.0.1 notes.lab, к приложению ходим через прокси: curl -sk https://notes.lab/.... Понадобится jq (программа для разбора JSON, урок 1.2); если её нет: sudo apt install jq.
Задание 1. Прочитай /metrics глазами на игрушечном примере
Цель: увидеть формат exposition и понять, как гистограмма превращается в корзины, до того как это станет частью «Заметок».
Предскажи: мы запишем в гистограмму четыре значения: 0.05, 0.3, 0.3 и 2.0 секунды, с корзинами 0.1, 0.5 и 1. Какое число будет в le="0.5"? А в le="+Inf"?
Ответ
В le="0.5" будет 3 (0.05 и два по 0.3, корзины кумулятивные), в le="+Inf" будет 4 (все наблюдения).
Шаги:
-
Создай отдельный каталог и виртуальное окружение (не в
~/notes):mkdir -p ~/metrics-demo && cd ~/metrics-demo python3 -m venv .venv .venv/bin/pip install prometheus_clientЧто здесь происходит:
python3 -m venv .venvсоздаёт «личную коробку» для Python-пакетов в каталоге.venv(урок 1.6 и тема 4 использовали то же самое), а.venv/bin/pip installставит библиотекуprometheus_clientтолько в неё, не трогая системный Python. -
Создай
demo.py:import time from prometheus_client import Counter, Gauge, Histogram, start_http_server # счётчик с меткой status: два ряда REQS = Counter("demo_requests", "Сколько запросов обработано", ["status"]) # гистограмма с тремя корзинами (плюс автоматическая +Inf) LAT = Histogram("demo_latency_seconds", "Время обработки", buckets=(0.1, 0.5, 1)) # датчик: значение можно ставить любое QUEUE = Gauge("demo_queue_size", "Размер очереди") start_http_server(8000) # /metrics на порту 8000 REQS.labels("200").inc(3) REQS.labels("500").inc() for value in (0.05, 0.3, 0.3, 2.0): LAT.observe(value) QUEUE.set(7) time.sleep(3600) -
Запусти в фоне и прочитай метрики:
.venv/bin/python demo.py & sleep 1 curl -s localhost:8000/metrics | grep '^demo_' | grep -v _createdРазбор:
&в конце первой строки запускает программу в фоне, чтобы терминал остался твоим;sleep 1даёт ей секунду стартовать;curl -sскачивает страницу молча (урок 2.4); первыйgrep '^demo_'оставляет строки, начинающиеся сdemo_(знак^значит «в начале строки»), аgrep -v _createdвыбрасывает строки со словом_created(клиент добавляет время создания каждой метрики, нам оно сейчас не нужно).В
demo.pyсначала объявлены три метрики:Counter,Histogram(с корзинами 0.1, 0.5, 1),Gauge. Метод.inc()увеличивает счётчик,.observe(x)кладёт значение в гистограмму,.set(7)ставит датчик.start_http_server(8000)запускает встроенный веб-сервер, который отдаёт/metrics.
Что должно получиться:
demo_requests_total{status="200"} 3.0
demo_requests_total{status="500"} 1.0
demo_latency_seconds_bucket{le="0.1"} 1.0
demo_latency_seconds_bucket{le="0.5"} 3.0
demo_latency_seconds_bucket{le="1.0"} 3.0
demo_latency_seconds_bucket{le="+Inf"} 4.0
demo_latency_seconds_count 4.0
demo_latency_seconds_sum 2.65
demo_queue_size 7.0
Последние знаки _sum могут отличаться из-за чисел с плавающей точкой. Убери фоновый процесс: kill %1.
Как читать вывод: каждая строка это один временной ряд. demo_requests_total два ряда (по статусам 200 и 500), у гистограммы четыре корзины и два служебных ряда _count и _sum. Клиент дописал _total к имени счётчика сам. Корзина le="0.5" равна 3: 0.05 и два по 0.3, ровно как в таблице теории. _sum 2.65 это 0.05 + 0.3 + 0.3 + 2.0. Все значения выведены как 3.0, потому что Prometheus хранит числа с плавающей точкой.
Объясни себе:
- Почему счётчик назван
demo_requests, а в выводе онdemo_requests_total? - Как из этих строк узнать среднее время запроса? (подсказка:
_sumи_count) - Что изменится в выводе, если сделать
LAT.observe(0.1): в какую корзину попадёт значение ровно на границе?
Типичные ошибки:
OSError: [Errno 98] Address already in use: порт 8000 занят прошлым запуском демо. Найди процессss -ltnp | grep 8000и останови его (kill), либо смени порт.ModuleNotFoundError: No module named 'prometheus_client': запустил системнымpython3, а не.venv/bin/python. Пакеты в venv видит только его интерпретатор.error: externally-managed-environmentприpip install: ставишь в системный Python. Используй venv, как в шаге 1.The virtual environment was not created successfully because ensurepip is not available: в Ubuntu модуль venv ставится отдельно. Выполниsudo apt install python3-venvи повтори шаг 1.
Задание 2. Подними стек мониторинга и увидь первый target DOWN
Цель: запустить Prometheus, node_exporter и cAdvisor в Compose, подключить их к сети «Заметок» и прочитать состояние целей.
Предскажи: основной стек «Заметок» работает на образе 0.4.1, там ещё нет /metrics. Какой будет статус цели notes и что напишет Prometheus в поле ошибки? Как отреагирует up?
Ответ
Цель notes будет down, а up{job="notes"} равен 0. Приложение живо и отвечает, но на /metrics возвращает 404 (в 4.2 «Заметок» такого пути нет), поэтому текст ошибки: server returned HTTP status 404 Not Found. Prometheus считает успехом только ответ 200 с корректным телом.
Шаги:
-
Убедись, что основной стек запущен (без него сеть
notes-netне существует), из каталога~/notes:cd ~/notes docker compose psdocker compose psпоказывает контейнеры проекта; в колонке STATUS должно бытьUpилиhealthy. -
Создай каталоги и файл
monitoring/compose.yml:mkdir -p monitoring/prometheus# Стек мониторинга. Основной compose.yml должен быть запущен раньше: # сеть notes-net создаёт он, здесь мы к ней только подключаемся. services: prometheus: image: prom/prometheus:v3.15.0 command: - --config.file=/etc/prometheus/prometheus.yml - --storage.tsdb.path=/prometheus - --storage.tsdb.retention.time=15d ports: - "9090:9090" volumes: - ./prometheus:/etc/prometheus:ro - prom-data:/prometheus restart: unless-stopped node_exporter: image: prom/node-exporter:v1.12.1 command: - --path.rootfs=/host pid: host ports: - "9100:9100" volumes: - /:/host:ro,rslave restart: unless-stopped cadvisor: image: gcr.io/cadvisor/cadvisor:v0.60.6 ports: - "8081:8080" # наружу 8081, внутри сети cAdvisor слушает 8080 volumes: - /:/rootfs:ro - /var/run:/var/run:ro - /sys:/sys:ro - /var/lib/docker/:/var/lib/docker:ro - /dev/disk/:/dev/disk:ro devices: - /dev/kmsg restart: unless-stopped networks: default: name: notes-net external: true volumes: prom-data: -
Создай
monitoring/prometheus/prometheus.yml:global: scrape_interval: 15s scrape_configs: # само приложение: /metrics по умолчанию - job_name: notes static_configs: - targets: ["notes:8080"] # метрики хоста - job_name: node static_configs: - targets: ["node_exporter:9100"] # метрики контейнеров (внутри сети порт 8080) - job_name: cadvisor static_configs: - targets: ["cadvisor:8080"]
Разберём compose.yml по частям (комментарии в самом файле объясняют остальное). image: prom/prometheus:v3.15.0 фиксирует версию образа. В command: перечислены флаги Prometheus: где конфиг, где база и срок хранения. ports: "9090:9090" открывает интерфейс на порту хоста (урок 4.3). В volumes: строка ./prometheus:/etc/prometheus:ro подключает твой каталог с конфигом внутрь контейнера только для чтения (ro), а prom-data:/prometheus это именованный том для базы. У node_exporter pid: host даёт ему видеть процессы хоста, а /:/host:ro,rslave подключает корень хоста только для чтения: без этого он мерил бы пустую файловую систему своего контейнера, а не диски машины (--path.rootfs=/host подсказывает, где искать «настоящий» корень). Сетевые метрики (node_network_*) при такой схеме относятся к сетевому пространству самого контейнера, а не хоста: чтобы мерить сеть хоста, у node_exporter нужен network_mode: host, в учебном стенде это не делаем. cAdvisor монтирует каталоги хоста и Docker, потому что читает данные о контейнерах прямо оттуда. В конце блок networks говорит: «не создавай свою сеть, подключись к уже существующей notes-net» (external: true).
-
Проверь конфиг до запуска, затем подними стек и через 20 секунд посмотри цели:
# Разбор: docker run --rm запускает временный контейнер и удаляет его после; # -v подключает наш каталог с конфигом; --entrypoint promtool запускает # проверяющую утилиту вместо самого Prometheus. docker run --rm -v "$PWD/monitoring/prometheus:/etc/prometheus:ro" \ --entrypoint promtool prom/prometheus:v3.15.0 \ check config /etc/prometheus/prometheus.yml docker compose -f monitoring/compose.yml up -d sleep 20 curl -s localhost:9090/api/v1/targets | jq -r '.data.activeTargets[] | "\(.labels.job)\t\(.health)\t\(.lastError)"'Разбор последней команды:
curl -s localhost:9090/api/v1/targetsспрашивает у Prometheus список целей в формате JSON.jq -rпечатает без кавычек;.data.activeTargets[]берёт каждую цель; в строке"\(.labels.job)\t\(.health)\t\(.lastError)"подставляются имя задачи, состояние и последняя ошибка, а\tэто табуляция между колонками.
Что должно получиться:
Checking /etc/prometheus/prometheus.yml
SUCCESS: /etc/prometheus/prometheus.yml is valid prometheus config file syntax
cadvisor up
node up
notes down server returned HTTP status 404 Not Found
Порядок строк может отличаться.
Как читать вывод: первые две строки от promtool: SUCCESS значит конфиг правильный. Дальше три цели: up у cadvisor и node значит, что Prometheus получил и разобрал их /metrics. У notes состояние down, а в последней колонке причина server returned HTTP status 404 Not Found: приложение живо, но пути /metrics в версии 0.4.1 нет. Это ожидаемо, починим в задании 3. Пустая последняя колонка у здоровых целей значит «ошибок нет». Заодно загляни в экспортёры: curl -s localhost:9100/metrics | grep -E '^node_load1 ' и curl -s localhost:8081/metrics | grep -c '^container_'. Открой в браузере http://localhost:9090/targets и найди ту же ошибку. Если браузера нет на сервере, пробрось порт: ssh -L 9090:localhost:9090 <сервер>.
Объясни себе:
- Почему в
prometheus.ymlадресnotes:8080, а неlocalhost:8080? - Почему cAdvisor опрашивается на порту 8080, хотя снаружи он на 8081?
- Зачем монтировать
/хоста в node_exporter и что бы он показывал без этого?
Типичные ошибки:
network notes-net declared as external, but could not be found: основной стек не запущен, сети нет. Запустиdocker compose up -dв~/notes, затем стек мониторинга.Bind for 0.0.0.0:9090 failed: port is already allocated: порт занят другим контейнером или процессом. Найдиss -ltnp | grep 9090.parsing YAML file /etc/prometheus/prometheus.yml: yaml: line 9: did not find expected key: сбился отступ вprometheus.yml. Проверьpromtool check config(шаг 4).cadvisorперезапускается и в логахfailed to open /dev/kmsg: в ВМ нет этого устройства. Убери секциюdevicesи добавьprivileged: trueтолько для учебного стенда.
Задание 3. Шаг проекта: /metrics в «Заметках», образ 0.5.0
Цель: добавить в app.py метрики (v5), пересобрать образ 0.5.0, увидеть up = 1 и зафиксировать состояние тегом v0.5.0.
Предскажи: мы отправим несколько запросов, включая /error и несуществующий путь /nope. Под какой меткой path будет виден /nope и почему?
Ответ
Под path="other". Метка берётся только из фиксированного списка маршрутов, всё остальное схлопывается в other, чтобы посторонние URL не размножали ряды. Статус при этом настоящий: 404.
Шаги:
-
Добавь зависимость. Установи пакет в виртуальное окружение проекта с закреплённой версией и допиши её в
requirements.txt, откуда её берёт сборка образа (урок 4.4):cd ~/notes echo 'prometheus_client==0.26.0' >> requirements.txt .venv/bin/pip install -r requirements.txt .venv/bin/pip freeze | grep -i prometheusВывод последней команды:
prometheus_client==0.26.0. Закреплённая версия (==) нужна, чтобы образ через полгода собрался с тем же клиентом, а не с новым, где что-то могло поменяться. -
В
app.pyдобавь импорт и объявления метрик выше классаHandler. Остальные импорты (time,threading,urlparse) в файле уже есть:from prometheus_client import (CONTENT_TYPE_LATEST, Counter, Gauge, Histogram, generate_latest) # маршруты, которые допустимы в метке path; всё остальное станет "other" KNOWN_ROUTES = {"/", "/notes", "/healthz", "/readyz", "/headers", "/slow", "/error", "/leak", "/burn", "/slowsql", "/metrics"} HTTP_REQUESTS = Counter( "notes_http_requests_total", "Число HTTP-запросов", ["method", "path", "status"]) HTTP_DURATION = Histogram( "notes_http_request_duration_seconds", "Длительность запроса, секунды", ["method", "path"], buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10)) NOTES_TOTAL = Gauge("notes_notes_total", "Сколько заметок сохранено") BUILD_INFO = Gauge("notes_build_info", "Версия сборки", ["version"]) NOTES_TOTAL_LOCK = threading.Lock() def route_label(path): # сырой URL в метку не попадает никогда: только известные маршруты return path if path in KNOWN_ROUTES else "other" def update_notes_total(): # чтение и обновление идут по очереди: старый результат не затрёт новый with NOTES_TOTAL_LOCK: NOTES_TOTAL.set(len(list_notes()))Что здесь:
Counter,Gauge,Histogramиз теории. Первый параметр это имя метрики, второй описание для# HELP, третий список имён меток.buckets=(...)это границы корзин в секундах: от 5 мс до 10 с, чтобы покрыть и быстрые ответы, и/slow.BUILD_INFOхранит версию сборки в метке, значение всегда 1: так принято показывать «строковые» данные.KNOWN_ROUTESиroute_labelреализуют защиту от взрыва кардинальности из теории.update_notes_total()пересчитывает число заметок в датчике. -
В классе
Handler(тот же, что и в прошлых версиях) добавь два метода.parse_requestвызывается после разбора строки запроса и запоминает время старта, аlog_requestвызывается стандартной библиотекой при отправке статуса ответа, там мы записываем метрики и передаём управление прежнему access-логу:def parse_request(self): self._t0 = time.monotonic() return super().parse_request() def log_request(self, code="-", size="-"): path = route_label(urlparse(self.path).path) HTTP_REQUESTS.labels(self.command, path, str(code)).inc() HTTP_DURATION.labels(self.command, path).observe( time.monotonic() - self._t0) super().log_request(code, size) -
В методе
_handleдобавь путь/metricsв множество известныхroutes(иначе он вернёт 404) и обработку рядом с остальными маршрутами. Метод_sendуже сам ставитContent-Length:if path == "/metrics": # generate_latest() отдаёт весь реестр метрик текстом return self._send(200, generate_latest(), CONTENT_TYPE_LATEST) -
Обнови датчики. В
main()послеinitialize_storage()добавьBUILD_INFO.labels(VERSION).set(1)иupdate_notes_total()(оберни его вtry/except STORAGE_ERRORS, чтобы недоступная база не мешала старту), а в_create_noteпослеsave_note(text)вызовиupdate_notes_total(). Полный вариант файла: versions/v5.py в эталоне. -
Пересобери образ и перезапусти сервис. В
compose.ymlу сервисаnotesукажиimage: notes:0.5.0рядом сbuild: ., в.envпоставьAPP_VERSION=0.5.0. Разбор команд:docker build -t notes:0.5.0 .собирает образ из текущего каталога и даёт ему имя и версию;docker compose up -d notesпересоздаёт только сервисnotes;grep -E '^(a|b)'печатает строки, начинающиеся наaилиb;-kуcurlне проверяет самоподписанный сертификатnotes.lab(урок 4.6):docker build -t notes:0.5.0 . docker compose up -d notes curl -sk https://notes.lab/metrics | grep -E '^(notes_build_info|notes_notes_total)' -
Дай Prometheus что посчитать и проверь цель.
for i in $(seq 10); do ...; doneповторяет команду 10 раз (урок 1.6),-X POST -d ...отправляет заметку,>/dev/nullвыбрасывает ответ. Запрос к Prometheus--data-urlencode 'query=...'передаёт выражение на языке запросов (PromQL, подробно в 8.3):path=~"/notes|/error|other"значит «путь подходит под одно из трёх значений».for i in $(seq 10); do curl -sk -X POST https://notes.lab/notes -d '{"text":"метрика"}' >/dev/null; done for i in $(seq 3); do curl -sk https://notes.lab/error >/dev/null; done for i in $(seq 2); do curl -sk https://notes.lab/nope >/dev/null; done sleep 20 curl -s localhost:9090/api/v1/query \ --data-urlencode 'query=notes_http_requests_total{path=~"/notes|/error|other"}' \ | jq -r '.data.result[] | "\(.metric.method) \(.metric.path) \(.metric.status) \(.value[1])"' -
Проверь
upи поведение при остановке сервиса, затем зафиксируй результат:docker compose stop notes sleep 30 curl -s localhost:9090/api/v1/query --data-urlencode 'query=up{job="notes"}' | jq -r '.data.result[0].value[1]' docker compose start notes git add app.py requirements.txt compose.yml monitoring git commit -m "Метрики Prometheus: /metrics, monitoring/ (v0.5.0)" git tag v0.5.0
Что должно получиться:
notes_build_info{version="0.5.0"} 1.0
notes_notes_total 0.0
POST /notes 201 10
GET /error 500 3
GET other 404 2
0
Как читать вывод: первая строка показывает версию сборки в метке. Вторая: число заметок в датчике. Дальше по строке на ряд: метод, путь, статус, счётчик: 10 успешных POST /notes (201), 3 вызова /error со статусом 500, 2 вызова /nope, которые стали other со статусом 404. Последнее значение 0: после stop цель не отвечает и up равен 0.
Значение notes_notes_total зависит от того, сколько заметок уже записано. После docker compose start notes через 15-30 секунд up{job="notes"} вернётся в 1, а счётчики notes_http_requests_total начнут с нуля: процесс перезапустился. Состояние эталона: app.py v5, образ 0.5.0, тег v0.5.0. Метрики доступны и через https://notes.lab/metrics (nginx проксирует всё подряд); в реальной среде этот путь закрывают от внешнего мира, к теме вернёмся в 8.4.
Объясни себе:
- Почему время старта запоминается в
parse_request, а не вhandle_one_request? (подсказка: keep-alive из HTTP/1.1, урок 2.4) - Почему после
stopзначениеupстало 0 не сразу, а через один-два интервала сбора? - Что случилось со счётчиками после рестарта и почему на графике это не должно выглядеть провалом скорости?
Типичные ошибки:
ModuleNotFoundError: No module named 'prometheus_client'в логах контейнера: пакет не попал вrequirements.txtили образ не пересобран. Проверь файл, выполниdocker buildзаново.Duplicated timeseries in CollectorRegistry: {'notes_http_requests_total'}: метрика объявлена дважды (например, объявление попало внутрь метода или файл импортируется повторно). Объявляй метрики один раз на уровне модуля.ValueError: Incorrect label count:labels()вызван с другим числом значений, чем в объявлении. УCounterтри метки, уHistogramдве.- Цель
notesостаётсяdownсserver returned HTTP status 404 Not Found: контейнер всё ещё на старом образе. Проверьdocker compose psи тег образа, выполниdocker compose up -d notes.
Сломай и почини
В этом разделе ты тренируешь главный навык дежурного: по симптому найти причину. Скрипт сам ломает конфиг стека мониторинга или добавляет лишний источник, не заглядывай в него: диагностика и есть упражнение. Скрипт правит только ~/notes/monitoring/prometheus/prometheus.yml и создаёт один контейнер с меткой break=8.2. Запускай его без sudo, от пользователя, который работает с Docker.
Скачай скрипт. У curl флаг -f означает «при ошибке сервера не сохраняй страницу с ошибкой», -L разрешает переходить по перенаправлениям, -o задаёт имя файла:
curl -fsSL -o /tmp/break-8.2.sh https://raw.githubusercontent.com/distinguished-sre/learning/main/devops/project/notes/break/8.2/break.sh
bash /tmp/break-8.2.sh 1
Сценарии: 1, 2 и 3. Проходи по одному: запусти, найди причину, почини сам или командой bash /tmp/break-8.2.sh fix, потом бери следующий. В конце выполни fix и удали скрипт: rm /tmp/break-8.2.sh. Если стек мониторинга лежит не в ~/notes, задай путь: NOTES_DIR=/путь/к/notes bash /tmp/break-8.2.sh 1.
Симптом
- Сценарий 1. На странице
http://localhost:9090/targetsцельnotesкрасная (down), хотяdocker compose psпоказывает, что приложение работает. - Сценарий 2. Цель
notesтоже красная, но текст ошибки другой, аcurlизнутри контейнера отвечает на/metricsнормально. - Сценарий 3. Все цели
up, но Prometheus стал заметно тяжелее и число рядов растёт с каждой минутой.
Для каждого сценария запиши, что видишь: текст в поле ошибки цели (lastError), значение up, число рядов.
Гипотезы
Составь список причин, которые дают такой симптом, и проверь их в порядке дешевизны. Например: цель недоступна по сети; путь /metrics отдаёт не 200; какой-то источник отдаёт слишком много рядов; конфиг Prometheus не перечитан.
Проверки
Порядок диагностики:
# 1. что говорит сам Prometheus о цели (health и lastError)
curl -s localhost:9090/api/v1/targets | jq -r '.data.activeTargets[] | "\(.labels.job)\t\(.health)\t\(.lastError)"'
# 2. отвечает ли приложение изнутри сети мониторинга
docker compose -f monitoring/compose.yml exec prometheus wget -qO- http://notes:8080/metrics | head -5
# 3. сколько всего рядов у метрик приложения
curl -s localhost:9090/api/v1/query \
--data-urlencode 'query=count({__name__=~"notes_.*"})' | jq -r '.data.result[0].value[1]'
# 4. какие метрики самые «жирные» по числу рядов
curl -s localhost:9090/api/v1/status/tsdb | jq -r '.data.seriesCountByMetricName[:5][] | "\(.name) \(.value)"'
# 5. какая задача даёт эти ряды
curl -s localhost:9090/api/v1/query \
--data-urlencode 'query=count by (job) ({__name__="notes_http_requests_total"})' | jq -c '.data.result[]'
Разбор: wget -qO- внутри контейнера делает то же, что curl -s снаружи: так ты проверяешь доступность из той же сети, в которой работает Prometheus (с ноутбука всё может выглядеть иначе). count(...) считает ряды, count by (job) считает ряды отдельно по каждой задаче. Команду 2 запускай из ~/notes.
Исправление
Разбор трёх сценариев
Сценарий 1. connection refused. В lastError видно Get "http://notes:8080/metrics" или, в этой поломке, Get "http://notes:8081/metrics": dial tcp 172.18.0.4:8081: connect: connection refused. Имя резолвится (контейнер есть), но на порту 8081 никто не слушает. Приложение слушает 8080: проверь docker compose ps и docker compose logs notes, затем сравни порт в targets файла prometheus.yml с портом приложения и верни 8080. Если бы в ошибке было no such host, это значило бы, что контейнера нет в сети notes-net. После правки docker compose -f monitoring/compose.yml restart prometheus, через 15-30 секунд up = 1.
Сценарий 2. Неверный metrics_path. Цель down с server returned HTTP status 404 Not Found, хотя приложение живо и curl на /metrics внутри контейнера даёт 200. В prometheus.yml у задачи notes появился metrics_path: /metric. Исправь путь (по умолчанию он /metrics, ключ можно вовсе убрать), проверь конфиг через promtool check config и перезапусти Prometheus. Признак, что дело в конфиге, а не в сервисе: curl руками работает, а Prometheus видит 404.
Сценарий 3. Взрыв кардинальности. Все цели up, но count({__name__=~"notes_.*"}) показывает тысячи, а seriesCountByMetricName ставит notes_http_requests_total на первое место. Запрос count by (job) указывает на задачу notes-shadow: это второй источник, который имитирует ошибку разработчика, когда в метку path попал сырой адрес вида /notes/17, и порождает 20 000 разных рядов. В реальной жизни ты бы нашёл виновную метку в коде и вернул route_label(...). Здесь достаточно убрать задачу из prometheus.yml и удалить контейнер break82-shadow (это делает fix). Уже записанные ряды не исчезнут сразу: они станут устаревшими и уйдут по retention, а чтобы очистить сразу на учебном стенде, выполни docker compose -f monitoring/compose.yml down -v (удалит том prom-data и всю историю). Главный вывод: метки метрик должны иметь конечный список значений, а sample_limit в конфиге задачи защищает Prometheus, отбрасывая цели с слишком большим числом рядов.
Убедись, что все три цели up, а docker ps -a --filter label=break=8.2 пуст.
ИИ в помощь
Общие правила работы с ИИ-помощником собраны на странице «ИИ-помощник», здесь только сценарии этой темы.
Задача: разобрать цель, которая показывает down.
В Prometheus цель notes в статусе down, lastError: Get "http://notes:8081/metrics": dial tcp 172.18.0.4:8081: connect: connection refused. Приложение слушает порт 8080 в Docker Compose-сети notes-net. Объясни построчно, что означает ошибка, и назови 2-3 места, где искать причину.
Проверь ответ: connection refused значит, что имя найдено, а порт закрыт. Типичная ошибка нейросетей: советовать «проверить DNS» или перезапустить Prometheus. Сверь адрес с prometheus.yml и портом приложения.
Задача: выбрать тип метрики и имя.
В приложении «Заметки» нужны метрики: число отправленных писем, текущее число открытых соединений с базой, время выполнения SQL-запроса. Для каждой назови тип (counter, gauge, histogram), имя по правилам Prometheus и набор меток с оценкой кардинальности.
Проверь ответ: у счётчика имя оканчивается на _total, у времени единица в секундах (_seconds), в метках нет неограниченных значений вроде user_id. Нейросети часто предлагают миллисекунды и метку с адресом запроса целиком.
Задача: проверить конфиг prometheus.yml перед запуском.
Вот мой prometheus.yml: <вставь файл без паролей>. Найди ошибки отступов и ключей, объясни, что делает каждый блок, и скажи, какой командой проверить файл до запуска.
Проверь ответ: проверь, что нейросеть назвала promtool check config, а не выдуманный флаг. Типичная ошибка: заменить имя контейнера на localhost, из-за чего цель перестаёт находиться.
Словарик урока
| Термин | Простыми словами |
|---|---|
| Сэмпл (sample) | Одна точка ряда: момент времени и число |
| Ряд (series) | Метрика с конкретным набором меток |
Состояние цели (health), lastError |
up или down на странице Targets и текст последней ошибки сбора |
scrape_timeout |
Сколько Prometheus ждёт ответа цели; не больше интервала |
| Prometheus | Программа, которая по расписанию собирает метрики и хранит их во времени |
| Pull | Схема, при которой сборщик сам приходит за данными, а не ждёт, пока их пришлют |
| Цель (target) | Один адрес, откуда собирают метрики, например notes:8080 |
| Задача (job) | Группа однотипных целей; становится меткой job |
| Экземпляр (instance) | Конкретный адрес цели внутри задачи; метка instance |
| Scrape | Один сбор метрик с цели: HTTP-запрос и разбор ответа |
scrape_interval |
Как часто собирать; у нас 15 секунд |
/metrics |
Страница приложения с текущими числами в текстовом формате |
| Формат exposition | Текстовый формат /metrics: # HELP, # TYPE и строки имя{метки} значение |
Метрика up |
Метрика, которую Prometheus сам пишет про цель: 1 ответила, 0 нет |
| Временной ряд | Последовательность пар «время, число» для одного имени и набора меток |
| Метка (label) | Пара имя="значение", которая делит метрику на группы |
| Кардинальность | Число уникальных комбинаций значений меток, то есть число рядов |
| Counter | Счётчик: только растёт, при перезапуске обнуляется |
| Gauge | Датчик: значение идёт вверх и вниз |
| Histogram | Распределение значений по корзинам: _bucket, _sum, _count |
Корзина (bucket, le) |
Диапазон гистограммы с верхней границей; корзины кумулятивные |
| Summary | Сводка с квантилями, посчитанными в приложении; в новом коде редко |
| Staleness | Пометка ряда устаревшим: сразу при неудачном сборе или исчезновении цели; без маркера ряд живёт в запросах до 5 минут (lookback delta) |
| TSDB, retention | База временных рядов Prometheus и срок хранения данных (по умолчанию 15 дней) |
| Exporter | Программа-переводчик: читает чужую систему и отдаёт /metrics |
| node_exporter, cAdvisor | Экспортёры метрик хоста и метрик контейнеров |
| Pushgateway | Шлюз для короткоживущих задач: они отправляют туда итог, Prometheus забирает |
| Service discovery | Автоматический поиск целей (в Kubernetes, 8.9) |
promtool |
Утилита проверки конфигов Prometheus |
Вопросы с собеседований
Раздел для повторения: ответь вслух, потом открой ответ. Вопросы с пометкой «часто» задают почти на каждом собеседовании по теме урока: начни с них.
1. [junior] [часто] Чем pull-модель Prometheus отличается от push и в чём плюсы pull?
Ответ
Prometheus сам ходит по целям (targets) и забирает метрики с /metrics через заданный интервал, это и есть pull. Push - когда приложение само отправляет данные в хранилище. У pull есть плюсы: по up сразу видно, что сбор с цели не удался (up 0: цель лежит или отдаёт мусор вместо метрик; up 1 ещё не значит, что приложение здорово), метрики можно открыть в браузере или через curl, а нагрузку на сбор контролирует сервер, а не клиенты. Цели находятся через service discovery (автоматический поиск целей, например в Kubernetes). Push нужен для коротких пакетных задач, которые успевают завершиться между опросами: для них есть Pushgateway, но это исключение, а не норма. Старые значения в нём не исчезают сами, их удаляют явно.
Что хотят услышать: Prometheus сам опрашивает /metrics, метрика up как проверка живости цели, service discovery, контроль нагрузки на стороне сервера, Pushgateway только для короткоживущих задач.
Красный флаг: «Prometheus принимает метрики от приложений» без оговорок или совет слать всё через Pushgateway.
2. [junior] [часто] Чем counter отличается от gauge и что у них происходит при рестарте процесса?
Ответ
Counter только растёт: число запросов, число ошибок. Gauge показывает текущее значение: размер очереди, память. При рестарте процесса счётчик сбрасывается в 0, датчик принимает новое текущее значение. Поэтому по счётчику смотрят rate(), а не значение.
Что хотят услышать: примеры для каждого типа, сброс счётчика, что rate его обрабатывает.
Красный флаг: «счётчик может уменьшаться» или использование gauge для числа запросов.
3. [junior] Коллега предлагает слать метрики из cron-скрипта прямо в Prometheus. Что скажешь?
Ответ
Prometheus сам забирает метрики по HTTP (pull), принимать «пуш» напрямую он не умеет. Скрипт живёт секунды, его не успеют опросить. Для таких задач в конце скрипта отправляют итог в Pushgateway, а Prometheus собирает уже Pushgateway. Но это для коротких задач, обычные сервисы остаются на pull.
Что хотят услышать: pull-модель, Pushgateway для короткоживущих задач, оговорку, что у Pushgateway нет автоматической очистки старых значений и up показывает живость шлюза, а не задачи.
Красный флаг: «просто поставим там push вместо pull» без понимания, зачем Prometheus ходит сам.
4. [junior] В Prometheus цель красная (DOWN). Твои действия
Ответ
Сначала смотрю страницу /targets и текст lastError. connection refused значит, что процесс не слушает порт; no such host значит, что нет имени в сети; context deadline exceeded значит, что цель не успела ответить за таймаут; 404 значит, что неверный metrics_path. Потом иду curl-ом к цели из той же сети, где работает Prometheus.
Что хотят услышать: разбор по тексту ошибки, проверка из сети самого Prometheus (а не с ноутбука), сверка порта и metrics_path, статус up.
Красный флаг: «перезапущу Prometheus» без чтения ошибки.
5. [junior] [на скорость] После деплоя график requests_total упал в ноль. Это баг?
Ответ
Скорее всего нет: процесс перезапустился, и счётчик обнулился. rate() и increase() замечают такой сброс и не показывают провала. Проверяю, что цель снова up, и смотрю на скорость запросов, а не на сырое значение.
Что хотят услышать: сброс счётчика, rate против сырого значения, проверка up и времени старта процесса.
Красный флаг: «откатим релиз», не посмотрев на график скорости.
6. [middle] Prometheus падает по OOM. Память растёт после вчерашнего релиза. Что делаешь?
Ответ
Подозреваю рост кардинальности: в метку попало что-то неограниченное. Смотрю число рядов и самые «тяжёлые» метрики: страница /status TSDB в UI или /api/v1/status/tsdb, запрос topk(10, count by (__name__)({__name__=~".+"})). Нахожу метрику и метку, откатываю или исправляю код, чтобы значения были из конечного списка. Временно поднимаю лимит памяти, но это не лечение.
Что хотят услышать: кардинальность, seriesCountByMetricName, связь с релизом, чистка на стороне приложения, а не «добавить памяти». Плюс упоминание, что можно ограничить ряды на цель параметром sample_limit.
Красный флаг: «увеличу память и retention», без поиска причины.
7. [middle] Алерт «ошибок больше порога» молчит, а сервис лежит. Почему?
Ответ
Если сервис не отвечает, ряды notes_http_requests_total перестают приходить, и после первого неудачного сбора Prometheus сразу помечает их устаревшими. Выражение ... > порога на пустом множестве ничего не возвращает, и алерт не срабатывает. Пропажу ловят отдельным алертом на up == 0 (или на absent()), это обязательная пара к алертам по значениям.
Что хотят услышать: staleness, пустой результат вместо нуля, up == 0, absent, for.
Красный флаг: «у алерта неправильный порог».
8. [middle] Какие корзины выбрать для гистограммы задержек и что будет, если выбрать плохо?
Ответ
Корзины должны покрывать реальные задержки и границы SLO (например, есть корзина ровно на пороге 0.5 с). Если все запросы по 20 мс, а первая корзина 100 мс, то p95 окажется где-то внутри неё, оценка будет грубой (квантиль вычисляется интерполяцией внутри корзины). Слишком много корзин раздувают число рядов: каждая корзина это ряд на каждую комбинацию меток.
Что хотят услышать: интерполяция внутри корзины, корзина на границе SLO, цена за каждую корзину, что менять корзины потом больно.
Красный флаг: «корзины любые, Prometheus разберётся».
9. [middle] Единственный Prometheus упал ночью, на графиках дыра. Как сделать надёжнее?
Ответ
Первый шаг: два одинаковых Prometheus, которые собирают одни и те же цели независимо (HA-пара), с постоянным диском и алертом на up самого Prometheus. Вторая проблема: данные у каждого свои, а графики надо смотреть с одного из них. Для долгого хранения и единого запроса добавляют Thanos, Mimir или VictoriaMetrics с записью в объектное хранилище (remote write). Начинаю с одного узла и разумным retention, пока он не упирается в объём.
Что хотят услышать: независимые реплики, а не кластер; персистентный том; внешнее хранилище для долгой истории; не усложнять раньше времени.
Красный флаг: «Prometheus кластеризуется из коробки».
10. [middle] [на скорость] Метрики CronJob пропадают, между запусками в Prometheus пусто. Что делать?
Ответ
Задача живёт слишком мало, поэтому её не успевают опросить. Решение: Pushgateway. Джоб перед завершением отправляет туда gauge last_success_timestamp и длительность, Prometheus забирает их у шлюза. Алерт строится на возрасте последнего успеха, а не на самой метрике.
Что хотят услышать: Pushgateway, метрику «время последнего успеха», оговорку про застывшие значения, алерт по возрасту.
Красный флаг: «уменьшим scrape_interval до секунды».
11. [middle] Аудит показал, что /metrics доступен всем из интернета через nginx. Это проблема?
Ответ
Да: там имена маршрутов, версии, счётчики, иногда внутренние адреса и состояние процесса. Это разведка для атакующего. Закрываю /metrics на прокси (отдельный location с deny или доступ только из сети мониторинга), а Prometheus продолжает ходить к приложению напрямую по внутренней сети.
Что хотят услышать: /metrics это внутренний интерфейс, закрытие на уровне прокси, отдельный порт или сеть, а не аутентификация «чтобы было».
Красный флаг: «метрики не секретные, пусть будут открыты».
12. [junior] Что такое job и instance и откуда они берутся?
Ответ
Prometheus сам добавляет к каждой собранной метрике два лейбла. job берётся из имени задачи в scrape_configs, instance - это адрес цели в виде хост:порт. По ним можно выделить один сервис или один конкретный под. Отдельно Prometheus для каждой цели записывает метрику up: 1, если сбор прошёл, 0, если нет. Поэтому в PromQL я начинаю диагностику с up == 0 и смотрю, у каких job и instance сбор сломан.
Что хотят услышать: job из scrape config, instance как хост:порт, метрика up, использование для диагностики.
Красный флаг: «лейблы job и instance пишет приложение в /metrics».
13. [middle] Сколько Prometheus хранит данные и что делать, если нужен год истории?
Ответ
Данные лежат в локальной TSDB на диске, по умолчанию хранятся 15 дней, срок меняет флаг --storage.tsdb.retention.time, а объём ограничивает --storage.tsdb.retention.size. Локальное хранилище не рассчитано на годы и на отказоустойчивость. Для долгой истории я настраиваю remote_write в отдельное хранилище: Thanos, Mimir, VictoriaMetrics или похожее. Чаще всего для графиков за год хватает агрегатов recording rules, а не сырых данных. Диск под Prometheus я считаю заранее: число рядов, интервал сбора и срок хранения.
Что хотят услышать: локальная TSDB, 15 дней по умолчанию, флаги retention, remote_write для долгого хранения, планирование диска.
Красный флаг: просто поставить срок хранения в 5 лет на маленьком диске.
Проверено на версиях
- Задание 1 (
demo.py) запущено сprometheus_client0.26.0 на Python 3.9 (macOS): вывод в уроке взят из прогона, заголовки# HELPи# TYPEдля клиента 0.26.0 тоже. promtool check configвыполнен наprom/prometheus:v3.15.0дляprometheus.ymlиз задания 2: конфиг валиден.- Prometheus v3.15.0, node_exporter v1.12.1, cAdvisor v0.60.6: образы в этом уроке не запускались,
compose.ymlне прогонялся, выводtargetsи запросов в заданиях 2 и 3 реалистичный, но не снят с настоящего стенда. app.pyv5 приведён к эталонуproject/notes/versions/v5.py(фрагменты уроков сверены с ним), в контейнере не запускался.- Скрипт поломок
break/8.2/break.sh: проверенshellcheck, не запускался. - Python: 3.12 в Ubuntu 24.04 и 3.14 в 26.04 (проверено для клиента только на 3.9), Docker Compose без ключа
version:.
Итог урока: ты умеешь
- умею объяснить, почему Prometheus собирает метрики сам (pull) и что означает
up - умею прочитать
/metricsруками и разобрать# HELP,# TYPE, корзины гистограммы - умею отличить counter, gauge и histogram и выбрать тип под задачу
- умею поднять Prometheus, node_exporter и cAdvisor в Compose и подключить их к сети «Заметок»
- умею проверить конфиг через
promtool check configи поlastErrorнайти причину DOWN - умею добавить
/metricsв приложение с ограниченным набором значений меток - умею находить взрыв кардинальности по числу рядов и
status/tsdb
Дальше: Урок 8.3: PromQL: rate, агрегации, квантили
Проверь себя
Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.
Тест работает с включённым JavaScript.