Зачем не cron
cron умеет одно: «дёргай скрипт в эту минуту». systemd timer умеет:
- Запускать через интервал после события (5 минут после boot, 1 час после предыдущего запуска), а не только в фиксированный момент времени.
- Догонять пропущенные срабатывания после выключения хоста (
Persistent=true). - Писать stdout/stderr автоматически в cmd-journalctl - никаких
>> /var/log/myapp.log 2>&1в скрипте. - Использовать всю инфраструктуру systemd: зависимости (
After=/Requires=), sandbox-изоляцию (PrivateTmp=,ProtectSystem=), cgroup-лимиты (MemoryMax=). - Рандомизировать старт (
RandomizedDelaySec=) чтобы 200 хостов не били в backup-сервер одновременно.
Cron остался - он не deprecated. Но для нового кода systemd timer удобнее.
Парность .timer + .service
Timer всегда работает в паре с одноимённым (или явно указанным) service-юнитом:
/etc/systemd/system/backup.timer ← когда запускать
/etc/systemd/system/backup.service ← что запускать
По умолчанию backup.timer триггерит backup.service. Если имена
различаются - указать явно:
[Timer]
Unit=other-backup.service
Типы триггеров
| Директива | Тип | Когда срабатывает |
|---|---|---|
OnCalendar= | calendar | В указанное время по календарю (как cron) |
OnBootSec= | monotonic | Через N после загрузки ядра |
OnStartupSec= | monotonic | Через N после старта systemd (≈ то же что boot) |
OnActiveSec= | monotonic | Через N после активации самого таймера |
OnUnitActiveSec= | monotonic | Через N после прошлого старта парного service |
OnUnitInactiveSec= | monotonic | Через N после прошлого завершения парного service |
Monotonic = считается от события, не от стенки. Не персистентный по умолчанию (после reboot счётчик сбрасывается). Удобно для «запусти через 10 минут после boot, потом каждые 6 часов».
Можно комбинировать - несколько On* в одном [Timer], сработает на любом
условии.
OnCalendar - синтаксис
Формат: DOW YYYY-MM-DD HH:MM:SS. DOW и любые поля можно опускать (*).
| Выражение | Что значит |
|---|---|
*-*-* 03:00:00 | каждый день в 03:00 |
Mon..Fri *-*-* 09:00 | по будням в 09:00 |
Mon *-*-* 00:00:00 | каждый понедельник полночь (= weekly) |
*-*-01 00:00:00 | первого числа каждого месяца (= monthly) |
*-*-* *:0/15:00 | каждые 15 минут |
2026-6,7,8-1,15 01:15:00 | 1 и 15 июня, июля, августа 2026 |
Mon *-05~03 | первый понедельник = третий с конца мая |
*-05~03/2 | третий с конца мая, потом через каждые 2 дня |
Алиасы: minutely, hourly, daily, weekly, monthly, yearly.
Проверить выражение перед коммитом - обязательно:
systemd-analyze calendar "Mon..Fri *-*-* 09:00"
# Original form: Mon..Fri *-*-* 09:00
# Normalized form: Mon..Fri *-*-* 09:00:00
# Next elapse: Wed 2026-04-30 09:00:00 EDT
# From now: 14h left
AccuracySec и RandomizedDelaySec - почему не точно
По умолчанию systemd триггерит таймер не точно в указанную секунду, а в
окно длиной 1 минута начиная с указанного времени. Это сделано
специально: если 5 таймеров стоят на daily (= 00:00:00), они
стартанут одновременно и положат I/O.
[Timer]
OnCalendar=daily
AccuracySec=1us # узкое окно - триггер в указанную секунду
RandomizedDelaySec=30min # ещё и случайная задержка до 30 мин
Дефолт большинства системных таймеров - AccuracySec=1h или больше.
Сужать только если задача реально критична ко времени.
Persistent - догонять после downtime
По умолчанию если в момент срабатывания хост был выключен - событие
пропущено. С Persistent=true (только для OnCalendar=) systemd
запоминает время последнего запуска в /var/lib/systemd/timers/ и при
старте проверяет - если пропустили, дёргает прямо сейчас.
[Timer]
OnCalendar=daily
Persistent=true # критично для backup'ов на ноутбуках/dev-VM
Минимальный пример: backup каждые 6 часов
/etc/systemd/system/backup.service:
[Unit]
Description=Nightly backup
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/do-backup.sh
User=backup
/etc/systemd/system/backup.timer:
[Unit]
Description=Backup every 6h after boot
[Timer]
OnBootSec=15min
OnUnitActiveSec=6h
Persistent=true
RandomizedDelaySec=5min
[Install]
WantedBy=timers.target
Активировать:
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer
Обрати внимание: WantedBy=timers.target (не multi-user.target),
иначе таймер не подхватится при загрузке.
Просмотр и дебаг
systemctl list-timers --all # все таймеры с next/last run
systemctl status backup.timer # state одного таймера
systemctl cat backup.timer # итоговый unit с drop-in'ами
journalctl -u backup.service -S today # что писал триггерящийся service
systemd-analyze calendar 'Mon *-*-* 04:00' # проверка OnCalendar
systemd-analyze timespan '15days 6h' # парсинг time-span
Удалить лишний дефолтный таймер (нет SSD - не нужен fstrim):
sudo systemctl disable --now fstrim.timer