systemd timers вместо cron: почему я перевёл все свои серверы
Привет! Я Семёнов Евгений Сергеевич, директор ITFresh. Вы знаете, после 15+ лет в Linux я настолько сросся с cron, что systemd timers казались мне чем-то излишним, просто модной штучкой. А потом случился переломный момент. На клиентском сервере, после ночного простоя, буквально испарились три *критически* важные cron-задачи. Бэкап не запустился, ночной отчёт не отправился, SSL-сертификаты не обновились. Катастрофа! А ведь systemd timer с опцией Persistent мог бы решить это одной строчкой. Именно тогда я и решил: всё, переводим наших клиентов на timers. И сейчас с удовольствием расскажу, почему мы это делаем.
Что не так с cron в 2025 году
Cron — это, конечно, легенда. Но, как и любая легенда, он со временем оброс своими «болячками». Вот основные проблемы, с которыми мы постоянно сталкиваемся у наших клиентов:
- Ваши логи, как правило, летят прямиком на mailto-почту. И что дальше? Зачастую их никто так и не открывает, они просто теряются в недрах ящика, как будто их и не было.
- Допустим, сервер «упал» в 3:00 ночи. Что тогда? Никакой автоматической обработки пропущенных запусков не предусмотрено. Ваш бэкап просто не сделается, а потом уже не наверстается. Мы сталкивались с этим: задача пропущена — и всё, вернуть её нельзя.
- Если вам нужно запускать задачу чаще, чем раз в минуту, — это проблема. Минимальный интервал здесь строго одна минута. О сабминутных задачах, к сожалению, можно даже не думать: они просто невозможны.
- А что насчёт сложных цепочек? Если одна задача должна стартовать лишь после того, как успешно выполнится другая, то задать такие зависимости не получится. Например, вы не сможете настроить: «запустить А только после успешного завершения Б». Это просто не предусмотрено.
- А вот с ограничением ресурсов тут беда. Отсутствие таких лимитов означает, что одна-единственная, даже не слишком «тяжёлая» задача способна влёгкую сожрать всю память сервера. И тогда прощай, стабильность.
- Синтаксис крона плохо читается, особенно когда вы пишете
*/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-задача может запросто "сожрать" все 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
Такой подход надёжно защищает сервер от любых «сбежавших» задач, и вам не нужны никакие дополнительные 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 уживаются без конфликтов.
