systemd timers: замена cron с логированием, зависимостями и контролем пропущенных запусков

Почему 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
Строка удалена из crontabcrontab -l
Синтаксис юнитов валиденsystemd-analyze verify /etc/systemd/system/имя.*

Источники​


 
Назад
Верх Низ