· 14 мин чтения

systemd timers вместо cron: почему я перевёл все свои серверы

systemd timers вместо cron: почему я перевёл все свои серверы

Привет! Я Семёнов Евгений Сергеевич, директор ITFresh. Вы знаете, после 15+ лет в Linux я настолько сросся с cron, что systemd timers казались мне чем-то излишним, просто модной штучкой. А потом случился переломный момент. На клиентском сервере, после ночного простоя, буквально испарились три *критически* важные cron-задачи. Бэкап не запустился, ночной отчёт не отправился, SSL-сертификаты не обновились. Катастрофа! А ведь systemd timer с опцией Persistent мог бы решить это одной строчкой. Именно тогда я и решил: всё, переводим наших клиентов на timers. И сейчас с удовольствием расскажу, почему мы это делаем.

Что не так с cron в 2025 году

Cron — это, конечно, легенда. Но, как и любая легенда, он со временем оброс своими «болячками». Вот основные проблемы, с которыми мы постоянно сталкиваемся у наших клиентов:

Так вот, все эти проблемы 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

Вот пошаговый план, который я лично использую на серверах наших клиентов:

  1. crontab -l и ls /etc/cron.{d,daily,weekly,hourly} — собираем полный список текущих задач.
  2. Для каждой задачи создаём пару .service + .timer в /etc/systemd/system/.
  3. Тестируем каждый сервис вручную: systemctl start myjob.service.
  4. Активируем таймер: systemctl enable --now myjob.timer.
  5. Проверяем следующие запуски: systemctl list-timers.
  6. На нашей практике мы делаем так: внедряем изменения, а затем ждём 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 рублей.

Грабли и особенности

Переведём ваши задачи с 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 уживаются без конфликтов.

Подпишитесь на рассылку ITfresh

Хотите получать полезную информацию? Раз в неделю мы делимся практическими гайдами для вас, руководители IT и сисадмины! Рассказываем про безопасность, 1С, успешные миграции, надёжные резервные копии и делимся реальными лайфхаками из наших проектов.

Реквизиты оператора персональных данных

ООО «АЙТИ-ФРЕШ», ИНН 7719418495, КПП 771901001. Юридический адрес: 105523, г. Москва, Щёлковское шоссе, д. 92, корп. 7. Контакт: info@itfresh.ru, +7 903 729-62-41. Оператор обрабатывает e-mail подписчика в целях рассылки информационных и рекламных материалов до момента отзыва согласия.