systemd timers вместо cron: почему я перевёл все свои серверы
Меня зовут Семёнов Евгений Сергеевич, директор АйТи Фреш. За 15+ лет работы с Linux я настолько привык к cron, что годами игнорировал systemd timers как «модное новое». Переломный момент настал, когда на одном из клиентских серверов после долгого даунтайма три критичных cron-задания были пропущены за ночь — бэкап не сделался, nightly-отчёт не отправился, SSL-сертификаты не обновились. systemd timer с опцией Persistent решил бы это одной строкой. С тех пор я планомерно перевожу клиентов на timers, и в этой статье расскажу, почему.
Что не так с cron в 2025 году
Cron — легенда, но легенда с возрастом. Основные боли, которые я встречал у клиентов:
- Логи уходят в mailto-почту, которая обычно никем не читается.
- Нет обработки пропущенных запусков — выключили сервер в 3:00, бэкап не сделался и не наверстается.
- Минимальный интервал — минута, сабминутные задачи невозможны.
- Невозможно задать зависимости: например, «запускать после успешного завершения другого job».
- Нет ограничений ресурсов — одна тяжёлая задача может съесть всю память сервера.
- Синтаксис крона плохо читается, особенно когда вы пишете
*/5 * * * 1-5.
Всё это systemd решает нативно. У нас на практике переход на timers снижает количество «магических» инцидентов с задачами в 2-3 раза.
Анатомия systemd timer
systemd timer — это два файла: .timer (когда запускать) и .service (что запускать). Пример — ежедневный бэкап в 3:15:
# /etc/systemd/system/backup.service
[Unit]
Description=Daily backup of /srv
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
Nice=19
IOSchedulingClass=idle
MemoryMax=2G
CPUQuota=50%
PrivateTmp=true
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup.service daily at 03:15
Requires=backup.service
[Timer]
OnCalendar=*-*-* 03:15:00
RandomizedDelaySec=900
Persistent=true
Unit=backup.service
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
sudo systemctl list-timers --all
Ключевые опции OnCalendar
OnCalendar принимает мощный синтаксис. Примеры:
| Выражение | Значение |
|---|---|
| daily | Каждый день в 00:00 |
| hourly | Каждый час |
| *-*-* 03:15:00 | Каждый день в 3:15 |
| Mon..Fri 09:00 | Каждый будний день в 9:00 |
| *-*-01 00:00:00 | Первого числа каждого месяца |
| *-*-* *:00/15 | Каждые 15 минут |
| *-*-* *:*:30 | Каждую минуту в 30 секунд |
Полезная утилита для проверки: systemd-analyze calendar 'Mon..Fri 09:00' покажет следующие 3-5 запусков.
Persistent: догнать пропущенное
Это моя любимая опция. Если сервер был выключен или timer был disabled в момент запланированного запуска, с Persistent=true задача выполнится сразу после возобновления работы. systemd пишет state-файл в /var/lib/systemd/timers/ и сверяется с ним при старте.
Типичный сценарий: вы отключили сервер на 6 часов для обслуживания в субботу утром. С cron задачи, запланированные на это время, пропали бесследно. С systemd Persistent — выполнятся при старте сервера, возможно с небольшой задержкой.
Мониторинг: journalctl как native-логи
В cron вы бы боролись с MAILTO или >> /var/log/myjob.log. В systemd всё проще:
# Последний запуск backup.service
journalctl -u backup.service -n 100
# Все запуски за последние сутки
journalctl -u backup.service --since '24 hours ago'
# Следить в реальном времени
journalctl -u backup.service -f
# Только ошибки
journalctl -u backup.service -p err
Вывод скрипта (stdout/stderr) автоматически попадает в журнал. Код возврата тоже — при падении вы увидите Main process exited, code=exited, status=1/FAILURE.
Ограничения ресурсов: sandboxing из коробки
В cron job может внезапно съесть 16 ГБ RAM или залить CPU на 100% и положить сервер. В systemd service это решается декларативно:
[Service]
# Память
MemoryMax=2G
MemoryHigh=1.5G
# CPU
CPUQuota=50%
CPUWeight=50
# I/O
IOWeight=10
# Сеть (через slice или firewall-direct)
IPAccounting=true
# Изоляция
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
NoNewPrivileges=true
ReadWritePaths=/var/backup /var/log/backup
Это защищает сервер от runaway-задач без отдельных cgroup-настроек.
Сабминутные задачи через OnUnitActiveSec
cron не умеет «каждые 30 секунд». systemd timer — пожалуйста:
[Timer]
OnBootSec=30s
OnUnitActiveSec=30s
Unit=my-monitor.service
Подходит для healthcheck'ов, мониторинга очередей, polling API.
Миграция существующего crontab
Пошаговый план, которым я пользуюсь на клиентских серверах:
crontab -lиls /etc/cron.{d,daily,weekly,hourly}— собираем полный список текущих задач.- Для каждой задачи создаём пару .service + .timer в
/etc/systemd/system/. - Тестируем каждый сервис вручную:
systemctl start myjob.service. - Активируем таймер:
systemctl enable --now myjob.timer. - Проверяем следующие запуски:
systemctl list-timers. - Через 24-48 часов убеждаемся, что всё работает, и только тогда удаляем из cron.
Пользовательские таймеры без root
Нужна задача, которая не требует root, и вы не хотите править /etc/?
mkdir -p ~/.config/systemd/user
# Создать файлы ~/.config/systemd/user/myjob.service и myjob.timer
systemctl --user daemon-reload
systemctl --user enable --now myjob.timer
# Чтобы работало после выхода пользователя
loginctl enable-linger myuser
Реальный кейс: миграция инфраструктуры медицинского центра
В сентябре 2025 мы переводили клиента — сеть медицинских центров в Москве, 9 филиалов, головной офис на 120 рабочих мест — с легаси-cron на systemd timers. На каждом сервере филиала было от 15 до 40 cron-задач: синхронизация пациентских данных, выгрузка в ФОМС, бэкапы, отчёты, обновления справочников.
Проблема была в том, что раз в неделю кто-то из администраторов жаловался «у нас снова не отправился отчёт в ФОМС». Оказалось — ночью сервер ребутился для обновления, и crontab пропустил задачу 3:30. С Persistent=true эта проблема исчезла.
Сервера — Dell PowerEdge R740 с Xeon Gold 6248, 64 ГБ RAM, хранилище на NVMe в RAID-10, подключены 40G Mellanox в дата-центр МТС. Миграцию 11 серверов сделали за 4 рабочих дня. Стандартизировали юниты через Ansible-роль, подключили journald с пересылкой в Graylog. За полгода после миграции — ни одного инцидента с пропущенными задачами. Стоимость работ — 78 000 руб.
Грабли и особенности
- OnCalendar и летнее время. Если ваш сервер в часовом поясе с переходом на летнее время, задача в 2:30 утром может запуститься дважды или пропуститься. Используйте UTC или непроблемное время (например, 3:15).
- Type=oneshot без RemainAfterExit. Для задач-цепочек с Wants= убедитесь, что статус зафиксирован правильно.
- Переменные окружения. В отличие от cron, systemd по умолчанию не подхватывает PATH из профиля. Указывайте явно:
Environment="PATH=/usr/local/bin:/usr/bin". - RandomizedDelaySec на кластере. Без него 10 серверов ломанутся в 3:15 одновременно. С RandomizedDelaySec=600 запросы размазываются по 10 минутам.
- daemon-reload обязателен после правки. Иначе старая версия юнита запускается снова.
Переведём ваши задачи с cron на systemd timers
Проанализируем текущие crontab, спроектируем systemd-юниты с Persistent, лимитами ресурсов и правильной обработкой зависимостей. Интеграция с journald и Graylog. Работаем через Ansible — для унифицированного деплоя на несколько серверов.
Телефон: +7 903 729-62-41
Telegram: @ITfresh_Boss
Семёнов Евгений Сергеевич, директор АйТи Фреш
FAQ — частые вопросы о systemd timers
- Зачем отказываться от cron?
- Лучшая интеграция с логами, зависимости, ограничения ресурсов, Persistent для пропущенных запусков, сабминутные интервалы.
- Где хранить systemd-юниты?
- Системные — в /etc/systemd/system/, пользовательские — в ~/.config/systemd/user/, дистрибутивные — /usr/lib/systemd/system/.
- Как запускать задачу раз в 30 секунд?
- OnUnitActiveSec=30s в секции [Timer].
- Где смотреть вывод задач?
- journalctl -u my-job.service — покажет всё: stdout, stderr, код возврата.
- Что делать с существующим crontab?
- Постепенно мигрировать, начиная с критичных задач. Cron и systemd timers уживаются без конфликтов.