Pingveraблог ← Блог
Главная › Блог › Мониторинг крон-задач

Крон молчит: мониторинг фоновых задач, бэкапов и выгрузок

27 июля 2026 · 13 мин чтения

Мониторинг крон-задач: задача сама отмечается, тишина считается событием

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

Направление наблюдения приходится развернуть

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

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

Способов узнать состояние фоновой задачи вообще-то много: локальный агент или exporter, журнал systemd, почта крона, метрики Prometheus, API планировщика, состояние Kubernetes CronJob. Можно вовсе не смотреть на задачу, а проверять результат — свежесть файла на диске, дату последней записи в таблице, появление объекта в хранилище. У каждого способа своя цена: агент надо ставить и обновлять, метрики — собирать и хранить, проверка результата требует доступа туда, где этот результат лежит.

Но если условие такое: ничего не ставить на сервер, никуда не подключаться и ничем не управлять, — вариант остаётся один. Инициативу отдаём задаче. Она получает короткий персональный адрес и обращается к нему сама, когда отработала. Мониторинг знает, по какому графику ждать сигнал, и поднимает тревогу, если тот не пришёл. Отрасль называет это dead man's switch: срабатывает не действие, а его отсутствие.

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

Как это выглядит на практике

Проверка типа «Задача» выдаёт адрес. Дальше строка crontab выглядит так:

30 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 https://app.pingvera.ru/hb/hb_XXXX

Здесь важен каждый символ, и меньше всего — адрес.

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

-m 10 — предохранитель. Мониторинг не имеет права стать причиной аварии: если наш сервис недоступен или тормозит, curl обязан сдаться через десять секунд и отпустить вашу задачу. -f и -s — чтобы ошибка не потерялась и чтобы в почту крона не сыпался индикатор прогресса.

Голого пинга хватает для ответа «дошла ли задача до конца». Дальше начинается интересное: у адреса есть три хвоста.

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

/fail — задача сама сообщает, что упала, и событие открывается сразу, не дожидаясь следующего срока. Для ночного бэкапа это разница между «узнали в 3:35» и «узнали через сутки».

/<код возврата> — то же самое, но числом: /0 означает успех, любой другой код — провал. Тело запроса, до 10 КБ, сохраняется как лог запуска и видно в карточке рядом с временем и длительностью. Когда в три часа ночи придёт оповещение, ответ на вопрос «что именно случилось» будет уже там, а не в почте крона, которую никто не читает.

Две ловушки, на которых мы поймали сами себя

Мы ставили эту схему на настоящий крон на своём сервере — не в тестах, а живьём, с реальной задачей раз в две минуты. За первый час она поймала две ошибки, обе наши, и обе поучительные.

Ловушка первая: код возврата, который всегда ноль

Обёртка выглядела безобидно:

out=$( { df -h /mnt/backup | tail -1; echo "готово"; } 2>&1 )
rc=$?    # ← всегда 0

Задача честно ломалась, монтирования не было, а мониторинг получал /0 и рисовал зелёное. Причина простая: $? — это статус последней выполненной команды, а последним тут оказался echo, который не падает никогда.

Правило, которое из этого следует: между самой задачей и rc=$? не должно быть ничего. С конвейерами тоньше: set -o pipefail действует только на тот shell, где конвейер исполняется. Если внутри backup.sh есть pg_dump | gzip, включение pipefail в обёртке этому конвейеру не поможет — строку нужно ставить в сам backup.sh. В обёртке она защищает только её собственные конвейеры.

Ошибка коварна тем, что не ломает ничего видимого. Мониторинг работает, письма не приходят, все довольны — и ровно поэтому её стоит проверить у себя прямо сейчас, если обёртка писалась на бегу. Правильный вариант — ниже, в разделе про боевую обёртку: там же выясняется, что одного rc=$? для production мало.

Ловушка вторая: тревога, которая гарантированно ложная

Вторую ошибку допустили уже мы, в самом продукте. Свежесозданная проверка присылала событие «задача не отметилась» через четыре минуты — то есть ровно в тот момент, когда человек ещё копирует адрес и идёт вписывать команду в crontab.

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

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

Обёртка, пригодная для боевого крона

Строка с && хороша, чтобы начать, и годится для задач, где нечего терять. У ночного бэкапа терять есть что, и там нужна обёртка. Вот она целиком, дальше по строкам:

#!/bin/bash
set -o pipefail                      # только для конвейеров ЭТОГО скрипта
PING=https://app.pingvera.ru/hb/hb_XXXX
hb() { curl -fsS -m 10 --retry 3 --retry-delay 5 --retry-connrefused "$@" >/dev/null; }

exec 9>/var/lock/backup.lock         # два запуска не должны наложиться
flock -n 9 || { hb "$PING/fail" --data-raw "предыдущий запуск ещё идёт"; exit 1; }

hb "$PING/start"
log=$(mktemp); trap 'rm -f "$log"' EXIT
timeout 30m /usr/local/bin/backup.sh >"$log" 2>&1; rc=$?
tail -c 8000 "$log" | hb --data-binary @- "$PING/$rc"
exit "$rc"

exit "$rc" в последней строке — не мелочь. Без него кодом возврата всей обёртки становится код curl, и получаются две одинаково неприятные ситуации: бэкап упал, но сигнал доставлен — крон считает запуск успешным; бэкап отработал, а мониторинг был недоступен — крон считает запуск провальным и шлёт вам письмо. Обёртка обязана сохранять семантику задачи, которую оборачивает.

--retry — потому что моргнувшая сеть не должна выглядеть как пропущенный бэкап. Три попытки с паузой в пять секунд закрывают типичный сетевой чих; при этом общий предел -m 10 на попытку не даёт обёртке залипнуть, если наш сервис лежит по-настоящему.

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

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

Лог во временный файл, а не в переменную. Вывод боевого бэкапа бывает в мегабайты: в переменной shell он целиком поедет в память, потеряет хвостовые переводы строк и споткнётся о нулевые байты, которые bash хранить не умеет. Мы отправляем последние 8 КБ — с запасом влезает в наш предел в 10 КБ. И отдельно стоит посмотреть, что вообще уезжает в этот хвост: строки подключения, пароли в отладочном выводе, персональные данные из выгрузки. Если задача такое печатает — чистите вывод перед отправкой, мониторинг для секретов не место.

Что будет, если недоступны мы

Heartbeat добавляет в вашу цепочку новое звено: задача → DNS → сеть → TLS → наш хаб → обработчик событий → Telegram или почта. Звено новое, значит и отказать может. Честные ответы на неудобные вопросы:

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

Задерживается ли реакция? Состояние задач пересчитывается раз в 15 секунд, так что событие открывается в пределах этого шага после того, как истёк допуск. Это же означает, что микроскопические допуски бессмысленны: меньше минуты ставить незачем.

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

Расписание против интервала

«Раз в сутки» и «каждую ночь в 03:30» звучат похоже, но ведут себя по-разному. Интервал отсчитывается от предыдущего сигнала: задача, отработавшая в 03:31, следующего срока ждёт до 03:31, потом до 03:33 — и постепенно уезжает. Расписание привязано к часам и никуда не едет.

Поэтому у задачи можно задать cron-выражение и часовой пояс отдельно. И вот здесь стоит остановиться, потому что пояс — самое частое место, где ожидания разъезжаются с реальностью.

Пояс в мониторинге задаёт только наши ожидания. На то, когда задача реально запустится, он не влияет никак — это решает крон-демон на вашем сервере. Если сервер живёт в UTC, а в проверке выбрана Москва, мы будем ждать сигнал в 03:00 по Москве, а крон запустит задачу в 03:00 по UTC, то есть на три часа позже, — и вы получите ложное событие каждую ночь. Проверьте timedatectl на сервере и либо укажите его зону в проверке, либо привяжите крон к нужной зоне явно:

CRON_TZ=Europe/Moscow
30 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 https://app.pingvera.ru/hb/hb_XXXX

CRON_TZ понимают Vixie cron и cronie — то есть штатный крон в Debian, Ubuntu, RHEL. У systemd-таймеров своя настройка (OnCalendar с указанием зоны), у Kubernetes CronJob — поле timeZone в спецификации. Общее правило одно: где задаёте расписание, там и задавайте пояс, а в мониторинге ставьте тот же.

Перевод часов. Две ночи в году в зонах с переходом ведут себя необычно: один локальный час не существует вовсе, другой случается дважды. Задача, назначенная на исчезнувший час, у разных реализаций крона отработает по-разному — какая-то пропустит, какая-то сдвинет. Мы в этих случаях сдвигаем ожидание вперёд, к ближайшему существующему времени. Если задача важная и живёт в зоне с переходом, надёжнее увести её из окна 02:00–04:00 или держать сервер и расписание в UTC — там переходов нет.

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

День месяца и день недели. В классическом cron эти два поля работают через ИЛИ, а не через И: выражение 0 0 13 * 5 означает «13-го числа ИЛИ в пятницу», а не «в пятницу тринадцатого». Мы следуем этому же правилу и проговариваем его вслух в описании — если увидите в переводе неожиданное «или», значит выражение делает не то, что вы думали.

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

И отдельно — то, ради чего стоило возиться. Интерфейс проговаривает выражение вслух: «по будням в 09:00», «каждые 15 мин», «1-го числа в 05:00», — и показывает шесть ближайших ожидаемых запусков в выбранном поясе. Причина конкретная: самая частая ошибка в cron — перепутанные местами день месяца и день недели. Выражение при этом остаётся синтаксически безупречным, крон его принимает, а работает оно не тогда. Заметить это можно единственным способом: если система скажет, как она вас поняла, до того как вы нажали «Сохранить».

Допуск: чем отличается «чуть задержалась» от «не запустилась»

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

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

Дальше всё как с обычными проверками, и это, пожалуй, главный практический довод: не нужно заводить ещё один сервис со своими оповещениями и своим логином. Событие приходит в те же каналы — почта, Telegram, Max, VK, Slack, Discord, вебхук, — попадает в общую ленту и в отчёт клиенту за месяц. Когда задача снова отметится успешно, событие закроется само: ручного «починил» у нас нет по устройству — мониторинг фиксирует факты, а не ведёт тикеты.

Честно о границах

Такой мониторинг видит ровно то, о чём задача рассказала сама, и ни байтом больше. Список того, чего он не умеет, стоит знать до внедрения, а не после.

Он не запускает задачи. Это не планировщик и не замена крону. Если крон-демон на сервере умер целиком, мы сообщим о тишине — но запустить за него ничего не сможем.

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

Он не поймает зависание у неразмеченной задачи. «Запустилась и не завершилась» работает только там, где есть сигнал /start. Без него зависшая задача выглядит как молчащая — событие придёт, но позже и с другой формулировкой.

Адрес пинга — это секрет, и обращаться с ним надо соответственно. Токен в нём — 128 бит случайности, перебрать нельзя. Но защита вся в самом адресе: отдельного пароля нет — иначе его нельзя было бы дёрнуть одной строкой из crontab. Что из этого следует практически:

  • Знающий адрес может отметить вашу задачу выполненной — то есть заглушить тревогу. Прочитать что-либо об аккаунте он не может.
  • Адрес попадает в места, о которых легко забыть: история shell, логи прокси и балансировщиков по пути, наши собственные access-логи, системы сбора ошибок. Полностью этого не избежать — стоит просто знать.
  • Мы принимаем и GET, и POST. GET удобен и нужен для одной строки в crontab, но у него есть цена: ссылку могут открыть случайно — превью в мессенджере, сканер ссылок в почте, любопытный бот, — и задача отметится выполненной без задачи. Если адрес где-то фигурирует в переписке или тикетах, используйте POST: curl -fsS -m 10 -X POST ….
  • Ротации адреса пока нет: если он утёк, проверку нужно удалить и создать заново — токен сменится. Это осознанный пробел, а не забытая мелочь; отдельная кнопка «сменить адрес» напрашивается, и она в списке.
  • В открытых репозиториях, публичных дампах конфигов и скриншотах адресу не место — ровно как токену API.

История запусков не бесконечна. Храним последние 50 сигналов на задачу и не дольше 30 дней. Этого хватает, чтобы разобрать инцидент и увидеть, что выгрузка, которая шла две минуты, последнюю неделю идёт двадцать. Для архива логов есть системы логирования, это не они.

С чего начать

Не стоит вешать проверку на каждую строку crontab. Мониторинг, присылающий десять оповещений в неделю про мелочи, перестают читать — и вместе с мелочами пропускают важное. Начните с того, где тишина стоит дорого:

  • Бэкапы — базы и файлов. Самое дорогое и самое незаметное: сломанный бэкап обнаруживается ровно в тот день, когда он понадобился.
  • Выгрузки и импорты — каталог в маркетплейс, остатки из 1С, цены поставщика. Тут тишина превращается в деньги напрямую: магазин работает, а заказов нет — иногда именно поэтому.
  • Продление сертификатов — certbot renew молчит ровно до дня, когда сертификат истёк. Доступность HTTPS мы проверяем и снаружи, но знать о сломавшемся обновлении заранее спокойнее.
  • Очереди и wp-cron — отложенные публикации, письма, синхронизация. Особенно если WP_CRON отключён и вынесен в системный крон.

Каждая такая проверка занимает один слот монитора — на бесплатном тарифе их пять, и этого хватает, чтобы закрыть бэкапы и главную выгрузку.

Что в сухом остатке

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

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

Частые вопросы

Почему мониторинг крона устроен «наоборот» — задача сама отмечается?

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

Почему код возврата иногда врёт и приходит «успех» при упавшей задаче?

Потому что $? в bash возвращает статус последней выполненной команды, а не той, которую вы имели в виду. Если между задачей и чтением $? стоит ещё что-нибудь — echo, tail, закрывающая скобка группы — код возьмётся от неё, и упавшая задача отчитается успехом. Для конвейера нужен set -o pipefail, иначе статус берётся от последнего звена, а не от упавшего. Правило: между самой задачей и rc=$? не должно быть ничего.

Часовой пояс в проверке заставит задачу запускаться в это время?

Нет. Пояс в проверке задаёт только то, когда сигнала ждём мы. Время запуска определяет крон-демон на вашем сервере по своей зоне. Если зоны разные, вы получите ложное событие каждый раз: мы ждём в 03:00 по Москве, крон запускает в 03:00 по UTC. Сверьте timedatectl и либо укажите в проверке зону сервера, либо добавьте CRON_TZ= первой строкой crontab (понимают Vixie cron и cronie; у systemd-таймеров и Kubernetes CronJob свои поля для зоны).

Задача запускается не по расписанию, а по событию — это подойдёт?

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

Поставьте бэкапы под наблюдение

Pingvera следит и за сайтами снаружи, и за фоновыми задачами внутри: не отметилась вовремя, упала с ошибкой или зависла — придёт оповещение в Telegram, Max или на почту. Без единого подключения к вашему серверу. Бесплатный тариф — до 5 проверок, без карты.

Начать бесплатно

Читайте также: Мониторинг обмена сайта с 1С: практическое руководство · Terraform-провайдер Pingvera: мониторинг как код · CLI Pingvera: мониторинг из терминала и скриптов · Не приходят заявки с сайта: почему форма молчит и как это ловить · Бесплатно проверить сайт.

← Все статьи · Политика конфиденциальности · pingvera.ru · Telegram-канал

На сайте осуществляется обработка пользовательских данных с использованием Cookie в соответствии с Политикой конфиденциальности.