✻ Урок 3.1 · Тема 3: Дашборды и алерты
Grafana: дашборды RED и USE как код
Содержание урока
Зачем это нужно
Середина первой недели в магазине. Метрики уже собираются, запросы к ним ты писать умеешь, но числа лежат в консоли 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». Остальные панели не изменились. Магазин жив, трафик идёт.
Гипотезы
- Prometheus лёг, и данных нет.
- Переменная
routeне загрузила значения. - Операция в селекторе не подходит под то, что подставляет переменная.
Проверки
Сначала проверь гипотезу 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» по порядку: запрос, источник, период.
Где это применить
- DevOps, урок 8.6: дашборды Grafana: те же панели на сервисе «Заметки» с provisioning из репозитория.
- load-tester, урок 7.3: Grafana, RED и USE: как читать дашборд во время нагрузочного теста.
- load-tester, урок 11.1: метод поиска узкого места: как по USE найти ресурс, который упёрся в предел.
Дальше: урок 3.2. Алерты и Alertmanager: дашборд работает, пока на него смотрят, а ночью экран никто не читает. Ты научишься писать правила, которые сами зовут человека и приходят с инструкцией.
Проверь себя
Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.
Тест работает с включённым JavaScript.