Процесс воркера существует. Он занимает скромные двести с лишним мегабайт памяти, не уходит в цикл перезапусков и на проверку отвечает бодро: «жив». А самая старая задача в очереди ждёт уже несколько часов. Клиент нажал «оформить», письмо с подтверждением не пришло, и никакой красный индикатор в панели не загорелся. Все смотрят на процесс, а ломается не он, а работа.

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

Начинаем с самого старого ожидающего

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

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

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

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

SELECT COALESCE(EXTRACT(EPOCH FROM now() - MIN(enqueued_at)), 0) AS oldest_pending_seconds
FROM jobs
WHERE status = 'pending';

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

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

Три состояния вместо одной цифры

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

  • ожидающие — поставлены, никто их ещё не взял;
  • выполняемые — взяты воркером, результата пока нет;
  • повторяемые — уже падали и ждут следующей попытки.

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

Документация RabbitMQ как раз разделяет эти понятия: она показывает отдельно сообщения в состоянии ready, то есть готовые к выдаче, и unacknowledged, то есть выданные потребителю, но не подтверждённые (RabbitMQ: Monitoring). Это прямая основа для различения «ждёт» и «выполняется». Там же упомянуты показатели публикации и доставки вместе с их скоростями, а также то, что один показатель не даёт полной картины. Сам принцип переносится и на другие системы: если брокер или ваша таблица не различают состояния, различие придётся ввести самим — например, отдельным полем статуса и временем начала выполнения.

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

Какие пары показателей что сообщают
Что наблюдаемВероятное объяснениеКуда смотреть дальше
Растёт возраст ожидающих, выполняемых мало или нетИсполнители не берут работуПодключения воркеров к брокеру, лимиты, блокировки
Выполняемых много, старейшая выполняется давно, завершений нетВоркеры заняты зависшими задачамиТаймауты внешних вызовов, зависимости
Ожидающие стабильны, повторяемых всё большеЗадачи падают и возвращаютсяТексты ошибок, одно и то же сообщение или разные
Длина большая, возраст небольшой и падаетПросто пиковая нагрузка, очередь разбираетсяНичего, наблюдать скорость

Последняя строка важна. Большая очередь сама по себе не авария: после рассылки на сто тысяч адресатов длина очереди будет внушительной, и это нормально, если она уверенно уменьшается.

Поступление против завершения

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

Здесь полезна простая арифметика. Пусть, условно, поступает 120 задач в минуту, а воркеры завершают по 90. Накопление — 30 задач в минуту, час такого режима даёт 1800 задач в запасе. Если же одна задача обрабатывается в среднем две секунды, а работают три воркера, пропускная способность составляет около 90 задач в минуту — число сходится с исходным допущением. Задержка новой задачи в этом режиме будет расти и приближаться к времени, нужному, чтобы разобрать накопленное: 1800 задач при 90 в минуту — это двадцать минут ожидания для той, что поставлена прямо сейчас. Эти цифры условные, они иллюстрируют метод, а не описывают чью-то систему.

Практический вывод: сравнивайте скорость поступления и скорость завершения как две линии на одном графике. Пока они идут вместе, система справляется. Расхождение — предупреждение, которое приходит задолго до того, как возраст перейдёт любой порог. RabbitMQ даёт для этого показатели публикации и доставки со скоростями; в других системах эти два счётчика придётся добавить самостоятельно: увеличивать один при постановке и другой при успешном завершении.

Тут есть тонкость, которую часто пропускают. Подтверждение сообщения (acknowledgement) и завершённая полезная работа — не одно и то же. Если воркер подтверждает сообщение в начале обработки, чтобы оно не вернулось в очередь, то для брокера задача «выполнена», а письмо ещё не отправлено, платёж не проведён, файл не обработан. Если подтверждает в конце — задача может выполниться дважды при падении между действием и подтверждением. Счётчик «завершено» должен увеличиваться тогда, когда произошёл бизнес-результат, а не тогда, когда брокер получил квитанцию. Иначе график скажет, что всё хорошо, там, где это не так.

Три способа, которыми очередь молчит

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

Исполнитель остановился, но не умер

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

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

Что делать: перезапуск часто помогает, но не следует считать его решением. Если воркер завис посреди внешнего действия, после перезапуска задача может быть выполнена повторно, и безопасность этого зависит от идемпотентности самой задачи. Автоматический рестарт по сигналу «возраст вырос» — удобная мера, но она не заменяет анализа: что именно будет, если задача пройдёт дважды.

Недоступна зависимость

Воркеры работают, берут задачи, но каждая упирается во внешний сервис: платёжный шлюз, почтовый API, хранилище файлов. Задачи либо висят до таймаута, либо падают и уходят в повтор.

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

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

Сообщение, которое падает всегда

Одна задача содержит данные, которые код не умеет обработать. Воркер берёт её, получает исключение, возвращает в очередь, берёт снова. Так по кругу, иногда со ста тысячами попыток.

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

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Почему порог не может быть общим

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

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

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

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

Контрольная задача, у которой есть финал

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

Идея проста. Раз в минуту или раз в несколько минут планировщик ставит в очередь служебную задачу. Воркер выполняет её так же, как настоящую, проходя тот же код, те же подключения и те же зависимости. В конце задача оставляет след: обновляет запись «последний успешный прогон» с временем, либо делает безобидное, но реальное действие, например записывает строку в служебную таблицу. Мониторинг затем проверяет не «жив ли воркер», а «когда в последний раз контрольная задача дошла до результата».

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

Есть несколько условий, чтобы такая проверка не лгала:

  • Тот же путь, что у настоящих задач. Если контрольная задача идёт в отдельную очередь или выполняется отдельным воркером, она проверяет не то. Лучше, чтобы она стояла в тех же очередях и забиралась теми же исполнителями.
  • Полезный результат, а не просто возврат из функции. Задача, которая заканчивается пустым return, подтверждает лишь, что код запустился. Запись в базу, обращение к ключевой зависимости или проверка доступности хранилища ближе к реальной работе.
  • Безопасность повторов. Контрольная задача должна быть идемпотентной: если она выполнится дважды, ничего не сломается. Это обычно легко: она просто обновляет время.
  • Отсутствие побочных эффектов во внешнем мире. Не отправляйте контрольным письмом настоящие письма и не списывайте деньги. Для зависимостей используйте безопасные операции чтения или специальные тестовые режимы, если сервис их предоставляет.

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

Отрепетировать до включения тревог

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

Разумный порядок такой.

  1. Собрать метрики без оповещений. Несколько дней смотреть на графики возраста, длины по состояниям, скоростей поступления и завершения. Выяснить, как выглядят обычный день и пиковый час.
  2. Выбрать пороги по наблюдениям. Предупреждение — выше обычного диапазона, тревога — там, где пользователь уже страдает. У каждой очереди свои значения.
  3. Остановить воркеры на тестовой среде. Убедиться, что возраст растёт линейно, контрольная задача перестаёт успевать, а оповещение приходит за ожидаемое время.
  4. Сымитировать недоступность зависимости. Например, направить воркер на адрес, который не отвечает. Проверить, что видны рост выполняемых или повторяемых и что причина понятна по счётчикам ошибок.
  5. Подложить сообщение, которое всегда падает. Убедиться, что оно упирается в предел попыток, уходит на разбор и что его присутствие видно отдельно.
  6. Проверить отсутствие ложных срабатываний. Устроить искусственный всплеск поступлений и убедиться, что при уверенно разбираемой очереди тревога не звучит.

Для такой репетиции удобна отдельная виртуальная машина с root-доступом: можно спокойно останавливать службы, ограничивать сеть и перезапускать воркеры, не рискуя боевой средой. На VPS MAATRIX доступны NVMe-диски, процессоры AMD EPYC и root-доступ; тарифы начинаются с конфигурации Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) и доходят до Djed (16 vCPU, 32 ГБ, 320 ГБ). Эти ресурсы не обещают конкретной ёмкости очереди, поэтому нагрузку тестовой среды стоит подобрать под ваши задачи и измерить, а не выводить из названия тарифа. Если вы используете n8n, в каталоге предложений есть установка с этим инструментом; наличие и операционную систему образа нужно уточнять в форме заказа.

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

Как собрать итоговый сигнал

Теперь у нас несколько источников. Из них стоит собрать понятную иерархию, чтобы дежурный не листал десять графиков в три часа ночи.

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

На втором месте — причинные индикаторы: скорости поступления и завершения, число повторяемых, максимум попыток, ошибки по зависимостям. Они не обязаны будить человека, но должны сразу помогать понять, что случилось, когда его всё же разбудили. Как отмечает документация RabbitMQ, инфраструктурные и прикладные метрики рассматриваются совместно, и ни один показатель не даёт полной картины (RabbitMQ: Monitoring). Это хорошо сочетается с нашей схемой: возраст — прикладной сигнал, загрузка и подключения — инфраструктурный контекст.

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

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