Инструкции

Как узнать, что 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, есть смысл перевести задачи на таймеры: они логируют результат, а не только факт запуска.

cronsystemd timer
Лог запускаjournalctl -u cronjournalctl -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Обёртка с notifyHeartbeat
Ошибка в скрипте, код ≠ 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 хранит лог выполнения задач?
На Debian и Ubuntu — в journald (journalctl -u cron) и в /var/log/syslog, если установлен rsyslog; на RHEL-семействе — journalctl -u crond и /var/log/cron. По умолчанию там только строки запуска с именем пользователя и командой; вывод и код выхода cron не сохраняет.
Почему скрипт работает из терминала, а из cron нет?
Cron не читает .bashrc и .profile: из окружения есть только HOME, LOGNAME, SHELL и PATH (обычно /usr/bin:/bin), оболочка /bin/sh. Задайте PATH прямо в crontab, используйте абсолютные пути и добавьте #!/bin/bash в скрипт, если нужен bash-синтаксис.
Как получать письмо от cron только при ошибке?
Cron отправляет письмо при любом непустом выводе. Сделайте так, чтобы при успехе скрипт молчал (stdout в лог-файл), а при ошибке писал в stderr, либо используйте обёртку, которая шлёт письмо только при ненулевом коде выхода.
Что делать, если cron-задача запускается второй раз, пока не закончилась первая?
Оберните команду в flock -n с файлом блокировки: второй экземпляр сразу завершится с кодом 1 вместо того, чтобы копить зависшие процессы. В systemd timers наложение исключено само по себе.
Что такое heartbeat-мониторинг и dead man's switch?
Это обратная схема контроля: задача при успешном завершении отправляет запрос на уникальный URL, а внешний сервис ждёт его с заданным периодом. Если запрос не пришёл в срок, приходит уведомление. Так обнаруживаются зависшие задачи, остановленный cron и выключенный сервер.
Как проверить, что бэкап действительно создался, а не пустой файл?
Смотрите не только время файла, но и размер: find /backup -name '*.sql.gz' -mmin -1500 -size +100k (порог подберите под свою базу). Дополнительно раз в неделю разворачивайте архив на тестовой базе — только это подтверждает, что бэкап пригоден.

Не проверяйте вручную — следите автоматически

Нотифарио проверит сайт с нескольких точек и пришлёт уведомление в Telegram, MAX, по SMS или звонком, когда что-то сломается. Оплата только за проверки, 50 ₽ на старт.

Подключить бесплатно Проверить сайт сейчас