Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Cron и systemd timers: планировщики задач

Планировщики в Linux: cron против systemd timers на практике

Как запускать задачи по расписанию: синтаксис crontab и его ловушки, systemd timers с календарём и Persistent=true, логирование, блокировки от наложений и выбор инструмента.

Прямой ответ

Задачи по расписанию на Linux-сервере решаются двумя инструментами: cron — минималистичный ветеран (crontab -e, строка 15 3 * * * /opt/scripts/backup.sh — ежедневно в 03:15), работает везде и не требует systemd; systemd timers — современная замена: юнит-сервис с командой + юнит-таймер с расписанием (OnCalendar=*-*-* 03:15:00), где каждая задача — полноценный сервис с логированием в journald, зависимостями, лимитами ресурсов и перезапусками. Практический выбор: на сервере с systemd таймеры предпочтительнее для всего нового — три причины: вывод задачи логируется автоматически (у cron — «письма на локальную почту, которых никто не читает»), Persistent=true догоняет пропущенные запуски (ноутбук/сервер спал в 03:15 — задача выполнится после пробуждения; cron просто промолчит), а статус виден штатно (systemctl list-timers — когда последний/следующий запуск каждой задачи — у cron этой видимости нет). Критичное правило для обоих: задача обязана быть идемпотентной и защищённой от наложений (предыдущий запуск ещё идёт, когда сработал следующий — flock у cron, встроенная serial-семантика у таймеров), иначе «ночной бэкап» однажды запускается в трёх экземплярах и съедает диск.

Cron: синтаксис и его ловушки

Формат строки: минута час день месяц день_недели команда (0 */6 * * * — каждые 6 часов; 30 2 * * 1 — понедельник 02:30; @daily — алиас). Ловушки, на которые наступает каждый: окружение голое (PATH из трёх каталогов, нет ваших переменных — команды абсолютными путями или явный PATH=/EnvironmentFile в обёртке; «вручную работает, из cron — command not found» — оно самое); нет логирования (stdout идёт в локальную почту — перенаправляйте явно: >> /var/log/task.log 2>&1, ещё лучше — обёртка-скрипт с датами); проценты экранируются (\% в команде — спецсимвол cron); день месяца И день недели — условие ИЛИ (неочевидно: 0 0 13 * 5 — каждое 13-е число И каждая пятница); переменные окружения не подставляются из shell профиля (crontab — не интерактивная сессия). Отдельные грабли — часовые пояса (cron работает в TZ системы; переезд сервера в UTC меняет «ночные» задачи) и отсутствие блокировки параллельных запусков (долгая задача + частое расписание = наложения — flock -n /var/lock/task.lock /opt/scripts/task.sh).

Systemd timers: сервис + расписание

Пара юнитов (общая база — systemd-юниты): backup.service — что делать (ExecStart=/opt/scripts/backup.sh, User=backup, лимиты MemoryMax=), backup.timer — когда ([Timer] OnCalendar=*-*-* 03:15 + Persistent=true; [Install] WantedBy=timers.target). Включение: systemctl enable --now backup.timer; инспекция: systemctl list-timers (последний/следующий запуск всех таймеров — главный операционный выигрыш), journalctl -u backup.service (логи всех запусков с таймстампами — автоматически). Сильные стороны против cron: Persistent (пропущенное догоняется), RandomizedDelaySec (размазывание пика — тысячи серверов не бьют в одну секунду), OnBootSec/OnUnitActiveSec (относительные расписания — «через 10 минут после старта»), изоляция и лимиты сервиса (задача не съест всю память — systemd прибьёт по MemoryMax), зависимости (After=network-online.target — задача не стартует до сети; cron слепо запускает в момент). Точность календаря проверяется без запуска: systemd-analyze calendar "*-*-* 03:15" показывает следующие срабатывания — синтаксис OnCalendar богаче cron-строк и не требует memorize порядка полей.

Надёжность задачи: независимо от планировщика

Планировщик лишь дёргает — надёжность живёт в самой задаче (bash-практики). Блокировка наложений: flock (cron) или природа таймера (сервис не запускается повторно, пока активен — systemctl start во время работы отклоняется). Обработка ошибок: set -euo pipefail в скрипте; ненулевой код — заметен (journald зафиксирует failed; для cron — проверка кода в обёртке). Алерт при провале: «задача молча не работает» хуже отсутствия задачи — healthchecks-паттерн: задача пингует внешний мониторинг (healthchecks.io-подобный: нет пинга — алерт «не запустилась»; есть failed-статус — алерт «упала»); или systemd OnFailure=notify@%n.service — юнит-нотификатор шлёт письмо/вебхук. Идемпотентность: повторный запуск не должен портить данные (бэкап в тот же файл — перезапись атомарно через tmp+mv; «начислить бонусы» — с ключом идемпотентности по дате). Часовой пояс явно: TZ в конфиге задачи/юнита, не надежда на системный. И документируемость: юниты/крон-строки — в git через ansible (сервер без «магических» правок руками — воспроизводимость).

Практический сценарий: ночной бэкап, который действительно работает

Задача: бэкап Postgres + файлов в 03:15, хранение 14 дней, алерты. Реализация таймером: pg-backup.service — скрипт с set -euo pipefail, pg_dump в tmp-файл + атомарный mv, ротация старых (find -mtime +14 -delete), пинг healthchecks в конце; pg-backup.timer — OnCalendar=*-*-* 03:15, Persistent=true, RandomizedDelaySec=300; юниты — из ansible-роли. Проверки при внедрении: systemd-analyze calendar — расписание читается как задумано; ручной systemctl start pg-backup.service — отработал, лог в journald, пинг дошёл; имитация провала (сломанный пароль БД) — сервис failed, healthchecks молчит → алерт «backup missed» через сутки — цепочка уведомления подтверждена (без проверки «тихая смерть» обнаружится через месяц, когда бэкап понадобится). Эксплуатационная рутина: systemctl list-timers в еженедельном обходе — LastTriggered у всех задач свежий; миграция старых cron-задач — по мере касания (трогаешь крон-строку — переносишь на таймер: логирование и Persistent окупаются сразу).

FAQ

Cron умер? Нужно ли мигрировать всё? Не умер: для простых периодических задач на любых системах (включая контейнеры без systemd) cron жив и достаточен; мигрировать «всё сразу» смысла нет — новые задачи на systemd-хостах сразу таймерами, старые — при касании. В контейнерах планировщик обычно внешний (K8s CronJob, CI-schedule), не cron внутри.

Что лучше для запуска каждые 10 секунд? Ни то, ни другое: минимальный шаг cron — минута; таймеры умеют OnUnitActiveSec=10s, но для частых задач правильнее постоянно живущий сервис со своим циклом (systemd-сервис с Restart=always и sleep в цикле) — планировщик для расписаний, не для «почти демона».

Как запускать от другого пользователя? Cron: crontab -u backup -e (или системный /etc/cron.d/ с полем пользователя); таймеры: User= в сервисе — предпочтительнее (плюс isolation-директивы: ProtectSystem, PrivateTmp — задача физически не может испортить лишнее).

Задача не запускается — чек-лист? Таймер: systemctl status *.timer (active? enabled?), journalctl -u task.service (ошибки запуска), systemd-analyze calendar (расписание); cron: grep CRON /var/log/syslog (срабатывал ли), права/абсолютные пути, MAILTO-почта с ошибками. В половине случаев — задача запускалась, но падала молча: отсюда правило явного логирования.

Источники: man crontab(5), документация systemd.timer и systemd.time — ссылки в шапке.