· 11 мин чтения

Systemd timer вместо cron: как перевести бизнес-задачи без простоев

Меня зовут Семёнов Евгений Сергеевич, директор АйТи Фреш. За последний год у меня дважды был один и тот же инцидент у разных клиентов: cron запустил pg_dump, дамп не доделался за ночь, в 00:05 следующего дня cron запустил второй экземпляр — и оба задыхались на IOPS, создав дубли файлов и выполнив backup дважды на половинах базы. Оба клиента потом долго восстанавливали консистентность. Решается проблема заменой cron на systemd timer — и заодно закрываются ещё несколько болячек, о которых я расскажу ниже. Это статья для системных администраторов среднего бизнеса на Linux-серверах: как перевести ваши бэкапы, ротации и сервисные задачи и зачем это стоит затевать.

Почему cron не годится для серьёзных задач

Cron — это инструмент из эпохи, когда сервер перезагружался раз в год, всё запускалось от root, и никому не было дела до «что будет, если задача длится дольше периода». В 2026 году его ограничения стоят денег и времени:

Systemd timer — это таймер как часть init-системы. Он знает про все сервисы, состояние сети, мониторинг и умеет взаимодействовать с остальными компонентами. Если вы в 2026 году разворачиваете новый сервер — systemd timer должен быть дефолтом.

Минимальный пример: бэкап PostgreSQL

Самый распространённый случай. У вас есть сервер 1С + PostgreSQL, каждую ночь нужно делать дамп базы и забрасывать на NAS. Один unit-файл для сервиса, второй — для таймера.

# /etc/systemd/system/pg-backup.service
[Unit]
Description=PostgreSQL nightly backup to NAS
Requires=network-online.target
After=network-online.target postgresql.service

[Service]
Type=oneshot
User=postgres
Environment="PGPASSWORD="
ExecStartPre=/usr/bin/test -d /mnt/nas-backups
ExecStart=/usr/local/bin/pg-backup.sh
StandardOutput=journal
StandardError=journal
# Если задача упала — не пытаемся повторить в тот же день
Restart=no
# Лимит по времени (6 часов максимум)
TimeoutStartSec=6h
# /etc/systemd/system/pg-backup.timer
[Unit]
Description=Run PostgreSQL backup every day at 03:00

[Timer]
OnCalendar=*-*-* 03:00:00
# Случайное смещение до 30 минут — чтобы несколько серверов не долбились в NAS одновременно
RandomizedDelaySec=1800
# Если сервер был выключен в 03:00 — догнать после запуска
Persistent=true
Unit=pg-backup.service

[Install]
WantedBy=timers.target

Активация:

systemctl daemon-reload
systemctl enable --now pg-backup.timer
systemctl list-timers | grep pg-backup

Всё — заодно получили журнал в journald, защиту от двойного запуска, рандомизацию, зависимости, persistent. Без единого flock-файла.

OnCalendar: синтаксис, который нужно знать

Формат сложнее чем cron, но сильно гибче. Шпаргалка, которую я раздаю админам клиентов:

ЗадачаOnCalendar
Каждую минуту*:0/1:0
Каждые 15 минут*:0/15
Каждые 10 секунд*:*:0/10
Каждый час в 00 минутhourly или *-*-* *:00:00
Каждый день в 03:00*-*-* 03:00:00
По будням в 10:00Mon..Fri 10:00
Каждое воскресенье в 04:00Sun *-*-* 04:00:00
Первого числа каждого месяца*-*-01 04:00:00
Каждые 6 часов*-*-* 00/6:00:00
Каждые 15 секунд*:*:0/15

Проверка синтаксиса без запуска:

systemd-analyze calendar "Mon..Fri 10:00"
# Покажет следующие 3 запуска + оригинальный формат

Защита от двойного запуска — главная фишка

Когда задача запускается как systemd service, она получает единое имя unit-а. Если этот unit уже в состоянии active/activating — вторая попытка просто не состоится. У cron пришлось бы ставить flock или mkdir-lock. Тут — бесплатно.

Пример из недавнего кейса: клиент — торговая компания, 38 рабочих мест, каждые 15 минут синхронизация прайс-листов с 1С на сайт. Скрипт на python, sometimes 20 минут, sometimes 5. На cron при проблеме с сетью скрипты накапливались, 5-6 процессов долбились в одну и ту же таблицу PostgreSQL, создавали deadlock. После перевода на timer — одна задача за раз, пропущенные запуски логируются и всё. Нагрузка на 1С упала на 15%.

Persistent: что делать с выключенным сервером

Опция Persistent=true в timer означает: если запланированный запуск был пропущен (сервер выключен, перезагрузка), он выполнится сразу при следующем старте таймера. Для бэкапов и отчётов это спасение.

Конкретный случай: в январе 2026 клиент-медклиника перезагрузила сервер 3 января в 14:00 для обновления. Очередной бэкап 1С был запланирован на 03:00 3 января — в cron-версии он был бы просто потерян. В systemd timer с Persistent=true после перезагрузки timer увидел, что 03:00 уже прошло, стартовал задачу в 14:03. Клиент узнал о «неудачном» окне только по логам, но бэкап состоялся.

Реальные use-case из моей практики

Ротация сертификатов Let's Encrypt

# /etc/systemd/system/acme-renew.timer
[Timer]
OnCalendar=*-*-* 04:30:00
RandomizedDelaySec=3600
Persistent=true

# service вызывает certbot renew с reload nginx через deploy-hook

Очистка журналов WAL PostgreSQL

# /etc/systemd/system/pg-wal-cleanup.timer
[Timer]
OnCalendar=hourly
Unit=pg-wal-cleanup.service

# service запускает pg_archivecleanup с удержанием последних 48h

Мониторинг SSL-сертификатов на всех доменах клиента

# Каждые 6 часов проверка expired
OnCalendar=*-*-* 00/6:00:00
# Если осталось меньше 14 дней — алерт в Telegram
# service: /usr/local/bin/ssl-check --alert-days 14

Ротация логов в приложении

# Каждую ночь + persistent
OnCalendar=daily
Persistent=true
# service: compress + аплоад на S3 + удаление >30d

Обновление GeoIP и Spamhaus zone-list

# Каждые 12 часов с разбросом 2ч
OnCalendar=*-*-* 02,14:00:00
RandomizedDelaySec=7200

Миграция с cron — пошагово

План, который мы применяем у клиентов при переводе:

  1. Выгружаем текущий crontab всех пользователей: for u in $(cut -f1 -d: /etc/passwd); do echo "=== $u ==="; crontab -u $u -l 2>/dev/null; done.
  2. Также смотрим /etc/cron.{hourly,daily,weekly,monthly} и /etc/cron.d/.
  3. Разбиваем задачи на группы: критичные (бэкапы, ротация, cleanup), сервисные (обновление GeoIP, sync-скрипты), временные (одноразовые, legacy).
  4. Для критичных пишем пару unit+timer в /etc/systemd/system/. Используем готовые шаблоны.
  5. Для сервисных — аналогично, но с меньшим приоритетом.
  6. Временные и legacy — оставляем в cron (нет смысла тратить время).
  7. После запуска timer — удаляем соответствующую строку из crontab и ставим комментарий # migrated to systemd timer /etc/systemd/system/pg-backup.timer.
  8. Через 2 недели проверяем systemctl list-timers --all и логи — убеждаемся, что всё работает.

Чего не получится сделать

Честно про ограничения, чтобы не разочаровывать:

Кейс: миграция 140 cron-джобов у хостинг-клиента

В феврале 2026 ко мне обратилась компания, у которой есть 14 серверов (дата-центры в Москве и Новосибирске, на Debian 12 и Astra Linux 1.7): 1С-кластер, веб-серверы, почтовик, CRM. Суммарно 140 cron-джобов, часть унаследована от прошлого админа, часть от приходящего подрядчика. Периодически что-то падало, нагрузка на файловый сервер в ночное окно превышала допустимую, бэкапы иногда пропускались.

Что мы сделали за 8 рабочих дней:

  1. Собрали все crontab и /etc/cron.* в один репозиторий для аудита. Выяснилось: из 140 джобов 26 были дубли, 11 — мёртвые ссылки, 18 — устаревшие.
  2. Написали Ansible-плейбук, который раскладывает шаблоны unit+timer на все 14 серверов.
  3. Мигрировали критические задачи: 32 backup-задачи, 18 cleanup, 14 ротаций сертификатов, 9 syncs.
  4. Добавили RandomizedDelaySec от 5 до 30 минут на все ночные backup-задачи — нагрузка на NAS в окне 02:00-04:00 упала на 40%, бэкапы стали завершаться стабильно.
  5. Настроили Zabbix: systemctl list-timers --all --no-pager --output=json парсится каждые 15 минут, алерт на любой unit в состоянии failed.
  6. За первый месяц после перехода — ноль дублирующихся запусков, ноль пропущенных бэкапов, три уведомления о падении скриптов (которые бы в cron прошли незаметно).

Стоимость проекта — 145 000 руб. Три старых бэкапа, которые раньше падали раз в неделю, теперь стабильны. Один раз у клиента это уже сэкономило день работы — бэкап, который бы пропустили, оказался востребован.

Переведём ваши cron-задачи на systemd timer — от 18 000 руб.

Я лично планирую и провожу миграцию cron→systemd timer на Linux-серверах в Москве и области. Аудит существующих cron-джобов, разделение на группы, Ansible-шаблоны, интеграция с Zabbix/Prometheus. Типовой проект для 1-5 серверов — 2-3 дня. Для распределённой инфраструктуры — 1-2 недели. Первый аудит бесплатно.

Телефон: +7 903 729-62-41
Telegram: @ITfresh_Boss
Семёнов Евгений Сергеевич, директор АйТи Фреш

FAQ — systemd timer

В чём главное преимущество systemd timer перед cron?
Четыре отличия: 1) Защита от двойного запуска — если предыдущая задача ещё работает, новая не стартует. 2) Persistent — пропущенные запуски выполняются после перезагрузки. 3) Логи автоматически идут в journald без ручного перенаправления. 4) Зависимости — задачу можно привязать к готовности сети, монтированию диска, другим сервисам.
Нужно ли переписывать все cron-джобы сразу?
Нет. У меня на серверах часто мирно живут оба: systemd timer для критичного (бэкапы, ротация ключей, cleanup), cron для однострочников и старых скриптов. Переход делается поэтапно, начиная с самых болезненных задач — у которых были случаи простоя или дублирования.
Как проверить статус всех таймеров на сервере?
Команда systemctl list-timers показывает все активные таймеры, время следующего запуска, время последнего и unit-файл. Для детального просмотра одного — systemctl status backup.timer. Логи выполнения — journalctl -u backup.service.
Что такое RandomizedDelaySec и зачем оно для бизнеса?
Случайное смещение внутри окна. Если 20 серверов одновременно в 3:00 ночи делают бэкап на один и тот же файловый сервер, он ложится. RandomizedDelaySec=1800 добавляет к каждому 0-30 минут случайно — нагрузка размазывается. В продовых кластерах это обязательная настройка.
Systemd timer работает на Astra Linux и РЕД ОС?
Да, и там, и там systemd используется как init-система, timer-модуль полностью совместим. На Astra Linux 1.7/1.8 и РЕД ОС 8 наши шаблоны timer/service работают без изменений. Это даёт преимущество перед cron при миграции на российский Linux — единый подход на всём парке.