
У упавшего сайта есть голос: он отвечает пятисоткой, таймаутом, сорванным сертификатом — чем-нибудь, что видно снаружи. У фоновой задачи голоса нет. Крон-строка, которую не перенесли при переезде на новый хостинг, не подаёт никаких признаков: она просто отсутствует. Скрипт бэкапа, падающий на второй секунде из-за кончившегося места, ведёт себя ровно так же, как скрипт, который отработал идеально, — снаружи ни то ни другое не отличить. Единственный сигнал, который у вас вообще есть, — это тишина. Весь мониторинг фоновых задач построен на том, чтобы научиться её слышать.
Обычная проверка работает так: мы раз в минуту сами обращаемся к сайту и смотрим, что он ответил. Инициатива наша, адрес известен, результат однозначен.
С крон-задачей ни одного из трёх условий нет. Она живёт внутри сервера, у неё нет адреса, и достучаться до неё можно единственным способом — зайти на машину. Этого мы не делаем и не будем: наш агент не принимает команд, у хаба физически нет способа что-либо запустить на клиентском сервере. Это записано в архитектуру как инвариант и не пересматривается.
Способов узнать состояние фоновой задачи вообще-то много: локальный агент или 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. Что из этого следует практически:
GET, и POST. GET удобен и нужен для одной строки в crontab, но у него есть цена: ссылку могут открыть случайно — превью в мессенджере, сканер ссылок в почте, любопытный бот, — и задача отметится выполненной без задачи. Если адрес где-то фигурирует в переписке или тикетах, используйте POST: curl -fsS -m 10 -X POST ….История запусков не бесконечна. Храним последние 50 сигналов на задачу и не дольше 30 дней. Этого хватает, чтобы разобрать инцидент и увидеть, что выгрузка, которая шла две минуты, последнюю неделю идёт двадцать. Для архива логов есть системы логирования, это не они.
Не стоит вешать проверку на каждую строку crontab. Мониторинг, присылающий десять оповещений в неделю про мелочи, перестают читать — и вместе с мелочами пропускают важное. Начните с того, где тишина стоит дорого:
certbot renew молчит ровно до дня, когда сертификат истёк. Доступность HTTPS мы проверяем и снаружи, но знать о сломавшемся обновлении заранее спокойнее.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: мониторинг из терминала и скриптов · Не приходят заявки с сайта: почему форма молчит и как это ловить · Бесплатно проверить сайт.