open atlas
↑ К треку
Linux: операционная система LIN · 11 · 03

Выбор между cron и таймерами и отладка пропущенных задач

cron побеждает по переносимости и простоте; таймеры systemd — по логированию, зависимостям и наверстыванию. Отладка пропущенной задачи: неверный пользователь, минимальный PATH, неэкранированный % в cron, часовой пояс/DST, отсутствие переноса строки в конце, таймер не включён.

LIN Senior ◷ 24 min
Уровень
ОсновыJuniorMiddleSenior

09:00, понедельник. Ночная задача резервного копирования, которая должна была запуститься в 02:00, не дала ни вывода, ни записи в лог, ни ошибки. Строка crontab выглядит верно. Скрипт прекрасно работает из оболочки. Эта ситуация — запланированная задача, которая молча ничего не сделала, — одна из самых раздражающих в операционной практике, потому что не за чем гнаться. Задача не завершилась с ошибкой — она никогда не была вызвана. Системная проверка шести наиболее частых причин занимает десять минут. Угадывание без чеклиста занимает часы. Этот урок даёт тебе чеклист, логику за каждым пунктом и критерии выбора между cron и таймерами systemd — чтобы взять правильный инструмент до инцидента.

Цель

После этого урока ты сможешь выбирать между cron и таймерами systemd на основе требований к логированию, зависимостям и переносимости, а также системно отлаживать пропущенную задачу, перебирая шесть наиболее частых причин: неверный пользователь, минимальный PATH, неэкранированный %, часовой пояс/DST, отсутствие символа новой строки в конце crontab и не включённый таймер.

1

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, где важны логирование, наверстывание или зависимости — а это большинство продакшн-задач.

2

Причина 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 никогда её не запускал.

3

Причина 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

Причина 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 (байт переноса строки)
5

Причина 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-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.