12k
All articles

Замена заданий cron таймерами systemd

Замените задания cron таймерами systemd: создайте юниты service и timer, проверьте расписание OnCalendar, настройте пропущенные запуски и изучите журналы и ошибки.

OpenReplay Team
OpenReplay Team
Замена заданий cron таймерами systemd

Чтобы заменить задание cron таймером systemd, нужно написать два unit-файла: .service, который запускает скрипт, и .timer, который определяет время запуска. Затем таймер включается командой systemctl enable --now.

Если вам доводилось в 9 утра открывать /var/log/backup.log и так и не понять, отработал ли ночной запуск, вы уже знаете, почему от cron отказываются. В этом руководстве на таймер переносится одна реальная строка crontab:

30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

Каждый раздел заменяет одну часть этой строки. Расписание превращается в OnCalendar=, перенаправление вывода — в журнал, а вопрос «запускалось ли?» — в systemctl status. Общие сведения о самом cron см. в статье о том, как автоматизировать задания cron. Команды работают в любом современном дистрибутиве на базе systemd.

Ключевые выводы

  • Таймер systemd — это unit .timer, секция [Timer] которого определяет, когда запускается соответствующий .service. Сервис задаёт, что запускать, а включается именно таймер.
  • Сервису, который запускает таймер, не нужна секция [Install]. Таймер подключается к загрузке системы через WantedBy=timers.target.
  • systemd-analyze calendar проверяет, что выражение OnCalendar= корректно разбирается, и выводит время следующего срабатывания.
  • Persistent=true запускает задание, пропущенное из-за выключенной машины, сразу после повторной активации таймера.
  • У таймеров systemd нет встроенного аналога MAILTO из cron. Для оповещений о сбоях используйте OnFailure=.

Чего не умеет cron

Cron запускает команды по расписанию — и почти ничего больше. Ниже перечислены пробелы, из-за которых задания обычно и переносят:

  • Никакого логирования, кроме отправки вывода по почте. Приходится либо вручную перенаправлять вывод в файл, либо полагаться на MAILTO. Задания таймеров автоматически пишут в журнал (см. раздел «Чтение логов»).
  • Никаких зависимостей между заданиями. Задания таймеров — обычные unit’ы, поэтому они могут зависеть от других unit’ов systemd.
  • Ничего не запускается, если в назначенное время машина была выключена. Persistent=true наверстывает пропущенный запуск (см. раздел о Persistent).
  • Нет простого способа проверить результат ночного запуска. systemctl status показывает последний запуск и его код завершения.

Создание таймера systemd от начала до конца

Рабочий таймер — это два файла в /etc/systemd/system/ и одна команда: sudo systemctl enable --now backup.timer. Начнём со скрипта.

#!/usr/bin/env bash
# /usr/local/bin/backup.sh
set -euo pipefail

SRC=/srv/app/data
DEST=/var/backups/app

tar -czf "$DEST/app-$(date +%F).tar.gz" -C "$SRC" .
find "$DEST" -name 'app-*.tar.gz' -mtime +14 -delete

set -euo pipefail заставляет скрипт завершиться с ненулевым кодом, как только любая команда завершится с ошибкой, и именно этот код systemd и фиксирует. Сделайте скрипт исполняемым и создайте каталог назначения. Если биты прав доступа вам незнакомы, их разбирает руководство по правам доступа к файлам в Linux.

sudo chmod +x /usr/local/bin/backup.sh
sudo install -d -o deploy -m 750 /var/backups/app

Далее — сервис, /etc/systemd/system/backup.service:

[Unit]
Description=Nightly tarball of /srv/app/data

[Service]
Type=oneshot
User=deploy
ExecStart=/usr/local/bin/backup.sh

Type=oneshot из systemd.service(5) подходит для скриптов, которые выполняются до конца и завершаются. systemd считает unit запущенным только после завершения скрипта. Если не добавить RemainAfterExit=, unit затем сразу возвращается в состояние inactive и никогда не отображается как «active». Замените deploy на существующую учётную запись без прав root, которая может читать SRC и записывать в DEST. Без User= задание выполняется от root. У сервиса нет секции [Install] и нет ссылки на таймер. Страница о таймерах в Arch Wiki объясняет почему: включается таймер, а он активирует сервис с совпадающим именем.

Теперь таймер, /etc/systemd/system/backup.timer:

[Unit]
Description=Run backup.service daily at 02:30

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true

[Install]
WantedBy=timers.target

timers.target — специальный target, с помощью которого systemd при загрузке поднимает все таймеры, которые должны работать. Перезагрузите конфигурацию systemd, один раз запустите задание вручную, затем поставьте его в расписание:

sudo systemctl daemon-reload
sudo systemctl start backup.service     # test run now; returns when the script exits
systemctl status backup.service         # check the result
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer

Поскольку каждое задание по расписанию — это обычный сервис, его можно запустить в любой момент командой systemctl start backup.service и отладить, не дожидаясь 02:30. Сам по себе enable лишь создаёт символические ссылки. Ключ --now из systemctl(1) дополнительно запускает таймер. list-timers принимает шаблон имени unit’а, а --all включает в вывод неактивные таймеры. Между запусками сервис отображается как inactive. Это нормально.

OnCalendar против пятипольной строки cron

Прежде чем вписывать выражение OnCalendar= в файл таймера, прогоните его через systemd-analyze calendar. Команда подтвердит, что выражение корректно разбирается, и выведет время следующего срабатывания. Грамматика, описанная в systemd.time(7), имеет вид DayOfWeek Year-Month-Day Hour:Minute:Second. Любое поле даты или времени может быть * (любое значение), списком значений через запятую или диапазоном, записанным через ... День недели можно опустить целиком. Если же он указан, используйте английские названия дней, например Mon или Monday, а не *.

Типичные расписания cron соответствуют выражениям OnCalendar= так, как показано ниже. Сторону cron в этой таблице подробно описывает шпаргалка по cron в Linux:

РасписаниеcronOnCalendar
Ежедневно в 02:3030 2 * * **-*-* 02:30:00
По будням в 22:3030 22 * * 1-5Mon..Fri 22:30
По воскресеньям в 03:000 3 * * 0Sun *-*-* 03:00:00
1-го числа месяца, в полночь0 0 1 * **-*-01 00:00:00
Каждые 15 минут*/15 * * * **:00/15

Строка cron 30 22 * * 1-5 превращается в OnCalendar=Mon..Fri 22:30, поскольку номера дней недели 1–5 в cron означают понедельник–пятницу. Суффикс /15 означает «повторять каждые 15 единиц». Проверить выражение можно с помощью systemd-analyze calendar:

systemd-analyze calendar "Mon..Fri 22:30" --iterations=3

Пример вывода (даты заменены заполнителями):

  Original form: Mon..Fri 22:30
Normalized form: Mon..Fri *-*-* 22:30:00
    Next elapse: Mon YYYY-MM-DD 22:30:00 CEST
       (in UTC): Mon YYYY-MM-DD 20:30:00 UTC
       From now: 5h 12min left
   Iteration #2: Tue YYYY-MM-DD 22:30:00 CEST
   Iteration #3: Wed YYYY-MM-DD 22:30:00 CEST

Строка Normalized form показывает краткую запись, развёрнутую в полную грамматику. Строка (in UTC) появляется, только если местное время отличается от UTC. В версиях старше systemd 253 дополнительные запуски помечаются как Iter. #2, а не Iteration #2. Для --iterations= нужна systemd 242 или новее.

Одна оговорка о точности: по умолчанию AccuracySec таймера равен одной минуте, поэтому задание на 02:30:00 не обязательно стартует ровно в эту секунду. Если нужна максимальная точность, которую может обеспечить systemd, добавьте AccuracySec=1us в секцию [Timer].

Persistent=true: функция для ноутбуков

С Persistent=true таймер, пропустивший запуск по расписанию из-за выключенной машины, запускает свой сервис сразу после повторной активации таймера. Обычно это означает следующую загрузку системы. В чистом cron аналога нет. Если ноутбук выключен в 02:30, cron просто пропускает задание. Для наверстывания пропущенных заданий существует отдельная утилита anacron (в системах семейства RHEL она поставляется вместе с cronie), но она планирует только с точностью до суток, а не на конкретное время дня. Согласно systemd.timer(5), systemd запоминает, когда в последний раз срабатывал каждый persistent-таймер. Persistent= не действует на таймеры без OnCalendar=, а догоняющий запуск всё равно выжидает задержку RandomizedDelaySec=, если она задана.

Монотонные таймеры, такие как OnBootSec= и OnUnitActiveSec=, ведут себя иначе: они перестают отсчитывать время, пока система находится в режиме сна, если не задан WakeSystem=. Для заданий, привязанных к времени суток, используйте OnCalendar= вместе с Persistent=true.

Чтение логов и статуса последнего запуска

Чтобы узнать, успешно ли отработало ночное задание, выполните systemctl status backup.service. Команда покажет, когда сервис запускался в последний раз, завершился ли он успешно или с ошибкой, а также код завершения процесса. journalctl(1) показывает всё, что вывел скрипт:

systemctl status backup.service
journalctl -u backup.service --since yesterday
journalctl -u backup.service --since "-2h"

--since принимает yesterday, today, абсолютные временные метки и относительные смещения с префиксом -. Учтите, что yesterday означает полночь начала вчерашних суток, поэтому в вывод попадут и сегодняшние записи.

Стоит один раз намеренно увидеть, как выглядит упавшее задание таймера systemd. Отредактируйте скрипт, указав SRC=/srv/app/missing, и запустите его:

sudo systemctl start backup.service
systemctl status backup.service
journalctl -u backup.service -n 20

tar завершится с ошибкой, set -e заставит скрипт выйти с ненулевым кодом, и unit перейдёт в состояние failed с этим кодом в статусе. Само сообщение об ошибке tar окажется в журнале. По окончании верните исходное значение SRC.

Когда cron по-прежнему подходит?

Для одиночного задания без зависимостей cron по-прежнему подходит и даже проще. Задание — это одна строка в crontab, cron доступен практически везде, и единственное ночное задание без зависимостей прекрасно на нём работает. Таймеры обходятся в два файла и несколько команд systemctl. К тому же у них нет встроенного аналога MAILTO. Чтобы получать уведомления о сбоях задания, укажите в директиве OnFailure= сервиса unit, который отправляет оповещение.

Заключение

Переход с cron на таймеры systemd даёт три вещи, которых нет в чистом cron: задание, которое можно запустить и проверить в любой момент, вывод, попадающий в журнал без всяких перенаправлений, и догоняющие запуски после простоя. Выберите задание crontab, о котором вы беспокоитесь чаще всего. Напишите для него сервис и таймер, проверьте расписание с помощью systemd-analyze calendar, протестируйте задание через systemctl start и удалите строку из crontab, как только systemctl status покажет успешный запуск по расписанию.

Часто задаваемые вопросы

Можно ли запускать таймер systemd от имени обычного пользователя без root?

Да. Поместите файлы .service и .timer в ~/.config/systemd/user/ и управляйте ими через systemctl --user, например systemctl --user enable --now backup.timer. По умолчанию пользовательский экземпляр systemd останавливается после закрытия последнего сеанса этого пользователя, и таймеры тоже перестают срабатывать. Выполните sudo loginctl enable-linger с именем пользователя, чтобы пользовательский экземпляр запускался при загрузке системы и продолжал работать без открытого сеанса.

Чем AccuracySec отличается от RandomizedDelaySec?

AccuracySec даёт systemd свободу группировать события таймеров и реже пробуждать CPU. Таймер может сработать в любой момент от запланированного времени до истечения AccuracySec после него; по умолчанию это окно составляет 1 минуту. RandomizedDelaySec действует против группировки: он сдвигает каждый запуск на случайную величину вплоть до заданного значения (по умолчанию 0), так что таймеры с одинаковым расписанием распределяются во времени, а не срабатывают одновременно. Если заданы оба параметра, случайная задержка применяется до того, как systemd выровняет событие с другими.

Как запланировать разовое задание в systemd, не создавая unit-файлы?

Используйте systemd-run с параметром таймера, например sudo systemd-run --on-calendar='Mon 14:00' /usr/bin/cleanup.sh. Эта команда создаёт временную (transient) пару из таймера и сервиса. Сразу запускается только таймер, а сервис выполняется, когда таймер срабатывает. Флаги вроде --on-active= и --on-boot= создают монотонные временные таймеры. Временные unit'ы не переживают перезагрузку, поэтому для повторяющихся заданий по-прежнему нужны unit-файлы.

Должен ли файл таймера называться так же, как запускаемый им сервис?

Нет, но это поведение по умолчанию. Таймер активирует сервис с тем же именем без суффикса, поэтому backup.timer запускает backup.service. Чтобы запускать unit с другим именем, задайте Unit= в секции [Timer], например Unit=nightly-backup.service. Значением не может быть другой unit .timer. Тем не менее systemd.timer(5) рекомендует давать таймеру и запускаемому им unit'у одинаковое базовое имя.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.