Настраиваем таймеры systemd вместо заданий cron

  • Автор:

systemd-timer-000.pngТехнологии постоянно развиваются, внося в нашу жизнь новые инструменты, которые вытесняют и заменяют старые и привычные нам. Но зачастую администраторы не спешат не только применять их на практике, но даже изучать. Нет, здоровый консерватизм безусловно оправдан, но исключительно в разумных пределах. Гораздо хуже, когда внедрение новых технологий тормозится по надуманным или "идеологическим" причинам несмотря на то, что они предоставляют гораздо более широкие возможности. Одна из таких технологий - таймеры systemd, которые позволяют по-новому взглянуть на некоторые классические задачи.

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

Сегодня мы поговорим от таймерах, как современной и эффективной замене cron. Cron - один из старожителей, пришедший к нам из мира UNIX и полностью следующий его философии, но современные системы гораздо более сложны, чем классический UNIX и некоторые подходы приходится пересматривать.

Да, сron многим привычен, но если рассматривать его сильные и слабые стороны, то в плюсы ему можно записать только простоту, все остальное - обратная сторона медали от этой простоты. Что умеет cron? Запускать задачи по расписанию и уведомлять администратора, если такой запуск завершился неудачей. Неудачей в данном случае считается возврат запускаемым приложением или скриптом кода ошибки, все остальное cron не касается, его дело запустить. Уведомление по электронной почте в современных реалиях фактически равноценно его отсутствию, мало кто будет настраивать почтовый сервер ради нескольких уведомлений.

В остальном cron является достаточно сложным и недружественным сервисом. Одним из популярных запросов в поисковых системах является "скрипт не работает в cron", это действительно так, ваш скрипт может прекрасно работать интерактивно, но не работать через планировщик, либо работать, но приводить к неожиданным результатам. И установить причину такого поведения иногда бывает достаточно сложно. Из этого вытекает одна из основных проблем - сложность отладки. Если вы не позаботились о логировании всех действий вашего скрипта, то найти источник проблем может оказаться крайне затруднительно.

Вторая проблема - тонкая настройка расписания. Хорошо, когда условие простое и задание можно проверить, просто уменьшив интервал, а если довольно сложное? Тогда вы все узнаете только по наступлению события.

Теперь посмотрим, что нам предлагает systеmd. Таймеры - это специальные триггеры, позволяющие запускать любое сервисы периодически, либо по наступлению какого-либо события. Здесь кроется существенное отличие от cron, запись которого содержит и расписание, и действие, в systemd эта логика разделена: таймер отвечает за управление сервисом, который уже выполняет действие. Для таймеров используются юниты с расширением .timer, в то время как для служб с расширением .service. Чтобы управлять каким-либо сервисом вам понадобится создать для него одноименный таймер.

В чем плюсы такого подхода? Их много. Давайте рассмотрим работу с таймерами на простом примере. Для начала создадим собственный юнит сервиса, который будет выполнять какое-нибудь простое действие, скажем выводить строку "Hello, world!". Для этого создадим файл с расширением .service в /etc/systemd/system:

touch /etc/systemd/system/hello.service

И начнем его заполнять, первой секцией является [Unit], в которую мы помещаем описание нашей службы:

[Unit]
Description=My Hello world service

В минимальной конфигурации этого достаточно, но возможности systemd гораздо шире. Здесь же можно указать зависимости, если для работы вашему сервису требуются, либо желательны другие службы. Для указания строгой зависимости используйте Requires, например:

Requires=mysql.service

В случае если указанные в этой опции службы не получится запустить, запуск нашего сервиса также завершится неудачей. Более мягкую зависимость предполагает директива Wants:

Wants=nginx.service

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

After=network.target

Это достаточно удобно, так как позволяет сразу указать требуемые условия для запуска сервиса, скажем для скрипта резервного копирования баз данных мы можем указать в зависимостях службу СУБД и, если она недоступна скрипт выполняться не будет. В то время как cron просто запустить задание без всяких проверок. Чтобы реализовать такое условие запуска службы потребуется указать:

Requires=mysql.service
After=mysql.service

Такой набор директив указывает, что нашей службе для работы обязательно нужен рабочий MySQL и запуск службы должен осуществляться только после его запуска.

Следующая секция - [Service], здесь мы описываем нашу службу. Обязательно указываем тип, в нашем случае oneshot - одноразовый запуск и выполняемое действие в директиве ExecStart, для служб с типом oneshot таких директив можно указать несколько.

[Service]
Type=oneshot
ExecStart=/bin/echo "Hello, world!"

Какие еще опции можно указать в данной секции? В рамках нашей задачи могут пригодиться WorkingDirectory - для указания рабочей директории, Environment для задания переменных окружения и User, Group чтобы запустить сервис от указанного пользователя или группы, например:

WorkingDirectory=/var/www
User=www-data
Group=www-data
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

И завершает юнит секция [Install] в которой укажем уровень запуска сервиса:

[Install]
WantedBy=multi-user.target

multi-user.target соответствует runlevel=3 или многопользовательский режим без графики.

В итоге у нас должно получиться:

[Unit]
Description=My Hello world service

[Service]
Type=oneshot
ExecStart=/bin/echo "Hello, world!"

[Install]
WantedBy=multi-user.target

Перезагружаем службу systemd:

systemctl daemon-reload

И пробуем запустить наш сервис:

systemctl start hello

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

systemctl status hello

Кроме состояния службы - неактивна, мы увидим результат ее прошлого запуска - заветную строку Hello, world!.

systemd-timer-001.png

Но как быть, если что-то пошло не так? И здесь systemd приходит на помощь, весь поток вывода вашего скрипта или приложения автоматически логируется, чтобы просмотреть лог выполните команду:

journalctl -u hello.service

systemd-timer-002.pngПеред нами вся история запусков службы, как удачных, так и не очень (мы специально внесли ошибку в команду). Таким образом можно достаточно эффективно проводить отладку сервиса и это еще один большой плюс - сервис всегда работает одинаково, хоть при ручном запуске, хоть через таймер.

Убедившись, что ваш сервис работает как надо перейдем к созданию таймера, для этого создадим еще один файл юнита:

touch /etc/systemd/system/hello.timer

Обратите внимание, что его название соответствует файлу юнита сервиса, но имеет другое расширение. Точно также начнём его заполнять, секция [Unit] должна содержать описание:

[Unit]
Description=My Hello world timer

За ней следует секция [Timer] в которой мы указываем условия запуска. Таймеры могут быть двух типов, событийные и монотонные. Первые активируются по событию, вторые выполняются периодически. Из событийных таймеров нас будет интересовать OnBootSec, срабатывающий через указанное время после старта системы. Из монотонных следует выделить:

  • OnUnitActiveSec - сработает через указанное время после активации целевого юнита
  • OnUnitInactiveSec - все тоже самое, только время будет отсчитываться с момента прекращения работы целевого юнита, хорошо подходит для "долгоиграющих" задач, скажем бекапов.
  • OnCalendar - сработает по условию календаря.

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

 [Timer]
OnBootSec=30m
OnUnitActiveSec=15m

Но наиболее интересным является таймер OnCalendar, который позволяет указать нужную дату и время срабатывания. В качестве формата используется:

DOW YYYY-MM-DD HH:MM:SS

Где DOW - Day Of Week - день недели, это необязательный параметр. За ним следует указание года, месяца и дня через дефис и час, минута, секунда через двоеточие. Для указания любого значения можно использовать *, перечисления делаются через запятую, а диапазоны разделяются .. (двумя точками). Знак тильды ~ можно использовать для указания количества интервалов от конца диапазона, а косую черту / в качестве модификатора. Несколько примеров:

*:*:00 - каждую минуту
*:15:00 - в 15 минут каждого часа
*-*-1,5,7 *:00:00 каждый час 1, 5 и 7 числа
Mon *-*~1 если последний день месяца понедельник
Mon *-*~7/1 последний понедельник месяца

Также можно использовать следующие алиасы:

minutely - ежеминутно
hourly - каждый час
daily - каждый день
monthly - каждый месяц
weekly - еженедельно
yearly - ежегодно
quarterly - ежеквартально
semiannually - раз в полгода

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

systemd-analyze calendar "Mon *-*~7/1"

systemd-timer-003.pngОпять таки очень удобно, не нужно гадать правильно ли вы создали сложное условие, его можно сразу проверить и увидеть планируемое время срабатывания.

Еще один важный момент - точность. По умолчанию точность таймеров systemd равна одно минуте, это значит, что при наступлении указанного в таймере времени запуск сервиса произойдет в случайный промежуток времени равный одной минуте. Это сделано для того, чтобы разнести срабатывание таймеров, назначенных на одно время и исключить повышенную нагрузку на систему. Изменить это поведение можно при помощи директивы AccuracySec, с ее помощью можно как увеличить, так и уменьшить значение точности. Например, увеличим ее до одного часа:

AccuracySec=1h

Еще одна полезная опция - Persistent, которая указывает запускать таймер немедленно, если был пропущен предыдущий запуск:

Persistent=true

И завершает юнит секция [Install] со следующим содержимым:

[Install]
WantedBy=timers.target

Итоговый юнит таймера в нашем примере будет выглядеть следующим образом:

[Unit]
Description=My Hello world timer

[Timer]
OnCalendar=*:*:00

[Install]
WantedBy=timers.target

В нашем случае условие самое простое - запуск один раз в минуту. Сохраним файл юнита и запустим его:

systemctl start hello.timer

После чего проверим его статус:

systemctl status hello.timer

systemd-timer-004.pngЕсли все сделано правильно, то таймер будет в состоянии активен (ожидание) и ниже будет указано время до следующего запуска. Подождем несколько минут и снова изучим журнал нашей службы:

journalctl -u hello.service

В котором вы увидите все запуски вашей службы, выполненные таймером.

Если вы внесли какие-либо изменения в файлы юнитов не забывайте перезапустить systemd командой:

systemctl daemon-reload

Теперь нам остается последний штрих - добавить таймер в автозагрузку, для чего воспользуйтесь командой:

systemctl enable hello.timer

Для выключения следует использовать:

systemctl disable hello.timer

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

Надеемся данный материал окажется вам полезным, и вы начнете применять таймеры systemd в своей практике вместо заданий cron.

Помогла статья? Поддержи автора и новые статьи будут выходить чаще:

Или подпишись на наш Телеграм-канал: Подпишись на наш Telegram-канал