Почему systemd timers выигрывают у cron
Cron выполняет задачу по расписанию и забывает о ней. Если сервер был выключен в момент срабатывания — запуск потерян, и вы об этом не узнаете. Логи нужно собирать отдельно, зависимости между задачами приходится эмулировать скриптами, а диагностика сводится к чтению
/var/log/syslog или grep CRON.systemd timers решают эти проблемы на уровне архитектуры:
- Пропущенные запуски — директива
Persistent=trueзапускает задачу при следующей загрузке, если предыдущий запуск был пропущен.
- Зависимости — таймер активирует сервисный юнит, а значит можно использовать
After=,Requires=,Wants=для контроля порядка.
- Логирование — весь вывод задачи автоматически попадает в journald, доступен через
journalctl -uбез дополнительной настройки.
- Мониторинг —
systemctl list-timersпоказывает время последнего и следующего запуска каждого таймера.
Архитектура: два юнита на одну задачу
Каждая задача состоит из двух файлов:
| Файл | Назначение |
|---|---|
имя.timer | Определяет расписание и условия срабатывания |
имя.service | Описывает, что именно выполнять |
Имена должны совпадать:
backup.timer активирует backup.service. Таймер не выполняет команды сам — он лишь запускает соответствующий сервис.Структура сервисного юнита
Сервисный юнит для таймера отличается от обычного: ему не нужен секция
[Install], потому что активацией управляет таймер.
INI:
## /etc/systemd/system/backup.service
[Unit]
Description=Ежедневный бэкап базы данных
[Service]
Type=oneshot
ExecStart=/usr/local/bin/db-backup.sh
User=backup
Group=backup
Nice=10
IOSchedulingClass=idle
Ключевые моменты:
Type=oneshot— systemd считает задачу завершённой, когда процесс изExecStartзавершится. Это стандарт для задач по расписанию.
User=иGroup=— запуск от непривилегированного пользователя безsudo.
Nice=иIOSchedulingClass=— снижение приоритета, чтобы фоновая задача не мешала основному сервису.
Структура таймерного юнита
INI:
## /etc/systemd/system/backup.timer
[Unit]
Description=Запуск бэкапа ежедневно в 03:00
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
Директивы расписания
| Директива | Смысл | Пример |
|---|---|---|
OnCalendar | Абсолютное время по календарю | *-*-* 03:00:00 — ежедневно в 03:00 |
OnBootSec | Относительно момента загрузки | OnBootSec=15min — через 15 минут после старта |
OnUnitActiveSec | Относительно последнего запуска сервиса | OnUnitActiveSec=1h — каждый час после завершения |
OnUnitInactiveSec | Относительно момента, когда сервис стал неактивен | OnUnitInactiveSec=30min |
OnCalendar поддерживает сокращения: hourly, daily, weekly, monthly. Полный список — в man systemd.time.Persistent=true
Если машина была выключена в момент, когда таймер должен был сработать, systemd зафиксирует пропуск и запустит задачу при следующей загрузке. Без этой директивы пропущенный запуск просто теряется — как в cron.
RandomizedDelaySec
Добавляет случайную задержку до указанного значения. Полезно, когда несколько серверов с одинаковым расписанием одновременно нагружают общий ресурс (например, репозиторий или API).
Активация и проверка
Bash:
## Перечитать конфигурацию после создания/изменения юнитов
sudo systemctl daemon-reload
## Включить таймер (стартует при загрузке)
sudo systemctl enable backup.timer
## Запустить таймер немедленно (для проверки)
sudo systemctl start backup.timer
## Посмотреть список всех таймеров с временем последнего и следующего запуска
systemctl list-timers --all
## Проверить статус конкретного таймера
systemctl status backup.timer
Вывод
systemctl list-timers содержит столбцы NEXT (следующий запуск), LEFT (сколько осталось), LAST (последний запуск), PASSED (сколько прошло), UNIT и ACTIVATES.Логирование через journalctl
Весь stdout и stderr сервиса автоматически пишется в journald. Никаких перенаправлений в файл не нужно:
Bash:
## Логи последнего запуска backup.service
journalctl -u backup.service --since "1 hour ago"
## Логи за сегодня
journalctl -u backup.service --since today
## Непрерывное слежение (аналог tail -f)
journalctl -u backup.service -f
Если нужно ограничить объём хранения логов, это настраивается глобально в
/etc/systemd/journald.conf через SystemMaxUse= и MaxRetentionSec=.Зависимости между задачами
Предположим, бэкап должен запускаться только после того, как база данных полностью стартовала. В сервисном юните:
INI:
[Unit]
Description=Бэкап PostgreSQL
After=postgresql.service
Requires=postgresql.service
After=— гарантирует порядок: бэкап не начнётся, пока postgresql не будет активен.
Requires=— если postgresql остановлен, бэкап тоже не запустится. Если нужна мягкая зависимость (запустить, даже если БД не поднялась), используйтеWants=.
Для цепочек из нескольких задач создайте отдельные таймеры с
OnUnitActiveSec или используйте Wants= и After= в секции [Unit] сервисных юнитов.Миграция с cron: пошаговый пример
Исходная строка в crontab:
Код:
0 4 * * * /opt/scripts/cleanup.sh >> /var/log/cleanup.log 2>&1
Шаг 1. Создаём сервис:
INI:
## /etc/systemd/system/cleanup.service
[Unit]
Description=Очистка временных файлов
[Service]
Type=oneshot
ExecStart=/opt/scripts/cleanup.sh
Шаг 2. Создаём таймер:
INI:
## /etc/systemd/system/cleanup.timer
[Unit]
Description=Ежедневная очистка в 04:00
[Timer]
OnCalendar=*-*-* 04:00:00
Persistent=true
[Install]
WantedBy=timers.target
Шаг 3. Активируем и удаляем строку из crontab:
Bash:
sudo systemctl daemon-reload
sudo systemctl enable --now cleanup.timer
## Убедившись, что таймер работает, удаляем строку из crontab
crontab -e
Диагностика типичных проблем
Таймер не срабатывает
Bash:
## Проверить, активен ли таймер
systemctl is-active cleanup.timer
## Посмотреть, когда ожидается следующий запуск
systemctl list-timers cleanup.timer
## Проверить синтаксис юнита
systemd-analyze verify /etc/systemd/system/cleanup.timer
Частая причина — забытый
systemctl daemon-reload после редактирования файла.Сервис запускается, но завершается с ошибкой
Bash:
## Статус с последними строками лога
systemctl status cleanup.service
## Полный лог последнего запуска
journalctl -u cleanup.service -b
Обратите внимание на код выхода.
status=1/FAILURE означает, что скрипт вернул ненулевой код. status=203/EXEC — systemd не смог запустить бинарник (нет файла, нет прав на исполнение, неверный путь).Задача не запускается после перезагрузки
Проверьте, что таймер включён:
Bash:
systemctl is-enabled cleanup.timer
Если вывод
disabled — выполните sudo systemctl enable cleanup.timer.Ограничения и нюансы
- Точность — по умолчанию таймеры срабатывают с точностью до 1 минуты (
AccuracySec=1min). Для более точного расписания уменьшите значение, но это увеличивает нагрузку на CPU из-за более частых пробуждений.
- Нет поддержки сложных выражений —
OnCalendarне умеет «каждый третий вторник месяца» в одной строке так же гибко, как cron. Для сложных расписаний может потребоваться несколько таймеров или обходная логика в скрипте.
- Один таймер — один сервис — нельзя одним таймером запускать несколько разных сервисов. Для каждого нужен свой таймер.
- Контейнеры и chroot — systemd timers работают только там, где запущен systemd как PID 1. В Docker-контейнерах без systemd используйте cron или внешний оркестратор.
Быстрая проверка после миграции
| Что проверить | Команда |
|---|---|
| Таймер активен | systemctl is-active имя.timer |
| Таймер включён автозапуск | systemctl is-enabled имя.timer |
| Следующий запуск корректен | systemctl list-timers имя.timer |
| Сервис отработал без ошибок | journalctl -u имя.service -b --no-pager |
| Строка удалена из crontab | crontab -l |
| Синтаксис юнитов валиден | systemd-analyze verify /etc/systemd/system/имя.* |
