Руководство по таймерам systemd: замена заданий cron для надежного планирования

Используйте таймеры systemd вместо cron, когда вам нужны журналы journal, обработка пропущенных запусков, зависимости и контроль ресурсов.

Руководство по таймерам systemd: замена заданий cron для надежного планирования

cron по-прежнему подходит для многих задач. Если вам нужно запускать shell-скрипт каждую ночь и у вас уже настроено перенаправление логов, нет смысла его заменять. Причина, по которой многие команды переводят запланированные задачи Linux на таймеры systemd, не в моде. Это связано с тем, что таймеры systemd предоставляют задаче реальный сервисный юнит, предсказуемые логи, обработку зависимостей, поведение при пропущенных запусках и ограничения ресурсов.

Это важно, когда задача не просто «выполнить команду». Резервному копированию может потребоваться смонтированный диск. Кэш-прогревателю может понадобиться работоспособная сеть. Экспортеру отчетов может потребоваться работа от учетной записи службы с ограниченными правами и оставление читаемых логов для следующего дежурного. Таймер systemd позволяет описать эти потребности в том же месте, где вы управляете остальным жизненным циклом службы.

Понимание таймеров systemd

Таймеры systemd — это файлы юнитов systemd, которые управляют тем, когда активируются другие юниты systemd, обычно сервисные юниты. В отличие от cron, который является отдельным демоном, таймеры systemd являются неотъемлемой частью системы инициализации systemd. Такая глубокая интеграция дает несколько значительных преимуществ, особенно в отношении надежности, ведения журнала и управления ресурсами.

Таймер systemd всегда работает в паре с другим юнитом, чаще всего с сервисным юнитом. Файл .timer определяет, когда должно произойти событие, а соответствующий файл .service определяет, какое действие должно быть выполнено при наступлении этого события. Такое четкое разделение ответственности делает таймеры systemd очень модульными и гибкими.

Ключевые преимущества таймеров systemd перед cron

Хотя cron функционален, таймеры systemd устраняют многие его ограничения, предлагая более надежное и многофункциональное решение для планирования:

  • Надежность и сохранение состояния: Если календарный таймер использует Persistent=true и система была выключена во время запланированного запуска, systemd записывает, что запуск был пропущен, и запускает соответствующий сервис после следующей загрузки. Обычный cron обычно не наверстывает пропущенное без отдельного инструмента, такого как anacron.
  • Интеграция с systemd: Таймеры используют мощные возможности ведения журнала systemd (через journalctl), управления зависимостями и контроля ресурсов (cgroups). Это означает лучший мониторинг, более четкое сообщение об ошибках и возможность определять сложные последовательности запуска или ограничения ресурсов для запланированных задач.
  • Воспроизводимость и контроль версий: Файлы юнитов systemd — это обычные текстовые файлы, которые можно легко хранить в системах контроля версий. Это позволяет создавать воспроизводимые развертывания и упрощает отслеживание изменений в запланированных задачах на нескольких системах.
  • Планирование на основе событий: Помимо простого планирования по времени, таймеры systemd могут запускаться относительно загрузки системы (OnBootSec) или после последней активации юнита (OnUnitActiveSec), предоставляя более динамичные возможности планирования.
  • Гибкие временные выражения: systemd предлагает богатый набор выражений календарных событий, часто более читаемых и универсальных, чем синтаксис cron, включая ежечасно, ежедневно, еженедельно и конкретные даты/время.
  • Управление ресурсами и зависимости: Сервисы systemd, запускаемые таймерами, наследуют окружение systemd, включая настройки cgroup, и могут объявлять зависимости от других юнитов systemd (например, ожидание доступности сети или базы данных перед запуском).
  • Обработка стандартного вывода/ошибок: systemd автоматически захватывает stdout и stderr сервисов, запускаемых таймерами, и направляет их в системный журнал, что делает отладку и аудит намного проще, чем вывод на основе электронной почты cron или ручное перенаправление.

Настройка таймеров systemd

Настройка таймера systemd включает создание двух файлов юнитов: сервисного юнита (.service) и юнита таймера (.timer). Эти файлы обычно размещаются в /etc/systemd/system/ для общесистемных таймеров или в ~/.config/systemd/user/ для пользовательских таймеров.

1. Сервисный юнит (файл .service)

Сервисный юнит определяет фактическую команду или скрипт для выполнения. Это стандартный файл сервиса systemd, но часто предназначенный для неинтерактивного запуска и выполнения определенной задачи.

Пример: /etc/systemd/system/mytask.service

[Unit]
Description=Мой сервис запланированной задачи

[Service]
Type=oneshot
ExecStart=/usr/local/bin/mytask.sh
User=myuser
Group=mygroup
# Опционально: Ограничение ресурсов в новых версиях systemd
# CPUWeight=50
# MemoryMax=1G

[Install]
WantedBy=multi-user.target

Пояснение:

  • [Unit]: Содержит общую информацию о юните.
    • Description: Человекочитаемое описание.
  • [Service]: Определяет конфигурацию, специфичную для сервиса.
    • Type=oneshot: Указывает, что сервис выполняет одну команду и затем завершается. Это распространено для запланированных задач.
    • ExecStart: Команда или скрипт для выполнения. Укажите полный путь.
    • User, Group: Определяет пользователя и группу, от имени которых будет выполняться команда. Всегда запускайте задачи с минимально необходимыми привилегиями.
    • CPUWeight, MemoryMax: Опциональные элементы управления cgroup. Они полезны, когда запланированная задача не должна истощать ресурсы остальной части хоста.
  • [Install]: Определяет, как юнит должен быть включен.
    • WantedBy=multi-user.target: Хотя этот раздел присутствует, он часто менее критичен для сервисов, запускаемых по таймеру, поскольку сам юнит таймера обычно определяет активацию. Однако он может быть полезен, если вы также хотите, чтобы сервис можно было активировать вручную или интегрировать в другие цели systemd.

2. Юнит таймера (файл .timer)

Юнит таймера определяет, когда должен быть активирован соответствующий сервисный юнит. Он должен иметь то же имя, что и его сервисный аналог (например, mytask.timer для mytask.service).

Пример: /etc/systemd/system/mytask.timer

[Unit]
Description=Запускает mytask.service ежедневно

[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=600
AccuracySec=1min

[Install]
WantedBy=timers.target

Пояснение:

  • [Unit]: Общая информация.
    • Description: Описание таймера.
  • [Timer]: Определяет конфигурацию, специфичную для таймера.
    • OnCalendar: Наиболее распространенная настройка, определяющая календарное событие. Использует выражения, такие как:
      • daily: Каждый день в полночь.
      • weekly: Каждый понедельник в полночь.
      • monthly: Первый день каждого месяца в полночь.
      • hourly: Каждый час в начале часа.
      • *-*-* 03:00:00: Каждый день в 3:00 утра.
      • Mon..Fri 08:00..17:00: Будние дни с 8 утра до 5 вечера.
      • Mon *-*-* 03:00:00: Каждый понедельник в 3 часа ночи.
    • OnBootSec: Активирует сервис через указанное время после загрузки системы. Например, OnBootSec=10min.
    • OnUnitActiveSec: Активирует сервис через указанное время после последней активации сервиса. Например, OnUnitActiveSec=1h для запуска каждый час после завершения предыдущего запуска.
    • Persistent=true: Критически важно для надежности. Если система была выключена во время запланированного запуска, сервис будет запущен вскоре после следующей загрузки.
    • RandomizedDelaySec=600: Добавляет случайную задержку до 600 секунд. Это полезно, когда многие машины используют один и тот же таймер, и вы не хотите, чтобы все хосты обращались к базе данных, API или серверу резервного копирования в одну и ту же секунду.

Реальная миграция с cron на таймер

Предположим, у вас есть такая запись в корневом crontab:

15 2 * * * /usr/local/sbin/backup-app.sh >> /var/log/backup-app.log 2>&1

На тихой машине это работает, но у этого подхода есть обычные слабые места. Если диск для резервного копирования не смонтирован, скрипт может завершиться ошибкой на полпути. Если сервер выключен в 2:15 ночи, запуск пропускается. Если скрипт выводит полезную ошибку, кому-то нужно вспомнить, какой пользовательский файл журнала проверять. Если скрипт начинает использовать слишком много памяти, cron не поможет его ограничить.

Версия systemd разделяет команду и расписание:

# /etc/systemd/system/backup-app.service
[Unit]
Description=Резервное копирование данных приложения
RequiresMountsFor=/mnt/backups
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=backup
Group=backup
WorkingDirectory=/srv/app
ExecStart=/usr/local/sbin/backup-app.sh
MemoryMax=1G
CPUWeight=40
Nice=10
# /etc/systemd/system/backup-app.timer
[Unit]
Description=Запуск резервного копирования приложения каждую ночь

[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
RandomizedDelaySec=15min
AccuracySec=1min
Unit=backup-app.service

[Install]
WantedBy=timers.target

Есть несколько деталей, на которые стоит обратить внимание. RequiresMountsFor=/mnt/backups сообщает systemd, что путь должен быть смонтирован до запуска сервиса. After=network-online.target и Wants=network-online.target полезны только в том случае, если ваш сетевой менеджер действительно предоставляет сервис ожидания сети; во многих дистрибутивах этот сервис отключен по умолчанию. Если резервное копирование выполняется только на локальный диск, исключите сетевую зависимость.

Type=oneshot подходит для скриптов, которые выполняют свою работу и завершаются. Не используйте его для демона, который остается запущенным. WorkingDirectory= избавляет от скриптов, которые случайно зависят от запуска из оболочки в определенном каталоге. User=backup обычно лучше, чем запуск задачи от root и надежда, что каждая команда внутри скрипта будет осторожной.

После сохранения файлов:

sudo systemctl daemon-reload
sudo systemctl enable --now backup-app.timer
systemctl list-timers backup-app.timer

Чтобы немедленно протестировать задание, запустите сервис, а не таймер:

sudo systemctl start backup-app.service
journalctl -u backup-app.service -n 100 --no-pager

Это различие предотвращает множество путаниц. Запуск backup-app.timer активирует расписание. Запуск backup-app.service выполняет фактическое резервное копирование.

Выбор правильного выражения таймера

OnCalendar= является ближайшей заменой синтаксиса cron, но читается иначе. Вы можете проверить, что systemd думает о значении выражения, прежде чем развертывать его:

systemd-analyze calendar 'Mon..Fri 03:30'
systemd-analyze calendar '*-*-01 04:00:00'
systemd-analyze calendar 'Sun *-*-* 23:00:00'

Используйте календарные таймеры для работ по настенному времени: ночные резервные копирования, еженедельные отчеты, ежемесячная очистка, проверки сертификатов и другие задачи, где важен человеческий календарь. Используйте монотонные таймеры для поведения «запустить после того, как что-то произошло»:

[Timer]
OnBootSec=10min
OnUnitActiveSec=1h

Этот шаблон запускает сервис через десять минут после загрузки, а затем снова через час после последней активации. Он хорошо подходит для опроса, локальной очистки и легких циклов обслуживания. Это не то же самое, что «в нулевую минуту каждого часа». Если задача выполняется двенадцать минут, следующий запуск отсчитывается от времени активации, а не от ваших ожиданий по настенному времени.

Также подумайте о перекрытии. Для обычного сервисного юнита systemd не запустит вторую копию того же активного юнита только потому, что наступило следующее событие таймера. Если ваша задача может выполняться дольше, чем ее интервал, решите, приемлемо ли это. Иногда правильным ответом является блокировка в скрипте, например, flock, потому что она может выдать четкое сообщение «предыдущий запуск все еще активен». Иногда правильный ответ — увеличить интервал.

Операционные привычки, которые экономят время

Представление таймеров — это ваша первая панель мониторинга:

systemctl list-timers --all

Оно показывает последний запуск, следующий запуск и юнит, который активирует каждый таймер. Если таймер указан в списке, но сервис никогда не запускается, проверьте календарное выражение и то, включен ли таймер. Если сервис запускается и завершается с ошибкой, на время проигнорируйте таймер и проверьте сервис:

systemctl status backup-app.service
journalctl -u backup-app.service --since today

Когда вы редактируете любой из файлов юнитов, выполните:

sudo systemctl daemon-reload
sudo systemctl restart backup-app.timer

Перезапуск таймера после изменений расписания — хорошая привычка, потому что это заставляет время следующей активации обновиться немедленно. Если вы изменили только сам скрипт, daemon-reload обычно не требуется.

Для пользовательских таймеров используйте systemctl --user и размещайте юниты в ~/.config/systemd/user/. Они полезны для рабочих станций разработчиков и автоматизации на пользователя, но имеют одну важную особенность: по умолчанию пользовательские сервисы привязаны к сеансу входа пользователя. Если вам нужно, чтобы пользовательский таймер продолжал работать после выхода из системы, включите lingering с помощью loginctl enable-linger username. Это осознанный административный выбор, а не что-то, что нужно скрывать в статье как волшебное исправление.

Когда cron все еще является лучшим инструментом

Не переносите все слепо. Cron легче читать для крошечных пользовательских задач, особенно на старых серверах или минимальных контейнерах, где systemd не является PID 1. Если ваше единственное требование — «запускать эту безвредную команду каждые пять минут», cron может быть самым понятным ответом.

Таймеры systemd окупаются, когда задача имеет сервисоподобные потребности: контролируемая идентификация, логи в журнале, ограничения ресурсов, зависимости, поведение при наверстывании или стандартное развертывание через файлы юнитов. На практике я использую таймеры, когда запланированная задача разбудила бы кого-нибудь, если бы она завершилась ошибкой. Дополнительный файл юнита стоит того, когда он дает следующему оператору прямой путь от «что запускалось?» к «что не удалось?» к «что изменилось?».

Еще одна привычка, которую стоит перенять при миграции: оставляйте старую запись cron закомментированной рядом только до тех пор, пока таймер не выполнится успешно несколько раз, затем удалите ее. Дублирующиеся расписания — тихий источник проблем. Две задачи резервного копирования могут конкурировать за одну и ту же блокировку, две задачи очистки могут удалить файлы раньше, чем ожидалось, и две задачи отчетов могут отправить дублирующиеся электронные письма. После включения таймера проверьте systemctl list-timers --all, подтвердите журнал сервиса и убедитесь, что старый путь cron больше не активен.