мониторинг и SRE Все курсы

✻ Урок 3.1 · Тема 3: Дашборды и алерты

Grafana: дашборды RED и USE как код

⏱ 3 ч

Зачем это нужно

Середина первой недели в магазине. Метрики уже собираются, запросы к ним ты писать умеешь, но числа лежат в консоли Prometheus, и показать их менеджеру нельзя. Он просит одно: «Повесим экран на стену перед распродажей. Хочу глянуть и понять, всё ли хорошо». Я сижу рядом как опытный коллега и скажу честно: за фразой «глянуть и понять» прячется полдня работы.

Расскажу, как я сам однажды собрал дашборд (dashboard: страница с графиками, на которой всё о сервисе видно сразу). Сорок панелей (панель: один график или число на странице), красиво, у каждой своя палитра. На разборе меня спросили: «Покажи, где тут видно, что оплата тормозит». Я искал минуту и сорок секунд. За это время покупатель, у которого висел платёж, закрыл вкладку. Дашборд был полон правды, но не отвечал ни на один вопрос быстро. С тех пор я проверяю свои страницы так: человек, которого разбудили среди ночи, за десять секунд должен сказать, горит или нет, а потом за минуту понять, где.

Сегодня ты научишься делать такие страницы в Grafana. Сначала поймёшь, что на первом экране должно быть, а чего нет. Потом соберёшь два дашборда: RED (что чувствует покупатель) и USE (что творится с ресурсами). И главное, сохранишь их файлами, а не мышиными кликами: дашборд, которого нет в git, пропадёт вместе со стендом.

Шаг проекта: в ~/monitoring-lab/03-dashboards-alerts/dashboards лежат два файла, lab-red.json и lab-use.json, и скрипты, которые загружают их в Grafana и выгружают обратно. Всё это лежит в git.

Что нужно знать

  • Три сигнала, RED и USE: урок 1.1. Цели по качеству и p95: урок 1.2.
  • Метрики и Prometheus, метки, типы метрик: урок 2.1. rate, sum by, histogram_quantile: урок 2.2. Откуда берутся метрики CPU, памяти и базы: урок 2.3.
  • Стенд с профилем monitoring запущен, Grafana открывается на http://localhost:3000. Скрипт трафика traffic.sh из урока 1.2, практика 1.
  • curl, jq, git: load-tester, урок 1.1 и урок 3.1.

Картина целиком

Представь приборную панель самолёта. Пилот не читает все датчики подряд. Сверху главные приборы: высота, скорость, курс. Взгляд скользнул, и ясно, что полёт идёт нормально. Ниже вспомогательные, на которые смотрят, когда главные что-то сказали. Дашборд устроен так же. Сверху то, что чувствует покупатель (RED), ниже то, что происходит с ресурсами (USE). А сама «панель» собирается по чертежу: чертёж это файл, и по нему приборную панель можно собрать заново.

flowchart TD
    Q["Вопрос:<br>покупателю хорошо?"] --> R["RED: трафик,<br>ошибки, задержка"]
    R --> U["USE: что с ресурсом?<br>загрузка, очередь, ошибки"]
    U --> J["JSON-файл<br>в git"]
    J --> G["Grafana<br>через HTTP API"]
    G --> Q

Схема замкнута. Дашборд отвечает на вопрос, на него смотрят, правят, выгружают в файл и снова загружают. Ниже мы пройдём по каждому шагу: что класть на экран, как устроена панель, как не писать один и тот же дашборд на каждый маршрут и как не потерять результат.

Теория

Дашборд отвечает на вопрос, а не показывает всё

Открываешь незнакомый дашборд: сорок графиков, у каждого своя шкала. Что с магазином? Непонятно. Проблема не в цифрах, а в том, что страница не ответила на вопрос.

У нормального дашборда есть первый экран, и он отвечает на три вопроса подряд. «Горит или нет?» Это видно за десять секунд: ошибки, нагрузка и задержка покупателя. «Где именно?» Это за минуту: по маршрутам, по ресурсам. «Почему?» Это уже ниже, в панелях с ресурсами. Если после десяти секунд ты не знаешь, горит ли, страница плохая, как бы красиво она ни выглядела.

Простое правило: на первом экране пять-восемь панелей. Откуда число? За десять секунд глаз окидывает примерно столько блоков: ты читаешь заголовок, смотришь на график и идёшь дальше. Двенадцатая панель уже не успеет попасть в этот взгляд, а сорок превращаются в стену. Порядок сверху вниз повторяет ход мысли дежурного: сначала симптомы, потом ресурсы. Каждая панель с названием, в котором есть единица или смысл: «Доля 5xx», а не «Панель 7».

Прикинь сам: на дашборде двенадцать графиков, и каждый вечер дежурный находит на нём проблему только через две-три минуты. Что ты сделаешь: добавишь ещё графиков или уберёшь лишние?

Уберёшь. Двенадцать панелей оставь, но сгруппируй: четыре главные наверху (трафик, ошибки, p95, запросы в работе), остальные ниже под заголовком «Ресурсы». Минуты уйдут не на поиск, а на чтение. Добавление новых графиков только удлиняет страницу.

Осторожно: путают дашборд и расследование. Дашборд показывает, что нужно знать всегда. Когда нужен редкий запрос «а что с этим пользователем», его делают в Explore (режим свободных запросов в Grafana, мы к нему вернёмся), а не добавляют панель «на всякий случай».

Главное: дашборд отвечает за десять секунд, горит ли, и за минуту, где; для этого на первом экране пять-восемь панелей в порядке «симптомы, потом ресурсы».

Чтобы собрать такую страницу, нужно понимать, из чего она состоит.

Grafana: источник данных, запрос и панель

Grafana сама ничего не измеряет и не хранит. Это окно, через которое ты смотришь в чужое хранилище. Как телевизор: показывает то, что ему передали по кабелю.

Кабель называется источником данных (data source): настроенное подключение, в котором записано, куда Grafana ходит за числами. На стенде их четыре, и они подключены заранее:

  • Prometheus хранит метрики;
  • Loki хранит логи;
  • Tempo хранит трейсы, то есть пути запросов через сервисы (подробно в теме 4);
  • Alertmanager рассылает тревоги (подробно в уроке 3.2).

Сегодня нужен только Prometheus. У каждого источника есть uid, уникальное имя: у Prometheus оно равно prometheus, и панели ссылаются на источник именно по нему.

Теперь панель (panel): это один график или число на странице. Устроена она просто: запрос к источнику плюс вид (линия, столбцы, число, таблица). Запрос на PromQL, который ты писал в теме 2, вставляется в панель без изменений.

Теперь пример на «Магазине». Возьми запрос sum by (route) (rate(http_requests_total[1m])). В консоли Prometheus он нарисует график, но без подписей осей и единиц. Если вставить тот же запрос в панель типа «Time series» с единицей requests/sec, получится «RPS по маршрутам»: по линии на каждый маршрут, подписи справа, значения при наведении мыши. Запрос тот же, а читать стало легче.

Подписи, единицы и сложенные линии делают из ответа Prometheus картину. Сначала смотри на единицу в углу и на легенду: без них число на оси ничего не значит.

Чтобы пробовать запросы, у Grafana есть режим Explore: пустая страница с одним запросом, без дашборда. Здесь ищут ошибку в запросе, пока не готова панель. Хороший порядок такой: сначала запрос в Explore, убедился, что цифры верные, потом переносишь его в панель.

Главное: Grafana только рисует, а числа берёт из источника данных; панель это запрос плюс вид, а запрос отлаживают в Explore.

Какие именно запросы класть на панели? Начнём с покупателя.

RED на дашборде: ошибки, трафик, задержка

Вспомни из урока 1.1: RED это три вопроса о сервисе глазами покупателя. Rate: сколько запросов в секунду. Errors: какая доля из них плохая. Duration: сколько они длятся. Каждому вопросу соответствует одна панель, и вместе они закрывают почти все жалобы покупателей.

Rate показывает, живёт ли магазин и идёт ли трафик: sum by (route) (rate(http_requests_total[$__rate_interval])). Падение до нуля при обычном дне тоже сигнал, а не «всё чисто». Errors это доля, а не штуки: (sum(rate(http_requests_total{status=~"5.."}[$__rate_interval])) or vector(0)) / clamp_min(sum(rate(http_requests_total[$__rate_interval])), 0.001). Почему доля? Двадцать ошибок в минуту при ста запросах это катастрофа, а при ста тысячах пустяк. Часть or vector(0) подставляет ноль, когда пятисотых нет совсем, иначе на графике «No data» вместо ровной линии. clamp_min(..., 0.001) не даёт делить на ноль, когда трафика нет.

Duration это квантили: histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[$__rate_interval]))). Рисуй минимум p50 и p95 на одной панели: p50 показывает типичного покупателя, p95 тех, кому хуже всего. Среднее на этой панели не нужно по причине, которую ты уже знаешь из 1.2: оно прячет хвост.

Один график запросов по маршрутам даёт ещё одну полезную картинку. Разброс задержек можно показать heatmap (тепловой картой): по горизонтали время, по вертикали корзины гистограммы, цвет это число запросов. Если p95 подозрительно скачет, на heatmap видно, выросли все запросы или у части появился «второй горб» медленных. Для первого экрана хватает линий.

Ниже живая модель дашборда RED/USE во время нагрузки. Наведи мышь на любой график: вертикальная линия появится на всех панелях в один момент, и причина свяжется со следствием.

На картинке сценарий «медленная оплата». Сначала p95 прыгает ступенькой, а число запросов в работе растёт: запросы стоят и ждут оплату. Позже заполняется пул, появляются очередь и ошибки. Ошибки приходят последними, поэтому читать панели надо вместе, а не по одной.

Прикинь сам: за минуту 600 запросов, из них 30 ответили 503. Какая доля на панели Errors? Что покажет панель, если в минуту был один запрос и он упал?

Тридцать на шестьсот: 5%. Один упавший из одного: 100%. Цифра честная, но на таком трафике бессмысленная. Поэтому у алертов, о которых мы поговорим в следующем уроке, всегда есть минимальный трафик, а на дашборде рядом стоит панель Rate: она подсказывает, что трафика почти нет и верить доле не стоит.

Первая пара карточек говорит «5% из шестисот», вторая «100% из одного»: на второй Errors красный, но Rate подсказывает, что за ним почти ничего нет. Всегда смотри на Rate рядом с долей.

Осторожно: status=~"5.." это регулярное выражение «код из трёх символов, первый 5». Опечатка status="5.." (с равно вместо =~) молча вернёт пустоту: такого кода нет. График будет ровным нулём, и тебе покажется, что ошибок нет.

Оранжевая линия видит сбой, синяя считает тот же трафик и показывает ровный ноль. Если бы ты открыл только синюю, ты бы решил, что ошибок нет: сначала сверь оператор =~ в запросе панели.

Главное: RED это три панели: Rate (запросов в секунду), Errors (доля, а не штуки), Duration (p50 и p95, не среднее); читать их надо вместе.

RED описывает покупателя, но не объясняет причину. Для причины смотрят на ресурсы.

USE на дашборде: загрузка, насыщение, ошибки ресурса

Сервис отвечает по пять секунд, а процессор показывает 12%. Где проблема? Не в процессоре. Значит, где-то очередь, а мы её не смотрим.

Представь кассу в магазине. Касса занята 100% времени, и очереди нет: покупатели подходят ровно как закончил предыдущий. Другая касса занята тоже на 100%, но у неё стоит очередь из десяти человек. Занятость одинаковая, а положение разное. Первое называют загрузкой (utilization): какую долю времени ресурс занят. Второе насыщением (saturation): сколько работы ждёт своей очереди. Третье: ошибки (errors) самого ресурса, например отказ выдать соединение. Метод USE (Utilization, Saturation, Errors) в том, чтобы для каждого ресурса посмотреть эти три вещи. Касса в этом сравнении перестаёт быть точной в одном: у кассы очередь видна глазами, а у ресурсов сервиса её приходится искать в метриках.

В «Магазине» ресурсы такие. Процессор хоста: загрузка 1 - avg(rate(node_cpu_seconds_total{mode="idle"}[$__rate_interval])). Процессор контейнера shop: sum(rate(container_cpu_usage_seconds_total{container_label_com_docker_compose_service="shop"}[$__rate_interval])), число ядер в работе. Память: sum(container_memory_working_set_bytes{container_label_com_docker_compose_service="shop"}). А вот пул соединений с базой: набор из пяти заранее открытых «линий» в PostgreSQL, и главный ресурс всего стенда. Загрузка пула это сколько линий занято: shop_db_pool_size - shop_db_pool_available. Насыщение это shop_db_pool_waiting: сколько запросов ждут свободную линию. Ошибки пула это ответы 503 с причиной «database pool timeout», и их видно на панели Errors из RED.

Теперь разберём на цифрах. Пул из пяти соединений, все пять заняты: загрузка 5 из 5, 100%. Ждут двое: насыщение 2. Загрузка 100% сама по себе ещё не беда (касса без очереди). Беда, когда waiting больше нуля: именно тогда запросы замедляются и в конце концов падают с 503. Поэтому для USE в первую очередь смотрят на насыщение: оно честнее загрузки.

Сценарий «медленная оплата» показывает самое неприятное: процессор и память в порядке, а пул забит. Если бы ты смотрел только на CPU, ты бы сказал «всё хорошо».

Зелёная линия упала в ноль, красная поднялась до трёх, а ожидание выросло с долей миллисекунды до пяти секунд, то есть до таймаута пула. Процессор при этом никуда не делся: беда видна только на панелях пула.

Прикинь сам: пул 5 соединений, занято 5, ждут 0. Процессор 12%, p95 нормальный. Нужно ли будить дежурного?

Нет. Пул загружен полностью, но очереди нет, а покупатели ничего не замечают. Это загрузка без насыщения. Если же waiting держится выше нуля минуту, будить пора: очередь значит, что запросы уже стоят.

Осторожно: «процессор загружен» не значит «процессор насыщен». Сто процентов без очереди задач ещё не беда, насыщение это очередь, а не проценты. На стенде очередь процессора отдельной метрикой не смотрим, поэтому для него хватит загрузки и задержки покупателя.

Главное: для каждого ресурса смотри загрузку, насыщение (очередь) и ошибки; очередь честнее процентов, а пул соединений тоже ресурс.

У нас два набора панелей, RED и USE. Теперь надо сделать так, чтобы дашборд не дублировался на каждый маршрут.

Переменные и окно rate: один дашборд на все маршруты

Менеджер просит: «А покажи только каталог». Копировать дашборд ради этого не надо. В Grafana есть переменная (variable): выпадающий список вверху страницы, значение которого подставляется в запросы. Переменная route берёт значения из метрик командой label_values(http_requests_total, route): Grafana сама спросит у Prometheus, какие бывают значения метки route. В запросе она записывается как $route.

Это и есть содержимое выпадающего списка «Маршрут»: Grafana спросила у Prometheus значения метки и ничего не придумывала. Выбранное значение попадает в запрос как есть.

Теперь самое тонкое. Если выбрано «All» или два маршрута, Grafana подставляет не слово, а регулярное выражение вроде (/api/products|/api/orders). Поэтому селектор должен быть route=~"$route": оператор =~ понимает регулярное выражение. С обычным = панель покажет «No data», потому что значения метки «/api/products» и текста «(/api/products /api/orders)» не совпадают буква в букву.

Вторая встроенная подстановка это $__rate_interval: окно для rate, которое Grafana подбирает сама. Зачем? Фиксированное [1m] ломается при увеличении масштаба. Если смотришь график за неделю, шаг точек вырастает до нескольких минут, а окно в минуту охватывает кусочек между точками, и график дырявый. Нужно окно, которое растёт вместе с шагом. Поэтому правило такое: $__rate_interval равен шагу точек плюс интервал опроса, но не меньше четырёх интервалов опроса. На стенде опрос раз в 5 секунд, значит окно не короче 20 секунд, и на любом масштабе в окне найдётся не меньше двух замеров.

Прикинь сам: опрос раз в 5 секунд, ты смотришь график за час, шаг точек 15 секунд. Какое примерно окно подставит $__rate_interval?

Шаг 15 секунд плюс интервал опроса 5 секунд: 20 секунд. Четыре интервала опроса тоже 20 секунд. Берётся большее, то есть 20 секунд (цифра приблизительная, как Grafana посчитает на твоём экране, смотри в панели Query inspector).

Осторожно: в переменной «All» по умолчанию подставляются все значения, которые были на момент загрузки. Если появится новый маршрут, а список старый, refresh у переменной должен быть «On time range change» или «On dashboard load», иначе новый маршрут не появится.

Главное: переменная заменяет копии дашборда, но с ней в селекторе пиши =~, а окно для rate бери $__rate_interval.

Ссылки и аннотации: дашборд внутри расследования

Дашборд редко живёт сам по себе. Человека к нему приводит тревога, а от него он идёт дальше: в логи, в трейсы, к инструкции. Если эти переходы нужно искать руками, десять секунд на ответ превращаются в десять минут. Поэтому хорошая панель знает, куда вести дальше.

Первое средство: ссылка на панели (data link). Ты задаёшь адрес, в который подставляется значение выбранной линии или переменная. Например, клик по линии маршрута /api/orders открывает дашборд логов уже с этим маршрутом. Второе средство: ссылки дашборда (dashboard links) в шапке: на runbook (инструкцию дежурному, подробно в уроке 3.2), на соседний дашборд, на карточку сервиса. Дежурный видит их сразу, без поиска по закладкам.

Третье средство: аннотации (annotations). Это вертикальные метки на всех графиках сразу: «в 14:05 выкатили версию 2.3», «в 14:32 сработал алерт». Они отвечают на самый частый вопрос разбора: «А что менялось перед тем, как стало плохо?» Без меток ты гадаешь по памяти, с метками видишь, что p95 вырос через минуту после выкатки.

Пример на цифрах. p95 оплаты подскочил с 0,2 до 1,8 с в 14:07. Метка выкатки стоит на 14:05. Два пункта на оси, две минуты разницы: подозреваем выкатку и проверяем её первой. Без метки то же самое занимает минут пятнадцать: опрос в чате и поиск в журнале деплоев.

Метки не обязательно ставить руками. Скрипт выкатки сам отправляет метку в Grafana запросом POST /api/annotations (текст, время, теги), а метки из алертов Grafana рисует сама. Договорись о тегах заранее: deploy для выкаток, incident для начала и конца инцидента, config для правок настроек вроде delay_ms. Тогда по тегу можно показать на дашборде только выкатки или только инциденты. Для разбора это самый дешёвый способ оставить след: через месяц по меткам видно, что и когда менялось, без поиска в чатах.

Осторожно: метка показывает совпадение во времени, а не причину. Выкатка и сбой могли совпасть случайно, и вывод надо проверить по логам и трейсам.

Ещё одна деталь, которая выручает ночью: обновление и окно времени. На дежурстве ставь окно «последние 3 часа» и автообновление раз в 30 секунд. Окно в 30 дней сглаживает минутную беду до незаметной точки, а окно в пять минут не показывает, как всё начиналось. Три часа обычно хватает, чтобы увидеть и начало беды, и текущее состояние. Для разбора задним числом окно растягивают вручную.

Главное: ссылки ведут от симптома к деталям, аннотации отмечают события на всех графиках; вместе они сокращают путь от «плохо» к «почему».

Красная панель должна подсказывать действие

Ночью дежурный открывает дашборд и видит красную панель «p95 задержки». Красная, и что дальше? Если ответ знает только автор дашборда, а он спит, панель громко сообщает о беде и ничем не помогает. Это как табло «Авария» в машине без инструкции: тревожно, но руки не знают, что делать.

Поэтому у дашборда есть ещё одно требование: дашборд ведёт к действию (actionable dashboard). У каждой панели, которая может покраснеть, есть ответ на вопрос «что делать первым». Для этого хватает трёх средств. Первое: описание панели (description в настройках): текст, который всплывает по значку «i» в углу, например «p95 выше 0,5 с: открой на lab-use панель “Ждут в очереди”; если там больше нуля, это runbook LabDbPoolQueue». Второе: ссылка на runbook или в Explore (мы разобрали её выше). Третье: порядок панелей: симптом стоит рядом с панелью, которая объясняет его причину, а не на другом конце страницы.

flowchart TD
    A["p95 красный<br>(выше 0,5 с)"] --> B{"Ждут в очереди<br>пула больше 0?"}
    B -->|да| C["Runbook LabDbPoolQueue,<br>шаг 1: кто держит соединения"]
    B -->|нет| D["Explore: логи<br>медленных запросов"]

Схема это то, что дежурный должен прочитать с красной панели, не думая: одно условие и два пути. На ней нет ни одного «посмотри, что там», каждая ветка заканчивается конкретным местом.

Подписи со стрелками на карточках это те самые подсказки. Красные p95, ошибки и занятость пула говорят «горит», а оранжевая очередь (три запроса сверх пяти соединений) указывает, откуда начать.

Пример на цифрах. Восемь покупателей оформляют заказы, пул даёт пять соединений: три заказа всегда ждут. Если на панели «Ждут в очереди» стоит 3 и в её описании написано «больше нуля: оплата держит соединения, смотри runbook», дежурный идёт к инструкции за секунды. Без описания он тратит минуты на вопрос «а это вообще плохо?».

Прикинь сам: на дашборде есть панель «Число потоков в сервисе» с красным порогом. Что делать с ней ночью?

Ничего разумного: ни действия, ни понятной нормы. Такая панель не должна быть красной на общем дашборде. Либо ей пишут описание с нормой и шагом, либо переносят в «Ресурсы» без порога, либо убирают: смотреть её можно в Explore, когда понадобится. Правило проверки простое: для каждой панели с порогом спроси «что сделает человек, который видит её красной в три часа ночи и не знает систему?». Нет ответа, значит панель не готова.

Осторожно: описание не заменяет runbook. В нём одна-две строки: что значит и куда идти. Шесть вопросов инструкции мы разберём в уроке 3.2.

Главное: у каждой панели, которая может стать красной, должен быть ответ «что делать первым»: описание, ссылка на runbook и соседство с панелью-причиной.

Если панель и ведёт к действию, её всё равно можно испортить. Как это происходит, посмотрим на пяти частых ошибках.

Чем дашборд портят: пять частых ошибок

Я видел много дашбордов, которые выглядели старательно и не помогали. Причины повторяются, и их проще знать заранее, чем находить во время аварии.

Слишком много панелей. Сорок графиков равны отсутствию ответа: глаз не знает, куда идти. Держи на первом экране пять-восемь панелей, остальное убери в свёрнутые строки (row) ниже.

Среднее вместо перцентиля. Средняя задержка скрывает медленный хвост: девяносто девять быстрых запросов вытащат среднее, а один покупатель ждёт десять секунд. Рисуй p95 (урок 1.2).

Штуки вместо долей. «Двенадцать ошибок» при трёх запросах в минуту катастрофа, при трёх тысячах пустяк. Для ошибок рисуй долю и рядом трафик.

Нет единиц и порогов. Число «0,43» без подписи читатель понимает по-разному: секунды, доля, штуки.

Разные оси без причины. Если у двух похожих панелей разный верх оси, они визуально врут: одинаковые линии означают разные значения. Для сравнимых панелей задавай одинаковые границы или явно подписывай.

Прикинь сам: на дашборде одна панель, «ошибок за минуту: 12», зелёная. Хорошо ли это?

Без трафика не понять. Если запросов 3000 в минуту, ошибок 0,4%, и это обычный день. Если запросов три, 12 ошибок значат, что магазин почти не отвечает. Поэтому рядом с ошибками всегда стоит доля и Rate.

Хороший признак зрелого дашборда: его читает тот, кто его не делал. Дай экран коллеге без объяснений и спроси «горит ли?». Если он отвечает за десять секунд, дашборд удался. Если спрашивает, что значит третья панель, подпись и единица на ней неудачные.

Главное: первый экран короткий, задержку рисуют перцентилем, ошибки долей, единицы подписаны, оси честные.

Линии и данные есть. Осталось сделать так, чтобы взгляд сам цеплялся за плохое.

Единицы, пороги и общее перекрестие

Смотреть на дашборд приходится боковым зрением. Если число «0,43» не отличается от «0,12», нужно думать, а на это нет времени. Три настройки делают панель читаемой.

Единицы (unit). Метрика отдаёт секунды, а ты привык читать «430 мс». Укажи для панели единицу seconds (s), и Grafana сама покажет «430 ms». Для доли выбери Percent (0.0-1.0) (в файле это percentunit, 0,05 станет «5%»), для запросов в секунду requests/sec (rps), для байтов bytes (IEC). Без единицы на панели просто «0,43», и читатель не знает, секунды это или процент.

Пороги (thresholds) красят график или число, когда оно переходит границу: базовый цвет зелёный, жёлтый от 0,3 с, красный от 0,5 с. Пунктирная линия на графике показывает, где граница. Порог ничего не отправляет и никого не будит: он только краска. Оповещения это отдельная тема следующего урока.

Общее перекрестие (shared crosshair). В настройках дашборда поле Graph tooltip, значение Shared crosshair. Когда наводишь мышь на одну панель, вертикальная линия появляется на всех, в одно и то же время. Эту настройку ты видел в виджете выше. Цепочка «в 14:32:10 вырос p95, в ту же секунду очередь пула стала шесть» связывается сама.

Пример на цифрах. Цель из урока 1.2: p95 каталога не выше 300 мс. Ставим жёлтый порог 0,3 с и красный 0,5 с. Ступенчатый тест даёт p95 по ступеням: 0,08, 0,12, 0,25, 0,48, 0,95. Четвёртая точка уже жёлтая, последняя красная, и ответ на вопрос «на какой ступени мы нарушили цель» виден глазами.

Пунктиры показывают границы, и ответ виден глазом: цель нарушена на четвёртой ступени, где линия впервые выше жёлтого.

Отдельно о цветах. Красный нужен для одного: «так нельзя». Если на экране красного много, глаз перестаёт его замечать, как и звонок, который звонит каждый час. Поэтому пороги ставят по цели из урока 1.2, а не «чтобы было красиво», и красным красят только то, на что человек должен среагировать. Остальное остаётся спокойным: синий, серый, зелёный. Тогда красная панель среди спокойных читается без подписи, и дежурный находит её боковым зрением.

Проверить цвета можно просто: распечатай экран в чёрно-белом или попроси коллегу с нарушенным цветовым зрением взглянуть на него. Если пороги читаются только по цвету, добавь пунктирную линию границы и подпись. Тогда дашборд понятен всем, а не только тем, кто видит красный и зелёный одинаково.

Главное: единицы делают числа читаемыми, пороги красят границу без оповещений, общее перекрестие связывает причину и следствие в одну секунду.

Честные метрики: зелёный экран при сломанном пути покупателя

Представь охранника на проходной магазина. Он проверяет одно: дверь открыта, свет горит. Ставит зелёную галочку и идёт пить чай. А в зале не работает касса, и покупатели уходят с полными корзинами. По его журналу день прошёл идеально.

Так же ведут себя метрики, которые измеряют «живость» процесса, а не результат для покупателя. Такие метрики называют нечестными: они не врут по цифрам, но создают ложное чувство безопасности. На стенде это видно сразу. Метрика up равна 1, пока Prometheus получает ответ от /metrics. Адрес /healthz магазина отвечает {"status":"ok"}, пока жив процесс: в базу и в оплату он не заглядывает. Включи замедление оплаты на пять секунд, и обе проверки останутся зелёными, а заказы начнут падать с 503.

Зелёная линия up лежит на ста процентах всю дорогу, а красная после 14:06 опускается к 94 и ниже цели в 99. Дашборд, на котором была бы только первая, показал бы «всё хорошо» ровно тогда, когда покупателям плохо.

Честный дашборд подчиняется трём правилам. Первое: измеряй путь покупателя, а не процесс. Доля успешных ответов на /api/orders, p95 заказа и проба, которая раз в минуту проходит путь «вошёл, положил в корзину, оформил» целиком (синтетическая проверка, blackbox из урока 2.3), честнее, чем «процесс жив». Проверка живости нужна, но как причина для расследования, а не как итог. Второе: считай все ошибки, которые видит покупатель. Не только 5xx: отказы входа 401 и 403 из-за сбоя провайдера авторизации, таймауты и пустые ответы тоже поломка с его точки зрения (подробнее про SLI в уроке 1.2). Третье: «нет данных» не значит «зелёный». Если метрика пропала, панель молчит, и это легко принять за тишину. Подпиши такое состояние явно (у панели Stat есть поле No value), а пропажу ловят правилом на absent() и up == 0.

Прикинь сам: все панели зелёные: up равен 1, /healthz отвечает 200, процессор 12%. А поддержка пишет, что заказ не оформляется. Чего на дашборде не хватает?

Не хватает панелей результата: доли успешных ответов на заказах, задержки заказа и, лучше всего, пробы, которая проходит путь покупателя. Всё, что есть на дашборде, измеряет здоровье серверов, а жалоба про заказ. Эти две вещи расходятся именно в самые плохие минуты.

Осторожно: честность стоит дорого. Проба ходит в боевой путь, и ей нужен тестовый покупатель, чтобы не плодить настоящие заказы. Начни с доли успешных ответов по ключевому маршруту: она бесплатна, метрика уже есть.

Главное: зелёная проверка живости не доказывает, что покупателю хорошо; меряй результат (доля успешных заказов, p95), считай все ошибки покупателя и не путай «нет данных» с «всё хорошо».

Дашборд готов и работает. Но час работы мышью исчезнет с первым docker compose down -v.

Дашборд как код: JSON в git

Ты час собирал дашборд мышью, а потом пересоздал стенд. Том с данными Grafana удалился, и страница пропала. Хранить результат надо там, где он переживёт стенд: в файле и в git. Это называют «дашборд как код» (dashboards as code): файл можно проверить глазами, сравнить с прошлой версией, откатить и перенести на другой компьютер.

Внутри Grafana дашборд это один документ в формате JSON (текст из пар «ключ: значение» с вложенностью). В нём заголовок, переменные и список панелей. У каждой панели есть запросы и место на странице (gridPos, подробности в практике). Выгружать и загружать дашборд можно через HTTP API Grafana, как это делается, покажу в практике.

Эталонный дашборд стенда shop-overview приходит другим путём: Grafana читает его из каталога на диске (это называют provisioning, настройки раздаются файлами: Grafana берёт их из файлов при старте, а не из кликов в интерфейсе). Файл остаётся источником правды, и при его изменении Grafana загружает версию из файла. Поэтому свой дашборд ты делаешь под другим uid, например lab-red. uid это имя, по которому дашборд находят в адресе и API. Есть ещё числовой id: он принадлежит конкретной базе Grafana, и при загрузке в другую его нужно обнулить (id: null).

Правильный цикл такой: правишь мышью, выгружаешь в файл, коммитишь. Файл обновился, история сохранилась. Если Grafana потеряла дашборд, ты загружаешь его скриптом за секунду.

Осторожно: путают «Save dashboard» в интерфейсе и git. Кнопка сохраняет дашборд только в базу Grafana. В git ничего не попадает, пока ты не выгрузил и не закоммитил.

Главное: дашборд это JSON; храни его в git, загружай и выгружай через API, свой делай под другим uid, чем у эталона.

Теперь соберём всё это руками. Прежде вернёмся к менеджеру: после практики у тебя будет экран, глядя на который за десять секунд видно, горит магазин или нет.

Практика

Файлы кладём в ~/monitoring-lab/03-dashboards-alerts. Нагрузку и поломки делаем только на своём локальном стенде. Стенд запущен с профилем monitoring (урок 1.1). Все числа в блоках «Что должно получиться» это пример: у тебя они будут другими.

1. Открой эталонный дашборд и найди в нём RED и USE

Подготовим папку и запустим фоновый трафик:

mkdir -p ~/monitoring-lab/03-dashboards-alerts/{dashboards,scripts}
cd ~/monitoring-lab/01-basics && ./traffic.sh 600 > /dev/null 2>&1 &

Второй командой traffic.sh из урока 1.2 уходит в фон (&): он будет крутить каталог, карточки и заказы несколько минут. Открой http://localhost:3000/d/shop-overview. Это эталонный дашборд «Магазин: обзор (эталон)». Вход не нужен: на стенде включён анонимный доступ с правами администратора, и это допустимо только на локальном стенде.

Проверь источник данных: в меню Connections открой Data sources, выбери Prometheus и внизу нажми Save & test. Должно появиться зелёное сообщение, что запрос к API Prometheus прошёл.

Теперь запиши в ~/monitoring-lab/03-dashboards-alerts/dashboard-notes.md: какая из двенадцати панелей отвечает за R, какая за E, какая за D, какие панели относятся к USE и чем «Пул БД» отличается от «Ожидания соединения БД». Подсказка: меню панели (три точки), пункт Edit, внизу виден запрос.

Что должно получиться: на «RPS по маршруту» несколько линий, у каждой от долей до пары запросов в секунду, «Доля HTTP 5xx» около нуля, на «Пул БД» три линии: размер пула, свободные и ожидающие. Ожидающих почти всегда ноль.

Как читать вывод: по картинке видно, что стенд живой: трафик идёт, ошибок нет, пул не ждёт. Это и есть «норма» (базовая линия), с которой ты будешь сравнивать поломку в практике 5.

Типичные ошибки:

  • «No data» на всех панелях: Prometheus недоступен или пуст. Проверь curl -s localhost:9090/-/ready, затем Status, Targets в интерфейсе Prometheus.
  • Пустая только панель «CPU контейнера shop»: на Docker Desktop у cAdvisor метки контейнеров бывают другими. Для урока это допустимо.

2. Скрипты загрузки и выгрузки

Два маленьких скрипта обслуживают весь цикл «правишь мышью, выгружаешь в файл». Они ходят в HTTP API Grafana (запросы по сети вместо кликов). На стенде анонимный вход получает роль Admin, поэтому токен не нужен (так безопасно только на локальном стенде). Хватает двух операций: GET /api/dashboards/uid/<uid> возвращает дашборд, POST /api/dashboards/db создаёт или обновляет его.

cd ~/monitoring-lab/03-dashboards-alerts/scripts
cat > dash-import.sh <<'EOF'
#!/usr/bin/env bash
# dash-import.sh файл.json: создать или обновить дашборд в локальной Grafana
set -euo pipefail
jq '{dashboard: (.id = null), overwrite: true}' "$1" \
  | curl -s -X POST localhost:3000/api/dashboards/db \
      -H 'Content-Type: application/json' -d @- \
  | jq -r '"\(.status // .message)  http://localhost:3000\(.url // "")"'
EOF
cat > dash-export.sh <<'EOF'
#!/usr/bin/env bash
# dash-export.sh uid > файл.json: выгрузить дашборд из локальной Grafana
set -euo pipefail
curl -fsS "localhost:3000/api/dashboards/uid/$1" | jq -e '.dashboard | .id = null | del(.version)'
EOF
chmod +x dash-*.sh

Разберём. cat > файл <<'EOF' ... EOF записывает всё между метками в файл, одинарные кавычки вокруг EOF запрещают оболочке подставлять переменные заранее. В dash-import.sh фильтр jq упаковывает файл в тело запроса: {"dashboard": <файл с id=null>, "overwrite": true}. Труба | отдаёт его в curl, где -d @- значит «тело прочитай из стандартного ввода». Последний jq печатает статус и адрес. В dash-export.sh из ответа API берётся только поле dashboard, обнуляется id и убирается version: эти поля принадлежат конкретной базе. Ключ -f у curl заставляет его завершиться с ошибкой на ответе 404 или 401, а -e у jq даёт ненулевой код, если результат null: тогда && mv ниже не выполнится и рабочий файл не затрётся.

Что должно получиться: команда ничего не печатает. Проверь, что файлы на месте: ls ~/monitoring-lab/03-dashboards-alerts/scripts покажет dash-export.sh dash-import.sh.

Типичные ошибки:

  • Permission denied при запуске: не сделал chmod +x.
  • curl: (7) Failed to connect to localhost port 3000: Grafana не запущена, проверь docker compose --profile monitoring ps в ~/learning/load-tester/project/shop.

3. Дашборд lab-red как файл

Создай файл. Это RED: переменная маршрута и четыре панели (трафик, ошибки, задержка, запросы в работе), пороги на p95 и общее перекрестие.

Что встретится в файле:

  • panels: список панелей дашборда; type у панели: вид графика (линии, число и так далее).
  • targets и expr: запросы панели, в expr лежит текст запроса PromQL.
  • legendFormat: подпись линии на графике.
  • fieldConfig.defaults.unit: единица измерения (секунды, проценты).
  • gridPos: место панели на сетке шириной 24 колонки: x и y задают угол, w и h ширину и высоту. Ширина 12 занимает половину экрана, две подряд дают строку.
  • route=~"$route": =~ сравнивает метку с шаблоном, а не буква в букву; так работает и «All», и выбор нескольких маршрутов (разбирали в разделе про переменные).
  • or vector(0): подставляет ноль, когда ошибок нет совсем. Пустая панель «No data» выглядит как поломка мониторинга, а ноль честно говорит «ошибок нет».
cat > ~/monitoring-lab/03-dashboards-alerts/dashboards/lab-red.json <<'EOF'
{
  "uid": "lab-red",
  "title": "Моя лаборатория: RED",
  "tags": ["lab"],
  "schemaVersion": 39,
  "refresh": "5s",
  "graphTooltip": 1,
  "time": { "from": "now-15m", "to": "now" },
  "templating": {
    "list": [
      {
        "name": "route",
        "label": "Маршрут",
        "type": "query",
        "datasource": { "type": "prometheus", "uid": "prometheus" },
        "query": { "query": "label_values(http_requests_total, route)", "refId": "route" },
        "refresh": 2,
        "multi": true,
        "includeAll": true,
        "current": { "text": "All", "value": "$__all" }
      }
    ]
  },
  "panels": [
    {
      "id": 1, "type": "timeseries", "title": "Rate: запросов в секунду",
      "gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 },
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "fieldConfig": { "defaults": { "unit": "reqps" }, "overrides": [] },
      "targets": [
        { "refId": "A", "legendFormat": "{{route}}",
          "expr": "sum by (route) (rate(http_requests_total{route=~\"$route\"}[$__rate_interval]))" }
      ]
    },
    {
      "id": 2, "type": "timeseries", "title": "Errors: доля 5xx",
      "gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 },
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "fieldConfig": { "defaults": { "unit": "percentunit", "min": 0 }, "overrides": [] },
      "targets": [
        { "refId": "A", "legendFormat": "5xx",
          "expr": "(sum(rate(http_requests_total{route=~\"$route\",status=~\"5..\"}[$__rate_interval])) or vector(0)) / clamp_min(sum(rate(http_requests_total{route=~\"$route\"}[$__rate_interval])), 0.001)" }
      ]
    },
    {
      "id": 3, "type": "timeseries", "title": "Duration: p50 и p95",
      "gridPos": { "x": 0, "y": 8, "w": 12, "h": 8 },
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "fieldConfig": {
        "defaults": {
          "unit": "s", "min": 0,
          "thresholds": { "mode": "absolute", "steps": [
            { "color": "green", "value": null },
            { "color": "yellow", "value": 0.3 },
            { "color": "red", "value": 0.5 } ] },
          "custom": { "thresholdsStyle": { "mode": "line" } }
        },
        "overrides": []
      },
      "targets": [
        { "refId": "A", "legendFormat": "p50",
          "expr": "histogram_quantile(0.5, sum by (le) (rate(http_request_duration_seconds_bucket{route=~\"$route\"}[$__rate_interval])))" },
        { "refId": "B", "legendFormat": "p95",
          "expr": "histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket{route=~\"$route\"}[$__rate_interval])))" }
      ]
    },
    {
      "id": 4, "type": "timeseries", "title": "Запросы в работе",
      "gridPos": { "x": 12, "y": 8, "w": 12, "h": 8 },
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "fieldConfig": { "defaults": { "unit": "short", "min": 0 }, "overrides": [] },
      "targets": [
        { "refId": "A", "legendFormat": "в работе", "expr": "sum(http_requests_in_progress)" }
      ]
    }
  ]
}
EOF

Проверь, что JSON корректный, и загрузи дашборд:

jq -e '.panels | length' ~/monitoring-lab/03-dashboards-alerts/dashboards/lab-red.json
~/monitoring-lab/03-dashboards-alerts/scripts/dash-import.sh ~/monitoring-lab/03-dashboards-alerts/dashboards/lab-red.json

Устройство файла. uid это имя дашборда, у эталона оно другое. graphTooltip: 1 включает общее перекрестие. templating.list описывает переменную route. У каждой панели есть datasource с uid: prometheus. Порог в панели Duration задан двумя ступенями поверх зелёного. Фигурные скобки {{route}} в legendFormat это шаблон Grafana: он подставит имя маршрута.

Что должно получиться (адрес пример):

4
success  http://localhost:3000/d/lab-red/moya-laboratoriya-red

Как читать вывод: 4 это число панелей, jq -e падает, если JSON сломан. success значит, что Grafana приняла дашборд. Открой адрес: четыре панели с живыми данными, вверху список «Маршрут».

Типичные ошибки:

  • parse error: Expected separator between values at line 12, column 9: пропущена запятая или кавычка около названной строки.
  • Панели пусты, в углу красный треугольник: наведи мышь, там текст ошибки. Чаще всего опечатка в запросе или другой datasource.
  • Нет ответа success, а есть сообщение вроде invalid dashboard UID: в файле лишняя обёртка dashboard/meta от ручной выгрузки. jq на такой файл не упадёт, а Grafana откажет. Достань содержимое: jq .dashboard.

4. Дашборд lab-use: ресурсы

Теперь второй дашборд, USE. Здесь нет переменных, зато пул соединений получает две линии: занято и очередь.

cat > ~/monitoring-lab/03-dashboards-alerts/dashboards/lab-use.json <<'EOF'
{
  "uid": "lab-use",
  "title": "Моя лаборатория: USE",
  "tags": ["lab"],
  "schemaVersion": 39,
  "refresh": "5s",
  "graphTooltip": 1,
  "time": { "from": "now-15m", "to": "now" },
  "panels": [
    {
      "id": 1, "type": "timeseries", "title": "Пул БД: занято и очередь",
      "gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 },
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "fieldConfig": { "defaults": { "unit": "short", "min": 0 }, "overrides": [] },
      "targets": [
        { "refId": "A", "legendFormat": "занято", "expr": "shop_db_pool_size - shop_db_pool_available" },
        { "refId": "B", "legendFormat": "ждут (очередь)", "expr": "shop_db_pool_waiting" }
      ]
    },
    {
      "id": 2, "type": "timeseries", "title": "Ожидание соединения БД: p95",
      "gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 },
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "fieldConfig": { "defaults": { "unit": "s", "min": 0 }, "overrides": [] },
      "targets": [
        { "refId": "A", "legendFormat": "p95",
          "expr": "histogram_quantile(0.95, sum by (le) (rate(shop_db_connection_wait_seconds_bucket[$__rate_interval])))" }
      ]
    },
    {
      "id": 3, "type": "timeseries", "title": "CPU хоста: загрузка",
      "gridPos": { "x": 0, "y": 8, "w": 8, "h": 8 },
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "fieldConfig": { "defaults": { "unit": "percentunit", "min": 0, "max": 1 }, "overrides": [] },
      "targets": [
        { "refId": "A", "legendFormat": "хост",
          "expr": "1 - avg(rate(node_cpu_seconds_total{mode=\"idle\"}[$__rate_interval]))" }
      ]
    },
    {
      "id": 4, "type": "timeseries", "title": "CPU контейнера shop, ядра",
      "gridPos": { "x": 8, "y": 8, "w": 8, "h": 8 },
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "fieldConfig": { "defaults": { "unit": "short", "min": 0 }, "overrides": [] },
      "targets": [
        { "refId": "A", "legendFormat": "shop",
          "expr": "sum(rate(container_cpu_usage_seconds_total{container_label_com_docker_compose_service=\"shop\"}[$__rate_interval]))" }
      ]
    },
    {
      "id": 5, "type": "timeseries", "title": "Память контейнера shop",
      "gridPos": { "x": 16, "y": 8, "w": 8, "h": 8 },
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "fieldConfig": { "defaults": { "unit": "bytes", "min": 0 }, "overrides": [] },
      "targets": [
        { "refId": "A", "legendFormat": "shop",
          "expr": "sum(container_memory_working_set_bytes{container_label_com_docker_compose_service=\"shop\"})" }
      ]
    },
    {
      "id": 6, "type": "timeseries", "title": "Соединения PostgreSQL",
      "gridPos": { "x": 0, "y": 16, "w": 12, "h": 8 },
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "fieldConfig": { "defaults": { "unit": "short", "min": 0 }, "overrides": [] },
      "targets": [
        { "refId": "A", "legendFormat": "numbackends",
          "expr": "sum(pg_stat_database_numbackends{datname=\"shop\"})" }
      ]
    },
    {
      "id": 7, "type": "timeseries", "title": "Ошибки: 5xx по кодам",
      "gridPos": { "x": 12, "y": 16, "w": 12, "h": 8 },
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "fieldConfig": { "defaults": { "unit": "reqps", "min": 0 }, "overrides": [] },
      "targets": [
        { "refId": "A", "legendFormat": "5xx",
          "expr": "sum(rate(http_requests_total{status=~\"5..\"}[$__rate_interval])) or vector(0)" }
      ]
    }
  ]
}
EOF
jq -e '.panels | length' ~/monitoring-lab/03-dashboards-alerts/dashboards/lab-use.json
~/monitoring-lab/03-dashboards-alerts/scripts/dash-import.sh ~/monitoring-lab/03-dashboards-alerts/dashboards/lab-use.json

В legendFormat нет шаблонов с двойными скобками: подписи заданы обычным текстом, поэтому оборачивать его в защиту от шаблонов не нужно. Раскладка по строкам такая. Верхняя строка про главный ресурс стенда, пул: загрузка и очередь рядом с ожиданием. Средняя про процессор и память. Нижняя про базу и ошибки.

Что должно получиться (пример):

7
success  http://localhost:3000/d/lab-use/moya-laboratoriya-use

Как читать вывод: семь панелей. На открытой странице «занято» колеблется между нулём и парой единиц, «ждут» держится на нуле, p95 ожидания в миллисекундах, CPU хоста небольшой.

Типичные ошибки:

  • «Память контейнера shop» и CPU контейнера пусты: метка cAdvisor container_label_com_docker_compose_service на твоей машине называется иначе. Открой Explore и введи container_memory_working_set_bytes, посмотри метки у реальных рядов.
  • Все панели пустые, но вверху нет ошибок: период слишком короткий для окна rate. Выбери «Last 15 minutes».

5. Поломка оплаты: читаем дашборд глазами

Теперь главное: проверим, что за десять секунд ты видишь, что магазин горит. Для нагрузки нужен скрипт, который оформляет заказы подряд. Заведи его:

cat > ~/monitoring-lab/03-dashboards-alerts/orders.sh <<'EOF'
#!/usr/bin/env bash
# orders.sh НОМЕР СЕКУНДЫ: один «покупатель» user000N оформляет заказы подряд
set -u
BASE=${BASE:-http://localhost:8000}
N=$(printf '%04d' "$1"); END=$((SECONDS + ${2:-120}))
TOKEN=$(curl -fsS "$BASE/api/login" -H 'Content-Type: application/json' \
  -d "{\"email\":\"user${N}@shop.lab\",\"password\":\"password\"}" | jq -er .token)
[ -n "$TOKEN" ] || { echo "не удалось войти: запущен ли стенд?"; exit 1; }
while (( SECONDS < END )); do
  curl -s -o /dev/null "$BASE/api/cart/items" -H "Authorization: Bearer $TOKEN" \
    -H 'Content-Type: application/json' -d "{\"product_id\":$((RANDOM % 10000 + 1)),\"qty\":1}"
  curl -s -o /dev/null -X POST "$BASE/api/orders" -H "Authorization: Bearer $TOKEN"
done
EOF
chmod +x ~/monitoring-lab/03-dashboards-alerts/orders.sh

Открой оба дашборда (lab-red и lab-use) в двух вкладках, запиши в заметки базовую линию (Rate, p95, очередь). Затем запусти восемь «покупателей» и замедли оплату:

cd ~/monitoring-lab/03-dashboards-alerts
for i in 1 2 3 4 5 6 7 8; do ./orders.sh "$i" 180 & done
curl -s -X POST localhost:8001/admin/config -H 'Content-Type: application/json' -d '{"delay_ms": 5000}'
curl -s localhost:3000/api/annotations -H 'Content-Type: application/json' \
  -d '{"dashboardUID":"lab-red","tags":["incident"],"text":"оплата: delay_ms 5000"}'

Первая строка запускает восемь копий скрипта в фоне, каждая под своим пользователем. Вторая (POST /admin/config) делает оплату медленной на лету: каждая оплата ждёт 5 секунд. Заказ держит соединение с базой, пока ждёт оплату, а в пуле их только пять. Третья команда ставит на lab-red аннотацию с тегом incident в текущий момент (поле time не задано, берётся «сейчас»), как это сделал бы скрипт выкатки.

Что должно получиться (ответ оплаты и картина на экране, цифры пример):

{"delay_ms":5000.0,"fail_rate":0.0}
{"id":1,"message":"Annotation added"}

На обоих дашбордах видна вертикальная метка «оплата: delay_ms 5000» (если нет, проверь в настройках дашборда, раздел Annotations: должен стоять встроенный запрос «Annotations & Alerts»). На lab-use через 10-20 секунд «занято» упрётся в пять, «ждут (очередь)» станет больше нуля. На lab-red p95 уйдёт вверх к пяти секундам и выше (гистограмма округляет вверх внутри корзины от 5 до 10 с). Позже, через 20-40 секунд, на Errors появится доля 5xx. CPU хоста и памяти контейнера почти не изменятся.

Как читать вывод: примерно в таком порядке: очередь (причина), задержка (боль), ошибки (отказ). Процессор в норме: без панели пула ты бы не нашёл причину. Запиши в dashboard-notes.md три строки: что выросло первым, что вторым, что не изменилось. Затем верни оплату и подожди минуту:

Порядок событий виден по флажку: очередь и задержка поднялись сразу, ошибки позже, когда запросы начали упираться в таймаут пула. Ошибки на дашборде появились последними, а причина (пул) уже стояла на экране раньше всех.

Теперь откроем лог и посмотрим на тех, кто пострадал:

Нижняя строка это заказ до поломки, 92 мс. Выше успешные заказы по пять секунд (ждут оплату) и красные 503 с текстом couldn't get a connection after 5.00 sec: они не дождались соединения. Ошибки и медленные успехи одной причины: оплата держит соединения.

Номер trace_id из красной строки ведёт в трейс, а там причина видна по полоскам:

Всё время заказа занимает один отрезок, оплата POST /pay на пять секунд: соединение с базой всё это время занято. Это и есть причина очереди в пуле.

До базы и оплаты запрос так и не дошёл: красный db.pool.getconn на пять секунд и сразу 503. Эти два трейса вместе объясняют дашборд: одни заказы держат соединения, другие ждут их и падают.

curl -s -X POST localhost:8001/admin/config -H 'Content-Type: application/json' -d '{"delay_ms": 50}'

Типичные ошибки:

  • Очередь не появилась: слишком мало копий orders.sh или оплата не замедлилась. Проверь ответ curl и что запущено восемь копий (jobs).
  • не удалось войти: запущен ли стенд?: магазин не отвечает, проверь curl -s localhost:8000/readyz.
  • Скрипты закончатся сами через 180 секунд. Остановить раньше: kill $(jobs -p).

6. Правка мышью и выгрузка обратно: шаг проекта

Добавь панель мышью. На lab-red нажми Edit, затем Add, Visualization. Выбери Prometheus, вставь запрос topk(3, sum by (route) (rate(http_requests_total[$__rate_interval]))), единицу requests/sec (rps), заголовок «Топ-3 маршрута» и сохрани дашборд (Save dashboard). Названия кнопок в Grafana 13 могут отличаться, ищи похожие. Заполни у панели «Топ-3 маршрута» поле Description (раздел Panel options), например: «Какой маршрут больше всех нагружен: первый шаг при росте p95». Теперь выгрузи результат в файл и закоммить:

cd ~/monitoring-lab/03-dashboards-alerts
./scripts/dash-export.sh lab-red > dashboards/lab-red.new.json && mv dashboards/lab-red.new.json dashboards/lab-red.json
jq '.panels | length' dashboards/lab-red.json
cd ~/monitoring-lab && git add 03-dashboards-alerts && git commit -m "3.1: дашборды lab-red и lab-use и скрипты загрузки"

Выгрузка идёт во временный файл и потом переименовывается: рабочий JSON не потеряется, если выгрузка упадёт посередине.

Что должно получиться (пример):

5
[main 4f2a9c1] 3.1: дашборды lab-red и lab-use и скрипты загрузки
 6 files changed, 410 insertions(+)

Как читать вывод: пять панелей: четыре из файла и одна твоя. Проверь описание командой jq -r '.panels[] | select(.description) | .title' dashboards/lab-red.json: в ответе должно быть «Топ-3 маршрута». Коммит записал дашборды в историю. Имя ветки и хеш у тебя другие.

Типичные ошибки:

  • curl: (22) The requested URL returned error: 404: нет дашборда с таким uid (или Grafana не тот адрес). Файл lab-red.json остался прежним, потому что mv не запустился. Проверь uid в адресе дашборда.
  • jq '.panels | length' печатает 0 или null, а в файле всего {"id":null}: выгрузка перезаписала рабочий JSON ответом без дашборда (так бывает со старой версией скрипта без -f). Восстанови из git: git checkout -- dashboards/lab-red.json.
  • nothing added to commit: ты не в репозитории ~/monitoring-lab, проверь git status.

Сломай и почини

В этом разделе сломаем дашборд, который только что работал. Прочитай симптом, запиши гипотезы и только потом смотри проверки.

Симптом

Ты открыл lab-red и в панели Rate поменял в запросе route=~"$route" на route="$route". В списке «Маршрут» выбрано «All». Панель Rate пустая, «No data». Остальные панели не изменились. Магазин жив, трафик идёт.

Гипотезы

  1. Prometheus лёг, и данных нет.
  2. Переменная route не загрузила значения.
  3. Операция в селекторе не подходит под то, что подставляет переменная.

Проверки

Сначала проверь гипотезу 1: если бы Prometheus лёг, пустыми были бы все панели, а не одна. Проверь гипотезу 2: выбери в списке один маршрут, например /api/products.

curl -s localhost:9090/-/ready

Что должно получиться (пример):

Prometheus Server is Ready.

Как читать вывод: Prometheus готов, значит гипотеза 1 отпадает. С одним выбранным маршрутом панель оживает: переменная работает. Остаётся третья. Открой в панели меню Inspect и пункт Query: он покажет запрос, который реально ушёл в Prometheus. Там вместо $route стоит (/api/products|/api/orders|...): регулярное выражение с вертикальными чертами, а оператор = ищет метку, равную этому тексту целиком.

Вот как это выглядит в Query inspector (две версии запроса при выбранном «All»):

route="$route"   ->  http_requests_total{route="(/api/cart/items|/api/categories|/api/orders|/api/products|/api/products/{id}|other)"}
                     result: []
route=~"$route"  ->  http_requests_total{route=~"(/api/cart/items|/api/categories|/api/orders|/api/products|/api/products/{id}|other)"}
                     result: 6 series

Как читать вывод: в обоих запросах подставился один и тот же текст со скобками и вертикальными чертами. Оператор = ищет метку, равную этому тексту целиком, и не находит (пусто), а =~ разбирает его как шаблон и находит все шесть маршрутов. Сначала смотри на подставленный текст, а не на запрос из редактора.

Исправление

Разбор

Верна гипотеза 3. При выборе «All» (или нескольких значений) Grafana подставляет регулярное выражение. Оператор = сравнивает буква в букву, совпадения нет, рядов нет, панель пуста. При одном значении текст совпадает, поэтому одиночный выбор «работает» и вводит в заблуждение. Чинится возвратом =~.

Правило: с переменными в селекторах Grafana всегда =~ и !~. Отдельно запомни диагностику «No data»: сначала запрос в Explore или Query inspector (что реально ушло в Prometheus), потом источник данных, потом период. Пустые все панели сразу значат, что не отвечает источник, а не запрос.

ИИ в помощь

Нейросеть быстро набросает запрос панели и подберёт единицу, но названия кнопок Grafana 13 у неё часто устарели. Общие правила: ИИ-помощник.

Задача: подобрать запрос и вид панели.

Grafana 13, источник Prometheus. Нужна панель «Задержка p95 по маршрутам» для сервиса магазина.
Метрика: http_request_duration_seconds, гистограмма с метками method и route.
Предложи запрос PromQL, тип панели, единицу и пороги. Объясни, зачем $__rate_interval вместо [1m].

Проверь ответ: вставь запрос в Explore и сравни с lab-red. Типичные ошибки: единицы миллисекунды вместо секунд (график «в тысячу раз выше»), rate без sum by (le) внутри histogram_quantile и меню, которых нет в твоей версии.

Задача: найти лишнее на дашборде.

Вот список панелей моего дашборда магазина: <вставь названия и запросы>. Первый экран вмещает 6 панелей.
Какие поставить наверх, какие убрать вниз, что лишнее? Проверь на правило «горит или нет за 10 секунд».

Проверь ответ: наверху должны стоять трафик, ошибки (доля), p95 и очередь пула. Если нейросеть оставила наверху CPU и память, а не ошибки покупателя, она перепутала симптом и причину. Типичная ошибка нейросетей: добавлять графики, а не убирать их.

Сначала прикинь порядок панелей сам, потом спрашивай нейросеть: если ответы разошлись, правильный тот, который ты можешь объяснить.

Словарик урока

Термин Простыми словами
Дашборд (dashboard) Страница Grafana с панелями: графиками и числами о сервисе
Панель (panel) Один график или число: запрос к источнику плюс вид
Источник данных (data source) Настроенное подключение Grafana к хранилищу: Prometheus, Loki, Tempo
Explore Режим Grafana для свободных запросов без дашборда
Загрузка (utilization) Доля времени, когда ресурс занят
Насыщение (saturation) Сколько работы ждёт в очереди к ресурсу
Переменная (variable) Выпадающий список вверху дашборда; её значение подставляется в запросы как $имя
$__rate_interval Окно для rate, которое Grafana подбирает по шагу графика и интервалу опроса
Порог (threshold) Значение, после которого панель меняет цвет; ничего не отправляет
Общее перекрестие (shared crosshair) Вертикальная линия, которая движется по всем панелям одновременно
uid Уникальное имя дашборда в адресе и API; числовой id принадлежит базе Grafana
Provisioning Загрузка настроек и дашбордов в Grafana из файлов на диске
Аннотация (annotation) Вертикальная метка события (выкатка, инцидент) на всех графиках дашборда
Нечестная метрика Метрика, которая остаётся зелёной, когда покупателю плохо: например, «процесс жив» вместо «заказ оформился»
Heatmap Тепловая карта распределения задержек: время по горизонтали, корзины по вертикали

Вопросы с собеседований

Раздел для повторения: ответь вслух, потом открой ответ. Короткие вопросы с пометкой [на скорость] тренируй на время: ответ за 30 секунд.

1. [junior] [часто] Что должно быть на первом экране дашборда сервиса?

Ответ

Ответ на три вопроса по порядку. «Горит ли?»: за десять секунд, по RED: трафик, доля ошибок, p95. «Где именно?»: за минуту, разбивка по маршрутам. «Почему?»: ниже, ресурсы по USE. Всего пять-восемь панелей, сверху симптомы покупателя, потом ресурсы.

Что хотят услышать: симптомы сверху, ресурсы ниже, ограничение по числу панелей, ответ за десять секунд.

Красный флаг: перечисляют всё, что можно мерить, и гордятся сорока графиками.

2. [junior] [часто] Что такое RED и что рисуют на каждой панели?

Ответ

RED это Rate (запросов в секунду), Errors (доля плохих ответов, например 5xx) и Duration (задержка, p50 и p95). Метод описывает сервис глазами покупателя. Errors рисуют долей, а не штуками, и ставят рядом Rate, чтобы видеть, сколько вообще трафика.

Что хотят услышать: три сигнала, доля ошибок вместо числа, квантили вместо среднего.

Красный флаг: называют Duration средним временем ответа.

3. [junior] [часто] Чем USE отличается от RED и когда его смотрят?

Ответ

RED про запросы и покупателя, USE про ресурсы: загрузка (utilization), насыщение (saturation) и ошибки (errors) для процессора, памяти, диска и пула соединений. Сначала смотрят RED: горит ли. Потом USE: какой ресурс виноват. Насыщение (очередь) честнее загрузки: касса занята на 100% без очереди ещё не беда.

Что хотят услышать: RED симптомы, USE причины, очередь важнее загрузки.

Красный флаг: считают загрузку 100% автоматически аварией или говорят, что USE нужен вместо RED.

4. [middle] [часто] Сервис отвечает по пять секунд, процессор на 12%. Как искать причину по дашборду?

Ответ

Низкая загрузка при медленном ответе значит, что запросы стоят в очереди. Смотрю насыщение ресурсов: на стенде shop_db_pool_waiting, не исчерпан ли пул соединений, и p95 внешней зависимости (оплата). Параллельно смотрю число запросов в работе: если оно растёт, запросы застряли. Дальше логи и трейсы.

Что хотят услышать: переход от симптома к очереди, пул соединений, зависимости, а не «добавим процессор».

Красный флаг: предлагают добавить ядер или перезапустить сервис без проверки.

5. [middle] Что такое $__rate_interval и зачем он нужен вместо [1m]?

Ответ

Это переменная Grafana: окно для rate, которое она подбирает по шагу графика и интервалу опроса (не короче четырёх интервалов опроса). Фиксированное окно при большом масштабе оказывается короче шага точек, и график получается дырявым. С $__rate_interval на любом масштабе в окне есть как минимум два замера.

Что хотят услышать: связь окна с шагом графика и интервалом опроса, дыры при фиксированном окне.

Красный флаг: говорят, что это просто псевдоним минуты.

6. [middle] [на скорость] Почему с переменными в селекторе Grafana пишут =~, а не =?

Ответ

При выборе «All» или нескольких значений Grafana подставляет регулярное выражение вроде (/api/a|/api/b). Оператор = сравнивает буква в букву и ничего не находит, поэтому панель пустая. =~ понимает регулярное выражение. Одиночный выбор с = работает и вводит в заблуждение.

Что хотят услышать: подстановка регулярки при «All», пустая панель при =.

Красный флаг: советуют убрать «All» из списка.

7. [junior] Что делает порог (threshold) на панели и чего он не делает?

Ответ

Порог красит график или число и рисует границу, когда значение её пересекло. Он никого не оповещает: для этого нужны правила алертов и Alertmanager (урок 3.2). Порог нужен, чтобы боковым зрением заметить, что что-то не так.

Что хотят услышать: порог это краска, оповещения отдельно.

Красный флаг: считают, что порог на панели сам разбудит дежурного.

8. [middle] [часто] Зачем хранить дашборд как код и как это делают на практике?

Ответ

Дашборд это JSON-документ. В git его можно сравнить с прошлой версией, откатить и перенести на другой стенд. Выгружают через HTTP API (GET /api/dashboards/uid/...), перед загрузкой обнуляют числовой id и отправляют POST /api/dashboards/db с overwrite. Боевые дашборды часто подключают файлами (provisioning): источник правды файл.

Что хотят услышать: JSON в git, uid против id, API или provisioning, правки мышью не попадают в git сами.

Красный флаг: уверены, что кнопка Save в интерфейсе сохраняет в git.

9. [middle] На панели «No data» при выборе «All», при одном значении работает. Что проверишь?

Ответ

Сначала Query inspector: какой запрос реально ушёл в Prometheus. Если в селекторе =, а переменная подставляет регулярное выражение, заменю на =~. Потом проверю источник данных (если пусты все панели сразу, дело в нём) и период времени.

Что хотят услышать: порядок: запрос, источник, период; различие «пусто везде» и «пусто на одной панели».

Красный флаг: сразу перезапускают Prometheus или Grafana.

10. [middle] Почему на панели Duration рисуют p50 и p95, а не среднее?

Ответ

Среднее прячет хвост: девять быстрых запросов и один медленный дают нормальное среднее, а десятый покупатель ждёт. p50 показывает типичного покупателя, p95 тех, кому хуже всего. Квантили считают из гистограммы функцией histogram_quantile по sum by (le) (rate(..._bucket[...])).

Что хотят услышать: хвост задержки, histogram_quantile, sum by (le).

Красный флаг: говорят, что среднее неточно считается.

11. [middle] Что значит «дашборд ведёт к действию» и как этого добиться?

Ответ

У каждой панели, которая может покраснеть, есть ответ на вопрос «что делать первым». Для этого пишут описание панели (что значит и куда идти), ставят ссылку на runbook или в Explore и кладут симптом рядом с панелью-причиной. Проверка: что сделает человек, который видит панель красной ночью и не знает систему? Если ответа нет, панель либо дорабатывают, либо убирают с общего экрана.

Что хотят услышать: описание панели, ссылка на runbook, соседство симптома и причины, проверка «ночью без автора».

Красный флаг: считают, что достаточно покрасить панель красным.

12. [middle] [часто] Все панели зелёные, up равен 1, а покупатели жалуются на заказы. Как такое бывает и что менять?

Ответ

Метрики измеряют живость процесса, а не результат покупателя. up и /healthz остаются зелёными, пока процесс отвечает, хотя оплата или база могут быть сломаны. Меняют так: рисуют долю успешных ответов и p95 по ключевому пути (заказ), считают все ошибки покупателя, а не только 5xx, добавляют синтетическую пробу пути целиком и подписывают «нет данных» явно.

Что хотят услышать: живость против результата, доля успешных заказов, проба пути покупателя, «нет данных» не равно «зелёный».

Красный флаг: предлагают добавить ещё одну проверку живости.

Проверено на версиях

Стенд «Магазин» из load-tester/project/shop: Grafana 13.2 (grafana/grafana:13.2.3), Prometheus 3.15, Docker Compose v2, jq 1.7, curl. Октябрь 2026.

Итог урока: ты умеешь

  • Объяснить, что должно быть на первом экране дашборда и почему на нём пять-восемь панелей.
  • Собрать панели RED: трафик, долю ошибок, p50 и p95, и объяснить, почему не среднее.
  • Разложить ресурс по USE: найти загрузку, насыщение и ошибки, в том числе у пула соединений.
  • Использовать переменную в селекторе (=~) и $__rate_interval.
  • Выбрать единицы и пороги и включить общее перекрестие.
  • Загрузить и выгрузить дашборд через API и хранить его JSON в git.
  • Сделать так, чтобы красная панель подсказывала действие: описание, ссылка на runbook, соседство с причиной.
  • Отличить честную метрику (результат покупателя) от нечестной (процесс жив) и поставить аннотацию выкатки или инцидента.
  • Найти причину «No data» по порядку: запрос, источник, период.

Где это применить

Дальше: урок 3.2. Алерты и Alertmanager: дашборд работает, пока на него смотрят, а ночью экран никто не читает. Ты научишься писать правила, которые сами зовут человека и приходят с инструкцией.

Проверь себя

Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.

Тест работает с включённым JavaScript.

тема 3 урок 3.1 3 ч курс 0/0 ← → уроки