Выбор между cron и таймерами и отладка пропущенных задач
cron побеждает по переносимости и простоте; таймеры systemd — по логированию, зависимостям и наверстыванию. Отладка пропущенной задачи: неверный пользователь, минимальный PATH, неэкранированный % в cron, часовой пояс/DST, отсутствие переноса строки в конце, таймер не включён.
09:00, понедельник. Ночная задача резервного копирования, которая должна была запуститься в 02:00, не дала ни вывода, ни записи в лог, ни ошибки. Строка crontab выглядит верно. Скрипт прекрасно работает из оболочки. Эта ситуация — запланированная задача, которая молча ничего не сделала, — одна из самых раздражающих в операционной практике, потому что не за чем гнаться. Задача не завершилась с ошибкой — она никогда не была вызвана. Системная проверка шести наиболее частых причин занимает десять минут. Угадывание без чеклиста занимает часы. Этот урок даёт тебе чеклист, логику за каждым пунктом и критерии выбора между cron и таймерами systemd — чтобы взять правильный инструмент до инцидента.
После этого урока ты сможешь выбирать между cron и таймерами systemd на основе требований к логированию, зависимостям и переносимости, а также системно отлаживать пропущенную задачу, перебирая шесть наиболее частых причин: неверный пользователь, минимальный PATH, неэкранированный %, часовой пояс/DST, отсутствие символа новой строки в конце crontab и не включённый таймер.
cron vs. таймеры systemd: честный компромисс. Ни один инструмент не лучше универсально. Правильный выбор зависит от системы и от того, что нужно задаче.
cron:
+ Переносимый — работает на любой POSIX-системе: macOS, BSD, контейнеры без systemd
+ Одна строка конфигурации — никакого двухфайлового ритуала
+ Знаком — любой системный администратор знает синтаксис crontab
- Нет логирования — вывод идёт на email или в /dev/null
- Нет порядка зависимостей — нельзя сказать «запустить после network.target»
- Нет наверстывания пропущенных запусков (без anacron)
- Минимальное окружение — источник ошибок
Таймеры systemd:
+ Полное логирование journald — каждый запуск захвачен, с поиском, структурированно
+ Порядок зависимостей — After=, Wants=, BindsTo= работают в обычном режиме
+ Persistent=true для наверстывания после простоя
+ RandomizedDelaySec для распределения нагрузки
+ systemctl status и list-timers для видимости
- Только systemd — бесполезен на Alpine, BSD, старых контейнерах
- Двухфайловый шаблон для каждой задачи
- После каждого изменения нужен daemon-reloadПравило большого пальца: используй cron для простых, переносимых, некритичных задач, где важна переносимость (виртуальный хостинг, контейнеры, скрипты, которые должны работать и на macOS). Используй таймеры systemd для всего на полноценном Linux-хосте с systemd, где важны логирование, наверстывание или зависимости — а это большинство продакшн-задач.
Причина 1 — неверный пользователь. Пользовательский crontab выполняется от имени этого пользователя. Корневой crontab — от имени root. Записи /etc/cron.d/ указывают пользователя явно. Несоответствие означает, что команда либо завершается с ошибкой (доступ запрещён), либо запускается в неожиданном контексте.
# Проверить, чей crontab содержит задачу:
crontab -l # твой собственный
sudo crontab -u alice -l # crontab alice
sudo crontab -l # crontab root
# В записях /etc/cron.d/ поле USER — шестой столбец:
grep backup /etc/cron.d/*
# /etc/cron.d/myapp: 0 2 * * * www-data /opt/app/backup.sh
# Проверить, была ли задача вообще вызвана (через спул почты или auth log):
sudo grep CRON /var/log/syslog | grep -i backup
# Jun 21 02:00:01 host CRON[3821]: (www-data) CMD (/opt/app/backup.sh)
# Эта строка означает, что cron ДЕЙСТВИТЕЛЬНО вызвал задачу — проблема в скриптеЕсли строка CRON есть в syslog — задача была вызвана, сбой в скрипте. Если строки нет — cron никогда её не запускал.
Причина 2 — минимальный PATH; Причина 3 — неэкранированный % в cron. Два самых частых молчаливых сбоя.
# Ловушка PATH: скрипт использует бинарник не из /usr/bin:/bin
# Отладка: добавить явную строку PATH в crontab и логировать PATH при запуске
PATH=/usr/local/bin:/usr/bin:/bin
0 2 * * * env >> /tmp/cron-env.log && /opt/app/backup.sh >> /tmp/backup.log 2>&1
# Теперь /tmp/cron-env.log показывает точное окружение, используемое cron
# Ловушка с процентом: формат даты с % в команде crontab
# НЕВЕРНО — % становится переносом строки; команда усекается:
0 2 * * * /usr/bin/backup.sh > /tmp/backup-$(date +%Y%m%d).log
# ВЕРНО — экранировать каждый %:
0 2 * * * /usr/bin/backup.sh > /tmp/backup-$(date +\%Y\%m\%d).log
# Подтвердить, выполнив точно ту команду, которую запустил бы cron:
sudo -u www-data env -i HOME=/var/www SHELL=/bin/sh PATH=/usr/bin:/bin /bin/sh -c '/opt/app/backup.sh'Трюк с env -i убирает твоё текущее окружение и запускает команду в голой оболочке — это максимально близко к воспроизведению окружения cron без ожидания самого cron.
Причина 4 — часовой пояс и DST; Причина 5 — отсутствие символа новой строки в конце crontab. Две менее очевидные ловушки.
# Часовой пояс: crond использует системный часовой пояс, если CRON_TZ не задан в crontab
# Проверить системный часовой пояс:
timedatectl status | grep "Time zone"
# Если система работает в UTC, а задача написана с расчётом на локальное время — она запустится в неверный час
# Исправление: задать CRON_TZ явно в crontab:
CRON_TZ=Europe/Moscow
0 2 * * * /usr/bin/backup.sh
# Ловушка DST: задача в 02:30 в ночь перевода часов назад запустится ДВАЖДЫ (02:30 будет дважды)
# Задача в 02:30 в ночь перевода часов вперёд будет ПРОПУЩЕНА (02:30 не существует)
# Не планируй критичные задачи в промежутке 01:00–03:00, если применяется DST
# Отсутствие переноса строки в конце: некоторые реализации cron молча игнорируют ПОСЛЕДНЮЮ СТРОКУ
# crontab, если она не заканчивается переносом строки. crontab -e добавляет его автоматически,
# но если скопировать в файл и установить через:
# crontab < /tmp/my-crontab
# ...последняя задача может никогда не запуститься. Всегда проверяй:
crontab -l | xxd | tail -3
# ...должно заканчиваться 0a (байт переноса строки)Причина 6 — таймер не включён (systemd). Самый частый сбой таймера systemd — таймер существует, но не включён: он отображается в list-unit-files как disabled и никогда не срабатывает.
# Проверить статус таймера:
systemctl status db-backup.timer
# Loaded: loaded (/etc/systemd/system/db-backup.timer; disabled; ...)
# ^^^^^^^^
# 'disabled' означает, что он не переживёт перезагрузку и может не быть активным сейчас
# Разница:
# 'enabled' = установлен для запуска при загрузке (symlink в timers.target.wants/)
# 'active' = работает/ожидает прямо сейчас
# Таймер может быть active (запущен вручную), но disabled (не перезапустится после перезагрузки)
systemctl is-enabled db-backup.timer # enabled / disabled / static
systemctl is-active db-backup.timer # active / inactive / failed
# Исправление: включить И запустить одной командой:
systemctl enable --now db-backup.timer
# Также: daemon-reload обязателен после любого изменения файла юнита
systemctl daemon-reload
systemctl restart db-backup.timer
# Полная последовательность проверки после пропущенного таймера:
systemctl list-timers --all | grep db-backup
journalctl -u db-backup.service -b --since "yesterday"
journalctl -u db-backup.timer -b --since "yesterday"Инцидент: ночное резервное копирование не запускается три дня. Оповещений не было.
# Шаг 1: определить планировщик
crontab -l | grep backup
# 0 2 * * * /opt/backup/run.sh > /var/log/backup-$(date +%Y%m%d).log 2>&1
# На основе таймера? Проверить:
systemctl list-timers | grep backup
# (нет вывода — это задача cron)
# Шаг 2: вызывал ли cron задачу?
sudo grep CRON /var/log/syslog | grep backup | tail -5
# (строк за последние 3 дня нет — cron не вызывал задачу)
# Шаг 3: проверить, не был ли случайно удалён crontab
crontab -l
# no crontab for deploy
# У пользователя 'deploy' нет crontab! Кто-то запустил 'crontab -r' или файл потерян.
# Шаг 4: восстановить из системы контроля версий или резервной копии
# (теперь команда хранит crontab в /etc/cron.d/ именно по этой причине)
# Но в исходной строке есть вторая ошибка:
# date +%Y%m%d содержит неэкранированные % — имя лог-файла было бы неверным.
# Команда после первого % становится stdin, поэтому имя файла —
# просто /var/log/backup- с отсечённым хвостом.
# Исправленная запись crontab:
0 2 * * * /opt/backup/run.sh >> /var/log/backup-\$(date +\%Y\%m\%d).log 2>&1
# Шаг 5: перейти на /etc/cron.d/, чтобы удаление учётной записи не уничтожало задачу:
cat > /etc/cron.d/backup << 'EOF'
0 2 * * * deploy /opt/backup/run.sh >> /var/log/backup-$(date +\%Y\%m\%d).log 2>&1
EOF
# Примечание: в файлах /etc/cron.d/ % всё равно нужно экранировать как \%Первопричины: (1) crontab -r молча удалил пользовательский crontab, (2) исходная команда содержала неэкранированные %, что дало бы повреждённое имя лог-файла. Исправление: перейти на /etc/cron.d/ (переживает управление пользователями) и экранировать %.
▸Почему это работает
Почему у cron нет логирования по умолчанию. cron появился до стандарта syslog и был спроектирован для мира, где у каждого пользователя был локальный почтовый ящик. Вывод идёт в локальный спул почты (/var/mail/<user>), который никто не читает на современных системах. Практическое следствие: каждая задача cron должна перенаправлять собственный вывод (>> /var/log/myjob.log 2>&1) или использовать скрипт-обёртку, который ведёт лог. Таймеры systemd обходят это полностью — журнал захватывает всё, что процесс сервиса пишет в stdout/stderr, без какого-либо перенаправления.
▸Частая ошибка
Планировать критичные задачи во время перехода на летнее время (01:00–03:00). В ночь перевода часов назад (осень) 02:30 наступает дважды — задача cron в это время запустится дважды. В ночь перевода часов вперёд (весна) 02:30 не существует — задача будет пропущена. Оба поведения неожиданны в продакшне. Исправление: планировать ночные задачи вне окна DST (00:00–01:00 или 03:30–23:00) или задать CRON_TZ=UTC, чтобы задача не зависела от часового пояса. Таймеры systemd имеют ту же проблему с OnCalendar, если не использовать UTC в выражении.
Ты пишешь новый таймер systemd: `db-backup.timer` с `OnCalendar=daily`. Запускаешь `systemctl start db-backup.timer`, он отображается как активный. Через три дня он ни разу не сработал. Какова наиболее вероятная причина?
cron побеждает по переносимости и простоте (одна строка конфигурации, работает везде); таймеры systemd — по наблюдаемости и надёжности (логирование в журнал, Persistent=true, порядок зависимостей). На любом полноценном systemd-хосте для важных задач предпочитай таймеры. Когда задача молча ничего не делает — работай по чеклисту: (1) неверный пользователь — проверить владельца через crontab -u или поле пользователя в /etc/cron.d/; (2) минимальный PATH — использовать абсолютные пути к бинарникам или задать PATH= в начале crontab; (3) неэкранированный % — каждый % в команде cron должен быть \%; (4) часовой пояс/DST — задать CRON_TZ=UTC или избегать промежутка 01:00–03:00; (5) отсутствие символа новой строки в конце — последняя строка crontab должна заканчиваться \n; (6) таймер не включён — systemctl is-enabled должен вернуть enabled, а daemon-reload должен быть выполнен после любого изменения файла юнита.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.