Очередь — самый честный датчик распределённой системы: она физически копит то, что система не успевает съесть, и врать ей незачем. Пока сервис отвечает на health-check кодом 200, глубина notify-очереди может расти часами — именно она первой скажет, что воркеры не справляются, что внешний SMTP-шлюз лежит, что издатель умер и трафика нет вовсе. Наблюдаемость очереди строится из двух слоёв. Первый — метрики брокера: глубины очередей, неподтверждённые доставки, число потребителей, скорости публикаций и раздач — их отдаёт management API и плагин rabbitmq_prometheus. Второй — метрики приложения: счётчики обработанных и провалившихся задач, in-flight воркеров и возраст сообщений, который считает только ваш код, потому что брокер про бизнес-граничные минуты не знает. Разберём оба слоя, соединим их в экспортёр и превратим цифры в алерты, которые будят дежурного по делу, а не на каждый пик трафика.
Мониторинг rabbitmq: какие цифры смотреть#
Начинается мониторинг rabbitmq с четырёх величин, которые видны и в management UI из второго урока, и в API. Глубина очереди — сообщения в состоянии ready: сколько работы ждёт; её считает и QueueDeclarePassive из кода ниже. Неподтверждённые доставки — unacked: сколько задач прямо сейчас держат воркеры; при стабильной работе это число колеблется вокруг суммы окон prefetch из десятого урока. Число потребителей: ноль при живом сервисе означает мёртвую подписку или упавший инстанс — это критическое значение, а не предостережение. Наконец, скорости: публикации в секунду против раздач — их разрыв и есть скорость накопления хвоста. Отдельный показатель — доля redelivered: растёт при обрывах соединений и вечных requeue, сигналя о проблемах транспорта из двадцатого урока или о ядах из шестнадцатого. Источники цифр: management API по пути /api/queues, панель Overview, а с версии 3.8 — плагин rabbitmq_prometheus, который отдаёт метрики на отдельном порту 15692 для скрейпинга.
Экспортёр глубины и счётчиков воркера
GOpackage main
import (
"log"
"net/http"
"time"
amqp "github.com/rabbitmq/amqp091-go"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var queueDepth = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "notify",
Name: "queue_depth",
Help: "сообщений в состоянии ready",
})
var queueConsumers = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "notify",
Name: "queue_consumers",
Help: "подписанных потребителей",
})
// опрос глубины: пассивное объявление не меняет топологию
func pollDepth(ch *amqp.Channel) {
q, err := ch.QueueDeclarePassive("notify", true, false, false, false, nil)
if err != nil {
log.Println("passive:", err)
return
}
queueDepth.Set(float64(q.Messages))
queueConsumers.Set(float64(q.Consumers))
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
if err != nil {
log.Fatal(err)
}
ch, err := conn.Channel()
if err != nil {
log.Fatal(err)
}
go func() {
for range time.Tick(5 * time.Second) {
pollDepth(ch)
}
}()
http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(":9090", nil))
}QueueDeclarePassive проверяет очередь без объявления: вернёт amqp.Queue с полями Messages и Consumers, а при несуществующей очереди даст ошибку, по которой удобно алертить о пропавшей топологии. Глубину и число потребителей снимают по таймеру в Gauge — они мгновенные значения; счётчики обработанных задач наращивают сами воркеры. Честный нюанс: unacked пассивное объявление не отдаёт — его берут из management API по пути /api/queues или из метрик плагина, в management UI это отдельная колонка.
Сигналы и реакции дежурного#
| Сигнал | Что значит | Реакция |
|---|---|---|
| depth растёт при ровном приходе | воркеры не справляются с потоком | масштабировать пул или инстансы |
| unacked упёрся в сумму окон | задачи зависли или стали тяжёлыми | смотреть теги потребителей и тайм-ауты |
| consumers = 0 | подписка умерла, инстанса нет | критический алерт немедленно |
| возраст p95 выше SLA | сообщения ждут дольше обещанного | разбирать очередь, а не логи |
| redelivered-доля растёт | обрывы, реконнекты или вечный requeue | логи транспорта и яды |
Кейс: ночной скрипт издателя сломался в 01:40, а заметили это в 11 утра, потому что дашборд был сплошь зелёный — глубина ноль, ошибок нет. Очередь честно показывала пустоту: перестал приходить трафик, и датчик давления показал отсутствие давления. Поэтому вместе с порогами сверху ставят пороги снизу: при ожидаемом потоке нулевая скорость публикаций днём — алерт, а не тишина. Вторая ловушка — политика max-length из пятого урока: классическая очередь с заполненным лимитом молча выбрасывает голову, глубина выглядит идеальной, а сообщения потеряны; следите за счётчиками удалений в UI или выбирайте overflow=reject-publish, чтобы отказ получал издатель, а не тишина.
Метрики очереди go: возраст сообщений#
Глубина отвечает на вопрос «сколько», а метрики очереди go дополняют вопросом «как долго». Возраст сообщения — разница между текущим временем и моментом публикации: воркер считает time.Since(d.Timestamp) и наблюдает в Histogram. Честный нюанс: поле Timestamp у amqp.Delivery заполняется из свойства timestamp сообщения, которое проставляет издатель в amqp.Publishing — если издатель его не заполнил, поле нулевое, и считать придётся по собственному заголовку вроде x-created-at. Возраст раскладывает инциденты быстрее глубины: глубина двенадцать сообщений при возрасте в минуты — терпимо; глубина двенадцать при возрасте в два часа — что-то держит голову очереди. Для SLA смотрят не среднее, а верхние перцентили — девяносто пятый и девяносто девятый: средний возраст легко спрятать за свежим потоком. Возраст сообщений в DLQ — отдельная величина: он показывает, сколько времени письмо мертво и сколько ещё можно потерять до разбора из шестнадцатого урока.
Возраст сообщения в воркере
GOvar lag = promauto.NewHistogram(prometheus.HistogramOpts{
Namespace: "notify",
Name: "message_lag_seconds",
Help: "возраст сообщения в момент обработки",
Buckets: []float64{1, 5, 15, 60, 300, 900, 3600},
})
func worker(tag string, msgs <-chan amqp.Delivery) {
for d := range msgs {
// Timestamp проставлен издателем в amqp.Publishing
age := time.Since(d.Timestamp).Seconds()
lag.Observe(age)
if err := send(d); err != nil {
d.Nack(false, false)
continue
}
d.Ack(false)
}
}Ведро гистограммы выровнено по здравым порогам задержки: до секунды — штатно, до минуты — терпимо, до пятнадцати минут — повод посмотреть, до часа — алерт. Наблюдение выполняется в момент взятия доставки: так метрика отвечает на вопрос, сколько сообщение ждало в очереди, а не сколько оно обрабатывалось. Издатель обязан заполнять Timestamp — иначе метрика будет измерять не задержку, а нули.
Плагин rabbitmq_prometheus включается командой rabbitmq-plugins enable rabbitmq_prometheus и отдаёт метрики на порту 15692. У него есть два профиля — упрощённый и полный per-object — с разным объёмом; состав и имена метрик менялись между версиями брокера, поэтому сверяйтесь с документацией своей версии, а не с чужими дашбордами. Скрейпинг настраивается в prometheus.yml, как разобрано в курсе по Prometheus. Деление труда простое: брокерские метрики — глубины, потребители, скорости — собирает Prometheus с брокера; бизнес-возраст, processed и failed — только ваше приложение, брокеру они неизвестны.
Алерты, которые будят по делу#
Сигнал превращается в алерт по правилам из курса по Prometheus: будит устойчивое состояние, а не пик. Глубина выше порога в течение десяти минут подряд — алерт; одиночный скачок при пересчёте ночного батча — тишина. Число потребителей равное нулю — алерт немедленный, без выжидания: подписка мертва, и каждое сообщение увеличивает хвост. Возраст p95 выше SLA — алерт по бюджету задержки: если обещали доставку за минуту, порог ставят на шестидесятой секунде, а не на среднем значении. Глубина DLQ больше нуля — тикет на разбор, не обязательно ночной пейджинг, но счётчик обязан быть виден. И обязательная пара к порогам сверху — порог снизу из warn-врезки: нулевой поток публикаций при ожидаемом трафике будит дежурного так же честно, как переполнение. Хорошая практика — дублировать каждый алерт ссылкой на панель с этим графиком: дежурный видит не текст «depth > 5000», а картину и тренд.
Дашборд очереди: пять панелей#
Панели дашборда собирают в пары, потому что поодиночке цифры обманчивы. Первая пара: глубина и unacked на одном графике — рост первой при спокойной второй означает нехватку воркеров, рост обеих — затык обработчиков. Вторая: скорости публикаций и раздач наложением — их расхождение видно раньше, чем глубина пробьёт порог. Третья: возраст p95 с линией SLA поверх — она честнее процентиля девяносто девять, потому что реагирует раньше. Четвёртая: потребители по тегам из семнадцатого урока — исчезновение одного тега на дашборде видно мгновенно. Пятая: глубина DLQ и доля redelivered — хвост аварий. Каждая панель отвечает на один вопрос, и если панель невозможно описать вопросом, она не нужна.
Экспортёр с net/http и client_golang запускается локально против брокера из второго урока: сети в песочнице нет, и библиотека Prometheus там не подключится. В песочнице доступны fmt, strings, strconv, math, sort, errors и time — логику сигналов и арифметику возраста можно отрепетировать программой: посчитайте возраст на фиксированных длительностях, разложите пороги по sort и выведите, какой алерт сработает первым. Интерактивы ниже закрепят значения полей и правила чтения цифр.
Чек-лист наблюдаемости очереди#
- Глубина ready и unacked снимаются регулярно: QueueDeclarePassive или брокерские метрики плагина.
- Число потребителей с алертом на ноль: мёртвая подписка копит хвост незаметно.
- Скорости публикаций и раздач наложением: расхождение — опережающий сигнал.
- Возраст сообщений с Timestamp от издателя: p95 против линии SLA.
- Счётчики приложения: processed, failed, in-flight из семнадцатого урока.
- Порог снизу на отсутствие трафика: пустая очередь при ожидаемом потоке — инцидент.
- Проверка политики max-length: молчаливое выбрасывание головы не должно прятаться за зелёным дашбордом.
Цифры урока опираются на соседей: пятый урок про QueueDeclare дал поля q.Messages и флаги очереди, десятый про prefetch — сумму окон, вокруг которой колеблется unacked, а семнадцатый про worker pool — счётчики processed, failed и in-flight. Глубина DLQ — сигнал от шестнадцатого урока про dead letter, management UI описан во втором уроке. Типы метрик, алерты и скрейпинг разобраны в курсе по Prometheus: Counter и Gauge, Gauge состояния, правила алертов и странице курса. Карта курса — на странице курса.
Проверка: что считает q.Messages
Экспортёр сделал QueueDeclarePassive и прочитал поле q.Messages. Что именно оно показывает?
Проверка: глубина стабильна, возраст растёт
Глубина очереди держится около пятидесяти сообщений уже час, но возраст p95 за тот же час вырос с минуты до двадцати. Что вероятнее всего происходит?
Какой сигнал сработает?
Прочитайте код и выберите, что он выведет:
type queueState struct {
depth int
consumers int
}
q := queueState{depth: 12, consumers: 0}
switch {
case q.consumers == 0:
fmt.Println("критический: подписки нет")
case q.depth > 10:
fmt.Println("масштабируем пул")
default:
fmt.Println("штатно")
}Соберите опрос глубины
Допишите пассивное объявление, чтение глубины и подсчёт возраста.
Соберите экспортёр метрик
Расставьте строки сборки: соединение, канал, пассивное объявление, значение gauge, HTTP-обработчик, запуск сервера.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: наблюдаемость очереди
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Наблюдаемость очереди двухслойная: брокерские метрики глубин, потребителей и скоростей плюс счётчики и возраст из приложения.
- q.Messages из QueueDeclarePassive — только ready; unacked считается отдельно и колеблется вокруг суммы окон.
- consumers равное нулю — критический алерт: мёртвая подписка копит хвост незаметно.
- Возраст сообщения считает воркер по Timestamp от издателя; смотрят верхние перцентили против SLA.
- Пустая очередь при ожидаемом трафике — тоже инцидент: ставьте порог снизу.
- max-length выбрасывает голову молча: глубина идеальна, а сообщения потеряны — следите за удалениями.
- Алерты будят устойчивые состояния: порог с выдержкой, ноль потребителей — немедленно, DLQ — тикет.
Домашнее задание
Соберите экспортёр из урока против контейнера из второго урока: пассивный опрос глубины по таймеру, гистограмма возраста с Timestamp от издателя из шестого урока и счётчики processed и failed из семнадцатого. Погоните нагрузку и уроните воркеры: убедитесь, что consumers равное нулю видно раньше, чем глубина пробьёт порог. Затем настройте три алерта — ноль потребителей, глубина выше порога десять минут, возраст p95 выше SLA — и искусственно вызовите каждый, проверив, что выдержка отсекает пики от реальных инцидентов.
Личные заметкиПусто
Вопросы и ответы по уроку
Опрашивать брокер через QueueDeclarePassive или брать метрики плагина?
Оба пути рабочие и не исключают друг друга. Плагин отдаёт всё оптом и не требует кода — его берут как основу; пассивный опрос полезен, когда нужна глубина конкретной очереди с минимальными задержками или отдельный эндпоинт для лёгкой проверки живости топологии. Помните: пассивное объявление создаёт сетевые вызовы, поэтому интервал опроса ставят разумным — раз в несколько секунд.
Почему возраст сообщения не считает брокер?
Брокер знает момент публикации на уровне свойства timestamp, но не знает бизнес-границы: для писем это SLA в минуту, для аналитики — час, и только приложению известны эти пороги. Воркер измеряет задержку в момент взятия доставки и наблюдает её в гистограмму — так метрика получается уже с разбиением по типам задач и каналам, что для разбора инцидентов ценнее общего числа.
Как не захлебнуться в алертах от десятков очередей?
Алерты на команды устанавливают на очереди, где промедление стоит денег: задачи, уведомления, DLQ. Технические и одноразовые очереди — RPC-reply из восемнадцатого урока, временные fanout-подписки — обходятся дашбордом без пейджинга. Помогает и агрегация: вместо отдельного алерта на каждую очередь — правило с лейблом имени очереди и выдержкой, чтобы всплеск по десяти очередям не дал десять звонков.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Excel в Go: чтение и генерация xlsx на excelize
Курс excelize на Go: создание xlsx, чтение, стили, формулы, диаграммы и StreamWriter для больших файлов. 25 уроков с примерами кода и проектом.
- Создавать и читать xlsx-файлы средствами excelize
- Работать с листами, ячейками, строками и столбцами
- Оформлять таблицы: стили, шрифты, границы, форматы чисел и дат
- Вставлять формулы, диаграммы и изображения