Планирование повторяющихся задач

TL;DR: Два механизма: cron (классический, формат * * * * * command) и systemd timers (современный, пара unit-файлов .timer + .service). cron проще для одноразовых задач, timers дают логирование через journald, зависимости и гарантию выполнения пропущенных запусков (Persistent=true).

Предварительные условия

  • cron: пакет cron (Debian/Ubuntu) или cronie (Arch/Fedora)
  • systemd timers: systemd (есть в любом современном дистрибутиве)

cron

Демон cron просыпается каждую минуту, проверяет расписания и выполняет команды от имени соответствующих пользователей.

Формат crontab

┌───────────── минута (0–59)
│ ┌───────────── час (0–23)
│ │ ┌───────────── день месяца (1–31)
│ │ │ ┌───────────── месяц (1–12 или jan–dec)
│ │ │ │ ┌───────────── день недели (0–7, 0 и 7 = воскресенье, или mon–sun)
│ │ │ │ │
* * * * *  command

Каждое поле допускает:

СинтаксисЗначениеПример
*Любое значение* * * * * — каждую минуту
5Конкретное5 * * * * — в :05 каждого часа
1,15Перечисление0 1,15 * * * — в 01:00 и 15:00
1-5Диапазон0 9 * * 1-5 — будни в 09:00
*/10Шаг*/10 * * * * — каждые 10 минут

Управление crontab

# Редактировать crontab текущего пользователя
crontab -e
 
# Показать текущий crontab
crontab -l
 
# Редактировать crontab другого пользователя (от root)
sudo crontab -u john -e
 
# Удалить crontab (осторожно — без подтверждения)
crontab -r

Каждый пользователь имеет свой crontab. Задачи выполняются от имени владельца crontab с его uid/gid и окружением.

Примеры

# Бэкап БД каждый день в 03:00
0 3 * * *  /opt/scripts/backup-db.sh >> /var/log/backup.log 2>&1
 
# Очистка tmp каждое воскресенье в 04:30
30 4 * * 0  find /tmp -type f -mtime +7 -delete
 
# Проверка SSL-сертификатов дважды в день
0 9,21 * * *  /opt/scripts/check-certs.sh
 
# Ротация логов каждый понедельник в 00:00
0 0 * * 1  /usr/sbin/logrotate /etc/logrotate.conf
 
# Каждые 5 минут (мониторинг)
*/5 * * * *  /opt/scripts/healthcheck.sh

Окружение cron

cron выполняет команды в минимальном окружении — не читает ~/.bashrc, ~/.profile. PATH обычно содержит только /usr/bin:/bin. Это главный источник проблем «работает в терминале, не работает в cron».

# Решение 1: абсолютные пути в команде
0 3 * * *  /usr/bin/python3 /opt/scripts/report.py
 
# Решение 2: задать PATH в crontab
PATH=/usr/local/bin:/usr/bin:/bin
0 3 * * *  python3 /opt/scripts/report.py
 
# Решение 3: задать переменные окружения
SHELL=/bin/bash
MAILTO=admin@example.com
0 3 * * *  /opt/scripts/backup.sh

MAILTO — cron отправляет stdout/stderr задачи на указанный email. Если MAILTO="" — вывод отбрасывается. Без MAILTO — отправляет владельцу crontab (если настроен MTA).

Системный crontab: /etc/crontab и /etc/cron.d/

Помимо пользовательских crontab, существуют системные:

# /etc/crontab — системный crontab, содержит дополнительное поле: имя пользователя
# мин час день мес dow  user  command
  0   3   *   *   *    root  /opt/scripts/system-backup.sh
 
# /etc/cron.d/ — директория для drop-in файлов (формат как /etc/crontab)
# /etc/cron.daily/   — скрипты, запускаемые ежедневно
# /etc/cron.hourly/  — ежечасно
# /etc/cron.weekly/  — еженедельно
# /etc/cron.monthly/ — ежемесячно

Директории cron.daily/, cron.hourly/ и т.д. — это обычные скрипты (без crontab-формата), которые запускает anacron или cron через записи в /etc/crontab. Скрипты должны быть исполняемыми и без расширения (файл backup, а не backup.sh — некоторые реализации cron игнорируют файлы с точкой в имени).

Ограничение доступа

# /etc/cron.allow — только перечисленные пользователи могут использовать cron
# /etc/cron.deny  — запрет для перечисленных (если cron.allow не существует)
 
# Если оба файла отсутствуют — поведение зависит от дистрибутива:
#   Debian: все могут использовать cron
#   Red Hat: только root

systemd timers

Пара unit-файлов: .timer (расписание) + .service (действие). Timer активирует соответствующий service по расписанию.

Структура

/etc/systemd/system/
├── backup.timer        ← расписание
└── backup.service      ← что выполнять

Timer и service связываются по имени: backup.timer активирует backup.service. Если нужно другое имя — указать Unit= в секции [Timer].

Пример: ежедневный бэкап

# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup timer
 
[Timer]
OnCalendar=*-*-* 03:00:00    # каждый день в 03:00
Persistent=true               # выполнить при загрузке, если пропущен
RandomizedDelaySec=600         # случайная задержка 0–10 мин (разнести нагрузку)
 
[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Database backup
After=postgresql.service       # запускать после PostgreSQL
 
[Service]
Type=oneshot                   # выполнить и завершиться
User=backup                   # от какого пользователя
ExecStart=/opt/scripts/backup-db.sh
StandardOutput=journal         # логи в journald
# Активировать
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
 
# Проверить
systemctl list-timers
# NEXT                        LEFT     LAST                        PASSED   UNIT
# Wed 2026-02-19 03:00:00 MSK 14h left Tue 2026-02-18 03:04:23 MSK 9h ago  backup.timer
 
# Логи последнего запуска
journalctl -u backup.service -n 20
 
# Запустить вручную (для теста)
sudo systemctl start backup.service

Формат OnCalendar

DayOfWeek Year-Month-Day Hour:Minute:Second
ЗначениеЭквивалент cronОписание
*-*-* 03:00:000 3 * * *Ежедневно в 03:00
Mon *-*-* 00:00:000 0 * * 1Каждый понедельник
*-*-* *:00:000 * * * *Каждый час
*-*-* *:*:00* * * * *Каждую минуту
*-*-* 09,21:00:000 9,21 * * *В 09:00 и 21:00
Mon..Fri *-*-* 09:00:000 9 * * 1-5Будни в 09:00
*-*-01 00:00:000 0 1 * *Первое число каждого месяца
*-01,07-01 00:00:000 0 1 1,7 *1 января и 1 июля
quarterlyРаз в квартал
weekly0 0 * * 0Еженедельно (понедельник, 00:00)
daily0 0 * * *Ежедневно в 00:00
hourly0 * * * *Каждый час
# Проверить расписание (покажет следующие запуски)
systemd-analyze calendar "*-*-* 03:00:00"
systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"
systemd-analyze calendar "weekly"

Монотонные таймеры (относительные интервалы)

Вместо календарного расписания — интервал от события:

[Timer]
OnBootSec=5min              # через 5 минут после загрузки
OnUnitActiveSec=1h          # каждый час после последнего запуска service
OnStartupSec=10min          # через 10 минут после запуска systemd (user instance)
ПараметрОтсчёт от
OnBootSecЗагрузка системы
OnStartupSecЗапуск user instance systemd
OnUnitActiveSecПоследняя активация service
OnUnitInactiveSecПоследняя деактивация service
OnActiveSecАктивация самого timer
# Пример: healthcheck каждые 5 минут
[Timer]
OnBootSec=1min               # первый запуск через минуту после загрузки
OnUnitActiveSec=5min          # затем каждые 5 минут

Persistent=true

Если система была выключена в момент запланированного выполнения, timer с Persistent=true запустит задачу при следующей загрузке. Информация о последнем запуске хранится на диске.

cron не имеет такой возможности — пропущенные задачи просто не выполняются (для этого в cron-мире существует anacron, но он работает только с daily/weekly/monthly интервалами).

cron vs systemd timers

cronsystemd timers
КонфигурацияОдна строкаДва unit-файла
Логированиеstdout/stderr → email или /dev/nulljournald (journalctl -u service)
ЗависимостиНетAfter=, Requires=, Wants=
Пропущенный запускПотерян (без anacron)Persistent=true
ТочностьМинутаМикросекунда (на практике — секунда)
РесурсыНе контролируетMemoryMax=, CPUQuota=, Nice= через cgroup
РандомизацияНетRandomizedDelaySec=
ТестированиеЖдать или менять расписаниеsystemctl start service
Мониторингcrontab -l, нет статуса выполненияsystemctl list-timers, systemctl status
Когда использоватьБыстрые одноразовые задачи, скриптыProduction-сервисы, задачи с зависимостями

Когда что выбирать

cron — когда нужно быстро добавить задачу: crontab -e, одна строка, готово. Для личных скриптов, простых задач без зависимостей.

systemd timers — когда важны: логирование, обработка ошибок, зависимости от других сервисов, контроль ресурсов, гарантия выполнения после простоя. Для production-серверов и инфраструктурных задач.

Пример: миграция с cron на systemd timer

Crontab-строка:

0 3 * * * /opt/scripts/cleanup.sh >> /var/log/cleanup.log 2>&1

Эквивалент в systemd:

# /etc/systemd/system/cleanup.timer
[Unit]
Description=Daily cleanup
 
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
 
[Install]
WantedBy=timers.target
# /etc/systemd/system/cleanup.service
[Unit]
Description=Cleanup old files
 
[Service]
Type=oneshot
ExecStart=/opt/scripts/cleanup.sh
# Логи автоматически в journald — не нужен >> file.log 2>&1
sudo systemctl daemon-reload
sudo systemctl enable --now cleanup.timer
 
# Убрать из cron
crontab -e  # удалить строку

Типичные ошибки

ОшибкаСимптомРешение
Относительный путь в croncommand not foundАбсолютные пути или задать PATH= в crontab
Скрипт работает в терминале, не в cronНет переменных окруженияcron не читает .bashrc. Задать переменные в crontab или в скрипте
% в команде cronКоманда обрезается% в cron — перевод строки. Экранировать: \%
Нет логов задачи cronНе знаешь, выполнилась ли>> /var/log/task.log 2>&1 или перейти на systemd timer
Timer enabled, service не запускаетсяsystemctl list-timers показывает timersystemctl status service — проверить ошибки. daemon-reload после изменений
Задача пропущена (сервер был выключен)Бэкап не созданcron: использовать anacron. Timers: Persistent=true
Две копии задачи одновременноПредыдущий запуск не завершилсяcron: flock (flock -n /tmp/task.lock /opt/scripts/task.sh). Timers: service по умолчанию не запускается повторно, пока предыдущий активен

Связанные материалы