Как узнать, что cron-задача или бэкап не выполнились
Как узнать, что cron задача не выполнилась: почему cron молчит, где искать следы в journalctl и syslog, обёртка на bash с уведомлением и heartbeat-контроль бэкапов.
Cron не сообщает о том, что задача не выполнилась: он запускает команду по расписанию, дожидается её завершения и забывает о ней. Единственный штатный канал обратной связи — письмо на адрес из MAILTO, а на большинстве серверов почта локально не настроена, и письма исчезают. Поэтому узнать о проблеме можно только двумя путями: либо после факта, разбирая логи (journalctl -u cron, /var/log/syslog), либо заранее поставив контроль, который сам скажет «задача не отработала».
Способов контроля несколько, и они ловят разные отказы. Письмо от cron покажет ошибку в выводе команды, но промолчит, если задача повисла. Файл-маркер поймает «не запустилась», но его надо кому-то проверять. Самый полный вариант — обратная схема, heartbeat-мониторинг cron-задач и бэкапов: задача сама отмечается по URL после успешного завершения, а если отметки нет в срок, приходит уведомление. Он замечает даже случай, когда сервер выключен и проверять уже некому.
Ниже — почему cron молчит, где искать следы вчерашнего запуска и как настроить каждый из способов контроля, включая обёртку на bash, которую можно скопировать в crontab.
Почему cron молчит
Cron ждёт завершения команды, но по умолчанию пишет в лог только факт запуска: вернулась ли она с кодом 0 или 127, в журнале не видно. На Debian и Ubuntu это можно включить: EXTRA_OPTS="-L 5" в /etc/default/cron добавит строки (CRON) error (grandchild #PID failed with exit status 1) для упавших задач; у cronie на RHEL такой опции нет. Типичные причины тихих отказов:
MAILTOне задан или нет MTA. По умолчанию вывод команды (stdout и stderr) cron отправляет владельцу crontab черезsendmail. Еслиsendmailне установлен, cron на Debian и Ubuntu запишет в лог(CRON) info (No MTA installed, discarding output)— и это всё; cronie на RHEL просто выбросит вывод без пометки (если не запущен с флагом-s, при котором вывод уходит в syslog строкамиCMDOUT). Ошибка была, её текст выброшен.- Окружение. Cron не читает
~/.bashrcи~/.profile: из окружения есть толькоHOME,LOGNAME,SHELLиPATH(обычно/usr/bin:/bin), оболочка —/bin/sh, а не bash. Скрипт, который отлично работал из терминала, падает сcommand not foundдляpg_dumpиз/usr/local/binили из-за bash-синтаксиса вроде[[ ]]. - Символ
%. В crontab%означает перевод строки, иdate +%Fпревращается в обрезанную команду. Нужно писатьdate +\%Fили выносить всё в скрипт. - Задача повисла.
mysqldumpждёт блокировку,rsyncзавис на сетевом диске, скрипт ждёт ввода с терминала. Cron в это время спокойно запускает следующие экземпляры — через сутки на сервере десять зависших процессов и ни одного бэкапа. - Сервер выключен или cron не запущен. После обновления
cron.serviceможет не подняться, а на VDS с пересозданием диска crontab уезжает вместе с системой. Никакой лог на этом сервере уже не поможет — нужен взгляд снаружи: хотя бы проверка доступности с внешних точек, чтобы за минуту отличить «сервер лёг» от «упала одна задача». - Часовой пояс. Старые версии cron берут часовой пояс на момент старта демона; сменили
/etc/localtime— задачи бегут по старому времени, пока cron не перезапущен. Новые cronie и Debian cron подхватывают смену сами, но перезапуск послеtimedatectl set-timezoneв любом случае не повредит.
Где искать следы: логи и коды выхода
Сначала убедитесь, что задача вообще стартовала. На Debian и Ubuntu cron пишет через syslog:
journalctl -u cron --since "yesterday" | grep backup
grep CRON /var/log/syslog | grep backup # файл есть, если установлен rsyslog; на Debian 12 по умолчанию только journald
На RHEL, AlmaLinux и Rocky — journalctl -u crond или /var/log/cron. Строка запуска выглядит так:
Sep 15 03:00:01 web1 CRON[18422]: (root) CMD (/usr/local/bin/backup.sh)
Если строки нет — задача не запускалась: проверьте systemctl status cron, crontab -l -u root и файлы в /etc/cron.d: они должны принадлежать root и быть без прав записи для группы и остальных (иначе в логе будет WRONG FILE OWNER или INSECURE MODE), а имя — состоять только из букв, цифр, _ и -: файл backup.sh cron на Debian пропустит молча, без единой строки в логе.
Если строка есть, а результата нет — задача упала или зависла. Код выхода cron по умолчанию не пишет (на Debian его включает -L 4, см. выше), поэтому надёжнее получить его самому. Быстрый способ прямо в crontab:
0 3 * * * /usr/local/bin/backup.sh >>/var/log/backup.log 2>&1; echo "exit=$? $(date +\%F_\%T)" >>/var/log/backup.log
Для зависших процессов смотрите ps -eo pid,etime,cmd | grep backup.sh — колонка etime покажет, сколько экземпляров и как давно висят.
systemd timers вместо cron
Если сервер на systemd, есть смысл перевести задачи на таймеры: они логируют результат, а не только факт запуска.
| cron | systemd timer | |
|---|---|---|
| Лог запуска | journalctl -u cron | journalctl -u backup.service |
| Код выхода | не пишется (Debian: -L 4) | в журнале: Main process exited, code=exited, status=1/FAILURE, Failed with result 'exit-code' |
| Защита от наложения | нет, нужен flock | второй запуск не стартует, пока идёт первый |
| Таймаут | нет | TimeoutStartSec= |
| Пропущенный запуск при выключенном сервере | теряется (нужен anacron) | Persistent=true — выполнится после включения |
| Список расписаний | crontab -l по каждому пользователю | systemctl list-timers --all |
Минимальная пара файлов:
; /etc/systemd/system/backup.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
TimeoutStartSec=2h
; /etc/systemd/system/backup.timer
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
После systemctl enable --now backup.timer неудачные запуски видны в systemctl --failed, а причина — в journalctl -u backup.service -n 50. Но и таймер не пришлёт уведомление сам: он лишь делает диагностику прозрачной.
Способы контроля на самом сервере
Четыре приёма ниже не требуют ничего, кроме самого сервера. У них общее слабое место: они живут там же, где задача, и ничего не скажут, если сервер выключен. Зато настраиваются за пять минут.
MAILTO и локальная почта
Самый дешёвый контроль — заставить cron присылать письма и сделать так, чтобы они доходили. В crontab:
MAILTO=admin@example.ru
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * /usr/local/bin/backup.sh
Письмо уйдёт только при непустом выводе, поэтому нормально работающий скрипт должен молчать, а падающий — писать в stderr. Для доставки достаточно лёгкого MTA-релея вроде msmtp (пакет msmtp-mta подставляет свой /usr/sbin/sendmail) или nullmailer, отправляющего через SMTP вашего почтового провайдера; ставить полноценный Postfix ради этого не нужно. Проверка: echo test | sendmail admin@example.ru и через минуту письмо во входящих.
Ограничения: письмо не придёт, если задача повисла (вывода нет), если сервер выключен, если релей сломался. И письма от cron легко тонут среди остальных — ошибка обнаружится через неделю при разборе почты.
Файл-маркер и проверка mtime
Идея: скрипт по окончании успешной работы обновляет файл, а отдельная проверка смотрит, не устарел ли он.
touch /var/lib/backup/last-ok # в конце backup.sh, только после успешного завершения
Проверка раз в час, которая ругается, если свежего маркера (моложе 25 часов) нет:
0 * * * * [ -n "$(find /var/lib/backup/last-ok -mmin -1500 2>/dev/null)" ] || echo "backup stale" | mail -s "backup stale on $(hostname)" admin@example.ru
Условие построено «нет свежего файла — тревога», а не «есть старый файл — тревога»: иначе отсутствие маркера (пересозданный сервер, ни одного успешного запуска) пройдёт незамеченным.
Тот же приём работает и с самим результатом: find /backup -name '*.sql.gz' -mmin -1500 -size +100k | grep -q . проверяет, что за сутки появился архив разумного размера (порог подберите под свою базу: -size у find округляет вверх, +1M отсечёт всё до 1 МиБ включительно). Это ловит случай «бэкап есть, но пустой» — из-за упавшего mysqldump при set +o pipefail в файл записываются 20 байт заголовка.
Слабое место — проверяющая задача живёт на том же сервере. Если он выключен или cron остановлен, проверять некому. И уведомление снова уходит через локальную почту.
Логирование с ротацией
Логи не заменяют уведомления, но без них в момент разбора вы будете гадать. Правило одно: каждый запуск пишет дату, результат и код выхода в свой файл, а logrotate не даёт ему разрастись. Файл /etc/logrotate.d/backup:
/var/log/backup.log {
weekly
rotate 8
compress
missingok
notifempty
}
Вариант без своих файлов — logger: backup.sh 2>&1 | logger -t backup отправляет вывод в journald, и потом его читают через journalctl -t backup --since -1d. Заодно логи попадают в общую ротацию журнала.
Обёртка с || notify
Чтобы не переписывать каждый скрипт, оберните запуск. Сохраните обёртку как /usr/local/bin/cronwrap; вызывается она так: cronwrap ИМЯ ТАЙМАУТ КОМАНДА.... Обёртка ставит блокировку от наложения, ограничивает время, пишет лог и шлёт уведомление при ненулевом коде:
#!/bin/bash
set -u
name=$1; limit=$2; shift 2
log=/var/log/cron-$name.log
lock=/run/lock/cron-$name.lock
notify() {
# свой канал: mail, curl в Telegram-бота или webhook
printf '%s\n' "$1" | mail -s "[$(hostname)] $name: $1" admin@example.ru
}
exec 9>"$lock"
if ! flock -n 9; then
notify "предыдущий запуск ещё идёт"
exit 1
fi
start=$(date +%s)
timeout --kill-after=60 "$limit" "$@" >>"$log" 2>&1
rc=$?
echo "$(date '+%F %T') rc=$rc took=$(( $(date +%s) - start ))s" >>"$log"
case $rc in
0) ;;
124) notify "убит по таймауту $limit" ;;
*) notify "завершился с кодом $rc, хвост лога: $(tail -n 3 "$log")" ;;
esac
exit $rc
В crontab остаётся одна строка:
0 3 * * * /usr/local/bin/cronwrap backup 2h /usr/local/bin/backup.sh
flock -n решает проблему накопления зависших экземпляров, timeout — проблему вечно висящей задачи: через два часа процесс получит SIGTERM, ещё через минуту — SIGKILL, а вы — письмо с кодом 124. Замените mail в notify() на curl к webhook или к боту в мессенджере, и уведомление перестанет зависеть от локальной почты. Внутри скрипта по-прежнему нужны set -eo pipefail, иначе ошибка в mysqldump | gzip даст код 0 от gzip.
Что обёртка всё равно не ловит: cron не запустил задачу, cron не работает, сервер выключен. Обёртка исполняется на том же сервере, что и задача, и разделяет его судьбу.
Обратная схема: отметка по URL после каждого успеха
Все способы выше «толкают» уведомление от сервера при ошибке. Heartbeat (его ещё называют dead man's switch) работает наоборот: задача при каждом успешном завершении отмечается на внешнем сервисе, а тот ждёт отметку с заданным периодом и допуском. Не пришла в срок — уведомление. Отсутствие сигнала само становится сигналом.
В cronwrap для этого достаточно одной строки в ветке 0):
0) curl -fsS -m 10 --retry 3 https://notifar.io/hb/ВАШ_ТОКЕН >/dev/null ;;
Именно так это устроено в Нотифарио: для монитора выдаётся уникальный адрес, задаётся ожидаемый период (например, раз в сутки) и допустимое опоздание, а уведомление уходит в Telegram, на почту, SMS или звонком. Важно ставить отметку только после успешного завершения, а не в начале скрипта, иначе упавший бэкап тоже «отметится».
Чем это лучше остального:
- Ловит «сервер выключен». Отметки нет, потому что некому её слать, — уведомление приходит с внешней стороны.
- Ловит «задача зависла». Зависший
rsyncне дойдёт доcurl, и через период плюс допуск вы об этом узнаете, даже безtimeout. - Ловит «cron не запустил». Отключённый демон, сломанные права на
/etc/cron.d, уехавший часовой пояс — всё выглядит одинаково: отметка не пришла. - Не зависит от почты сервера. Уведомление формирует внешняя система со своими каналами.
- Допуск на опоздание. Бэкап, задержавшийся на 15 минут из-за нагрузки, не разбудит ночью, если поставить допуск в час.
Чего heartbeat не делает: не показывает текст ошибки. Поэтому его сочетают с логом на сервере — уведомление говорит «бэкап не отметился», а journalctl -t backup объясняет почему. Период в Нотифарио задаётся интервалом (от минуты до суток) и допустимым опозданием, cron-выражений нет. Если задача бежит по сложному расписанию вроде «по будням в 9:00», где интервал не подходит, смотрите сравнение с Healthchecks.io — там та же heartbeat-механика, но с cron-выражениями. Для задач с периодом больше суток удобно отмечаться ежедневно из отдельной проверки, которая смотрит дату последнего успешного запуска по маркер-файлу.
Что ловит каждый способ
| Отказ | MAILTO | Маркер + mtime | Обёртка с notify | Heartbeat |
|---|---|---|---|---|
| Ошибка в скрипте, код ≠ 0 | да, если есть вывод | да | да | да |
| Задача повисла | нет | да | да, с timeout | да |
| Cron не запустил задачу | нет | да | нет | да |
| Cron остановлен | нет | нет | нет | да |
| Сервер выключен | нет | нет | нет | да |
| Локальная почта сломана | нет | нет | зависит от канала | да |
| Показывает текст ошибки | да | нет | да | нет |
Рабочая связка для важной задачи: обёртка с flock и timeout, лог с ротацией и heartbeat-отметка в конце. Если сервер под нагрузкой и задачи регулярно не укладываются в срок, добавьте контроль CPU, памяти и диска: часто «бэкап не выполнился» означает «кончилось место».
Коротко
- Cron по умолчанию логирует только факт запуска; код выхода и вывод команды надо сохранять самому — через перенаправление в лог или
logger. - Первая проверка при разборе:
journalctl -u cron(илиcrond) — была ли строкаCMD; вторая —psна зависшие экземпляры. - В crontab задавайте
PATHиMAILTO, экранируйте%, а в скриптах включайтеset -eo pipefail. - Systemd timers дают код выхода, таймаут и защиту от наложения из коробки, но уведомлений тоже не шлют.
- Обёртка с
flock,timeoutи|| notifyзакрывает ошибки и зависания, но не отказ самого сервера. - Heartbeat-отметка после успешного завершения — единственный способ узнать о выключенном сервере или остановленном cron.
Вопросы и ответы
Где cron хранит лог выполнения задач?
Почему скрипт работает из терминала, а из cron нет?
Как получать письмо от cron только при ошибке?
Что делать, если cron-задача запускается второй раз, пока не закончилась первая?
Что такое heartbeat-мониторинг и dead man's switch?
Как проверить, что бэкап действительно создался, а не пустой файл?
Не проверяйте вручную — следите автоматически
Нотифарио проверит сайт с нескольких точек и пришлёт уведомление в Telegram, MAX, по SMS или звонком, когда что-то сломается. Оплата только за проверки, 50 ₽ на старт.
Подключить бесплатно Проверить сайт сейчас