Представьте ночной дамп базы, настроенный через cron на 03:30. Полгода он снимается без сбоев. Потом сервер перезагружают для обновления ядра в 03:25, и в эту ночь копии нет: cron не запускает пропущенные задачи и никому о них не сообщает. Выясняется это через две недели, когда копия понадобилась.
Сценарий типичный, и он объясняет, почему рядом с cron появился второй механизм. Таймеры systemd решают ту же задачу иначе: больше текста при настройке, меньше сюрпризов в работе. Ниже оба инструмента на одном примере. Команды - для Ubuntu Server 26.04 LTS, расписания проверены на systemd.
cron: одна строка и четыре ловушки
Расписание пользователя редактируется командой crontab -e. Строка состоит из пяти полей времени и команды:
30 3 * * * /usr/local/bin/db-dump.sh Поля идут в порядке: минута, час, день месяца, месяц, день недели. Эта строка запускает скрипт каждый день в 03:30. Задачи всей системы лежат файлами в /etc/cron.d/, там после полей времени добавляется имя пользователя.
Простота - главное достоинство cron и причина, по которой он до сих пор уместен. Но у этой строки есть четыре особенности, каждая из которых рано или поздно срабатывает.
Окружение. Задача запускается не в вашей оболочке. Переменная PATH у cron короткая, переменных из ~/.bashrc нет, текущий каталог другой. Скрипт, который отлично работал в терминале, под cron падает с command not found. Лечится полными путями к программам и явным заданием переменных в начале crontab.
Вывод. Всё, что задача напечатала, cron пытается отправить письмом владельцу. Почтовой службы на сервере обычно нет, и вывод исчезает. Об ошибке вы не узнаете, пока не допишете в конец строки перенаправление в файл: >> /var/log/db-dump.log 2>&1. За этим файлом потом тоже нужно следить, чтобы он не заполнил диск.
Наложение. Если задача запускается каждые пять минут, а выполняется семь, cron запустит вторую копию поверх первой. Две копии дампа пишут в один файл, две синхронизации дерутся за одни данные. Защита - блокировка через flock:
*/5 * * * * flock -n /run/lock/sync.lock /usr/local/bin/sync.sh Ключ -n означает «не ждать»: если блокировка занята, вторая копия сразу завершится с кодом 1. Блокировки и другие приёмы, которые делают скрипты, которые не ломаются, разобраны отдельно.
Пропуски. Сервер был выключен в момент запуска - задача не выполнится до следующего раза. Для ежеминутной проверки это неважно, для еженедельного отчёта или ночной копии означает потерю целого цикла.
Короткий пример: разовую задачу «выполнить через два часа» можно поставить без единого файла командой sudo systemd-run --on-active=2h /usr/local/bin/task.sh. systemd создаст временный таймер, который исчезнет после срабатывания. Источник: документация systemd, systemd-run.
Таймер systemd: два файла вместо одной строки
Таймер состоит из пары юнитов с одинаковым именем. Служба описывает, что запускать, таймер - когда. Оба файла кладут в /etc/systemd/system/.
Файл backup.service:
[Unit]
Description=Nightly database dump
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/db-dump.sh Файл backup.timer:
[Unit]
Description=Run database dump every night
[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=10min
Persistent=true
[Install]
WantedBy=timers.target Type=oneshot говорит, что программа выполняется и завершается, в отличие от постоянно работающей службы. Секции [Install] в файле службы нет намеренно: её запускает таймер, а не загрузка системы. Остальные директивы те же, что в любом unit-файле службы: пользователь, зависимости, ограничения доступа.
В таймере три строки делают то, чего нет в cron. Persistent=true запоминает время последнего запуска на диске. Если момент запуска пришёлся на выключенный сервер, задача выполнится сразу после загрузки. Это ровно тот случай из начала статьи. RandomizedDelaySec=10min добавляет случайную задержку до десяти минут: когда у вас двадцать серверов снимают дамп в одну секунду и одновременно нагружают хранилище, разброс избавляет от пика.
Включается таймер, а не служба:
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer Последняя команда показывает время следующего и предыдущего запуска. Проверить задачу, не дожидаясь ночи, можно прямым запуском службы: sudo systemctl start backup.service. Результат и весь вывод скрипта читаются командами systemctl status backup.service и journalctl -u backup.service. Перенаправлять вывод в файл не нужно: он попадает в журнал с отметками времени, и за размером журнала система следит сама.
Наложение запусков исключено самим устройством механизма. Пока служба работает, таймер не запустит её второй раз, блокировки не требуются.
Расписания: перевод с языка cron
Формат OnCalendar читается проще, чем пять полей cron, но привыкать к нему приходится. Общий вид: ДеньНедели Год-Месяц-День Часы:Минуты:Секунды, любую часть можно заменить звёздочкой или опустить.
Таблица 1. Соответствие расписаний cron и OnCalendar
| Когда | cron | OnCalendar |
|---|---|---|
| Каждый день в 03:30 | 30 3 * * * | *-*-* 03:30:00 |
| Каждые 15 минут | */15 * * * * | *:0/15 |
| По будням в 09:50 | 50 9 * * 1-5 | Mon..Fri *-*-* 09:50:00 |
| По понедельникам в 08:00 | 0 8 * * 1 | Mon *-*-* 08:00:00 |
| Первого числа в полночь | 0 0 1 * * | *-*-01 00:00:00 |
| По будням каждый час с 10 до 18 | 0 10-18 * * 1-5 | Mon..Fri *-*-* 10..18:00:00 |
| Раз в час, сутки, неделю | @hourly, @daily, @weekly | hourly, daily, weekly |
У таймеров есть преимущество, которое экономит ночные тревоги: расписание можно проверить до применения. Команда разбирает выражение и показывает ближайшие срабатывания:
systemd-analyze calendar --iterations=3 "Mon..Fri *-*-* 09:50:00" Next elapse: Thu 2026-10-08 09:50:00 MSK
Iteration #2: Fri 2026-10-09 09:50:00 MSK
Iteration #3: Mon 2026-10-12 09:50:00 MSK Видно, что после пятницы следующий запуск в понедельник, расписание понято верно. На ошибку команда отвечает сразу: выражение *-*-* 25:00:00 она отклоняет с сообщением Failed to parse calendar specification. В cron опечатка в расписании обнаруживается по факту, когда задача не запустилась или запустилась не тогда.
Кроме календарных расписаний, таймеры умеют отсчитывать интервалы от событий. OnBootSec=5min запускает задачу через пять минут после загрузки системы, OnUnitActiveSec=1h - через час после предыдущего запуска. Вторая форма полезна, когда важен промежуток между запусками, а не время на часах: задача, которая выполнялась сорок минут, в следующий раз стартует через час после своего начала, а не «в начале следующего часа».
Что выбрать
Таблица 2. cron и таймеры systemd в эксплуатации
| Вопрос | cron | Таймер systemd |
|---|---|---|
| Сколько писать | одна строка | два файла |
| Пропущенный запуск | теряется | выполняется после загрузки при Persistent=true |
| Наложение запусков | возможно, нужен flock | исключено |
| Куда идёт вывод | в почту или в никуда | в журнал systemd |
| Проверка расписания | нет | systemd-analyze calendar |
| Зависимости | нет | «после сети», «после базы» |
| Ограничение ресурсов и прав | нет | все директивы службы |
| Статус последнего запуска | нет | systemctl status, list-timers |
Из таблицы следует простое правило. Задача, от результата которой что-то зависит - резервная копия, загрузка данных, отчёт, обновление сертификата, - заслуживает таймера. Вы видите, когда она выполнялась в последний раз и чем закончилась, и можете повесить на её сбой оповещение. Мелкая задача без последствий, вроде очистки временного каталога раз в сутки, прекрасно живёт в cron, и переносить её ради единообразия незачем.
Сами дистрибутивы уже сделали этот выбор. В Ubuntu ротация журналов, очистка временных файлов и обновление списков пакетов работают на таймерах, и команда systemctl list-timers на свежем сервере показывает десяток строк.
Нюансы, которые всплывают после переноса
Точность. По умолчанию таймер срабатывает с допуском до минуты: systemd объединяет близкие по времени события, чтобы реже будить процессор. Для ночного дампа это безразлично. Если задача должна стартовать секунда в секунду, добавьте в секцию [Timer] строку AccuracySec=1s.
Часовой пояс. OnCalendar считает время по часам сервера. Сервер в UTC и расписание «09:50» означают 12:50 по Москве. Пояс можно указать прямо в выражении: Mon..Fri *-*-* 09:50:00 Europe/Moscow. Это надёжнее, чем помнить о поясе каждого сервера, и переживает его смену.
Сбой задачи не вызывает повтора. Таймер запустит службу в следующий раз по расписанию, даже если предыдущий запуск закончился ошибкой. Для задачи, которая обязана выполниться, в файл службы добавляют Restart=on-failure с паузой. С Type=oneshot это работает и даёт несколько попыток подряд.
Таймеры пользователя. Обычный пользователь может завести собственные таймеры в ~/.config/systemd/user/ и управлять ими командой systemctl --user. По умолчанию они работают, только пока пользователь вошёл в систему. Чтобы они жили всегда, администратор один раз выполняет sudo loginctl enable-linger имя_пользователя. Об этом забывают, и таймер «почему-то» перестаёт срабатывать после закрытия сессии SSH.
Контейнеры. Внутри контейнера Docker обычно нет ни systemd, ни cron. Задачу по расписанию для контейнерного приложения удобнее запускать с хоста: таймер, служба которого выполняет docker compose run --rm или docker compose exec. Так расписание, логи и статус остаются там же, где у остальных задач сервера.
И последнее о переносе. Не обязательно переписывать всё за один раз. Таймеры и cron спокойно работают рядом, и разумный порядок такой: новые важные задачи сразу оформлять таймерами, старые переносить по мере того, как до них доходят руки или после первого пропущенного запуска.
Заметка из эксплуатации: Persistent=true хранит отметку последнего запуска в каталоге /var/lib/systemd/timers/. Если таймер создан впервые и ни разу не срабатывал, после включения он не запустит задачу «задним числом», а дождётся первого времени по расписанию. Источник: документация systemd, systemd.timer.
Проверка команд:
- Запущено в тестовой среде (systemd 255): оба юнита из статьи проверены командой
systemd-analyze verifyбез замечаний; все выражения OnCalendar из таблицы 1 разобраны командойsystemd-analyze calendarбез ошибок; три строки вывода с датами и сообщениеFailed to parse calendar specification- настоящий вывод. Поведениеflock -nпроверено: вторая копия завершается с кодом 1. - Не запускалось (нет systemd как PID 1 и cron):
systemctl enable --now,list-timers, срабатываниеPersistent, строки crontab. Сверено с документацией.