Перейти к содержимому

systemd-timer: планирование вместо cron

cron работает, но его логи — текстовый файл без структуры, а зависимости от сервисов приходится городить через костыли вроде Requires= в shell-скрипте. systemd-timer решает это: единый интерфейс управления, логи в journald, зависимости через familiar After=, WantedBy= — и всё в одном стеке.

Структура .service и .timer

Таймер — это отдельный юнит, который запускает .service. Разделение故意的: сервис можно вызывать и вручную, и по расписанию.

/etc/systemd/system/
├── backup.service
└── backup.timer

backup.service — обычный юнит, можно запустить через systemctl start backup.service.

backup.timer — триггер. Без него сервис не сработает по расписанию.

# /etc/systemd/system/backup.service
[Unit]
Description=Backup to storage
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target

Persistent=true — если машина была выключена в момент срабатывания, таймер догонит после загрузки.

Calendar-таймеры (OnCalendar)

Формат OnCalendar= — самый близкий аналог cron-выражений, но с другим синтаксисом.

ПримерСрабатывает
OnCalendar=dailyКаждый день в 00:00
OnCalendar=*-*-01 03:00Первого числа каждого месяца в 03:00
OnCalendar=*-*-* 02:00Каждый день в 02:00
OnCalendar=09..17:00Каждый час с 9 до 17
OnCalendar=*:0/15Каждые 15 минут
OnCalendar=Mon..Fri 09:30Будни в 09:30

Можно указать несколько значений через запятую:

[Timer]
OnCalendar=09:00,12:00,18:00

Для проверки синтаксиса до применения:

systemd-analyze calendar '*-*-01 03:00'

Вывод покажет следующую дату срабатывания. Полезно, когда формулировка «каждый второй вторник» записывается как *-*-1..31 03:00 — проверил, убедился, что не косякнул.

Монотонные таймеры

Монотонные таймеры отсчитывают от события, а не от часов.

ДирективаСрабатывает
OnBootSec=5minЧерез 5 минут после загрузки
OnStartupSec=10minЧерез 10 минут после старта systemd
OnUnitActiveSec=1hЧерез час после последнего запуска сервиса
OnUnitInactiveSec=1dЧерез день после завершения сервиса

OnBootSec и OnStartupSec похожи, но OnBootSec сбрасывается при каждой загрузке, а OnStartupSec — с момента запуска systemd-менеджера. На практике разница заметна в контейнерах и при live-миграции.

Комбинация OnBootSec + OnUnitActiveSec — способ сделать «каждый час, но не раньше загрузки»:

[Timer]
OnBootSec=10min
OnUnitActiveSec=1h

Проверка: systemctl list-timers

После включения и запуска:

sudo systemctl enable --now backup.timer
sudo systemctl list-timers --all
NEXT                        LEFT     LAST                        PASSED  UNIT            ACTIVATES
Mon 2024-11-18 00:00:00 MSK  6h left  Sun 2024-11-17 00:00:08 MSK 18h ago backup.timer   backup.service

Без --all показывает только активные таймеры. NEXT — когда сработает, LEFT — сколько осталось.

Если таймер не появился в списке — проверить статус:

systemctl status backup.timer
systemctl status backup.service
journalctl -u backup.service -n 50

Частая причина молчащего таймера — забытый WantedBy=timers.target.

Запуск под пользователем (systemd –user)

Не все задачи требуют root. Деплой-скрипты, автоочистка кэша в домашней директории, периодический git fetch — удобнее под пользователем.

Юниты кладутся в ~/.config/systemd/user/:

mkdir -p ~/.config/systemd/user
# ~/.config/systemd/user/sync.service
[Unit]
Description=Git sync

[Service]
Type=oneshot
WorkingDirectory=%h/projects/monorepo
ExecStart=/usr/bin/git fetch --all

[Install]
WantedBy=default.target
# ~/.config/systemd/user/sync.timer
[Unit]
Description=Git sync every hour

[Timer]
OnBootSec=2min
OnUnitActiveSec=1h

[Install]
WantedBy=timers.target

Активация:

systemctl --user enable --now sync.timer

Для пользовательских таймеров нужен linger, если они должны работать без логина:

sudo loginctl enable-linger username

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

Missing OnCalendar или монотонный таймер. Без директивы [Timer] таймер не сработает никогда. systemctl start backup.timer запустит юнит, но без расписания он просто лежит.

Забытый WantedBy. Без [Install] юнит не включается при загрузке. systemctl enable backup.timer отработает без ошибок, но в list-timers его не будет.

ExecStart в .timer. Таймер запускает только связанный сервис. Если положить ExecStart в .timer, systemd проигнорирует его и возьмёт из .service.

Persistent без последнего запуска. При первой активации Persistent=true не срабатывает — нет «последнего срабатывания» в истории. Сервис запустится только в следующий расчётный момент.

Логирование: cron vs timer в journald

Cron отправляет вывод в syslog или в cron.log, структура — текстовая строка с timestamp. Для разбора нужен grep или awk.

systemd-timer пишет stdout/stderr сервиса напрямую в journald:

journalctl -u backup.service -f

Фильтрация по времени, unit, severity — стандартные флаги journalctl:

ФлагЧто делает
-u backup.serviceТолько этот юнит
-n 100Последние 100 строк
-fFollow в реальном времени
--since "1 hour ago"За период
-p errТолько ошибки

Время срабатывания каждого запуска фиксируется в метаданных. Можно построить историю выполнения без парсинга текстовых логов.

journalctl -u backup.timer -o short-iso -n 20

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

Итого

Переход с cron на systemd-timer оправдан, если задача уже живёт в systemd-окружении. Единое управление, зависимости через After=, логи в journald — выигрыш ощутимый. Для one-liner в crontab systemd-timer избыточен, но для скриптов с зависимостями, логированием и автозапуском — зрелый инструмент.