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

Индекс LLMS: [llms.txt](/llms.txt)

---

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 — триггер. Без него сервис не сработает по расписанию.

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

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
```

```ini
# /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 |

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

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

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

```bash
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` — способ сделать «каждый час, но не раньше загрузки»:

```ini
[Timer]
OnBootSec=10min
OnUnitActiveSec=1h
```

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

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

```bash
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` — сколько осталось.

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

```bash
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/`:

```bash
mkdir -p ~/.config/systemd/user
```

```ini
# ~/.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
```

```ini
# ~/.config/systemd/user/sync.timer
[Unit]
Description=Git sync every hour

[Timer]
OnBootSec=2min
OnUnitActiveSec=1h

[Install]
WantedBy=timers.target
```

Активация:

```bash
systemctl --user enable --now sync.timer
```

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

```bash
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:

```bash
journalctl -u backup.service -f
```

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

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

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

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

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

## Итого

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