Таймеры systemd вместо crond
Предмет статьи
Речь о задачах, которые следует исполнять периодически, без указания сложных конструкций из даты:времени/периодичности (сиречь, о том, чем занимаются скрипты из /etc/cron.***ly/
, плюс не охваченные ими ежеминутные, ежеквартальные и ежесеместровые запуски).
Варианты
В части руководств встречаются рекомендации «пилить» таймеры под каждый отдельный юнит, существуют и генераторы таймеров на основе crontab — проекты некоторых можно глянуть здесь. Но мне они показались несколько выморочными, чересчур навороченными — ведь всё необходимое уже наличествует в systemd, и следует признать: находились таки примеры с таймерами/таргетами на отдельные периоды — часы/дни/недели и т.п.
Но посредством шаблонов можно сделать проще.
Реализация
Создать пару шаблонов
Для периодических таймеров:
# cat /lib/systemd/system/doit@.timer
[Unit]
Description = %i timer
[Timer]
OnCalendar = %i
Persistent = true
Unit = doit@%i.target
[Install]
WantedBy = timers.target
Для их «целей»:
# cat /lib/systemd/system/doit@.target
[Unit]
Description = %i timer target
StopWhenUnneeded = yes
doit — мол, «делать это еже…»
minutely …минутно, hourly …часно, daily …дневно, weekly …недельно, monhly …месячно, quarterly …квартально, semi-annually …семестрово, yearly …годно.
Само собой, «doit» что в именах, что в шаблоне таймера и периодически запускаемых сервисах можно заменить на «do», «run», «cron», «launch» или что угодно иное.
Включить и запустить периодические таймеры
# systemctl enable --now doit@{minute,hour,dai,week,month,quarter,semi-annual,year}ly.timer
Создать сервисы, требующие периодического исполнения
Пример найден на просторах интернетов, нужную периодичность указывать через секцию [Install]:
# cat /lib/systemd/system/logrotate.service
[Unit]
Description = Rotate system logs
[Service]
Nice = 19
IOSchedulingClass = 2
IOSchedulingPriority = 7
ExecStart = /usr/sbin/logrotate /etc/logrotate.conf
[Install]
WantedBy = doit@daily.target
И, наконец, включить их
# systemctl enable <список таких сервисов>
…напоследок проверив, что юниты нужных сервисов активированы:
# ls /etc/systemd/system/doit@*ly.target.wants
/etc/systemd/system/doit@daily.target.wants:
logrotate.service
/etc/systemd/system/doit@minutely.target.wants:
unbound-stats.service
/etc/systemd/system/doit@monthly.target.wants:
unbound-anchor.service
…и удостоверившись, что всё заработало:
# systemctl list-timers
Wed 2017-12-20 12:28:00 IRKT 22s left Wed 2017-12-20 12:27:00 IRKT 37s ago doit@minutely.timer doit@minutely.target
Wed 2017-12-20 13:00:00 IRKT 32min left Wed 2017-12-20 12:00:00 IRKT 27min ago doit@hourly.timer doit@hourly.target
Thu 2017-12-21 00:00:00 IRKT 11h left Wed 2017-12-20 00:00:18 IRKT 12h ago doit@daily.timer doit@daily.target
Thu 2017-12-21 11:12:16 IRKT 22h left Wed 2017-12-20 11:12:16 IRKT 1h 15min ago systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Mon 2017-12-25 00:00:00 IRKT 4 days left Mon 2017-12-18 00:00:00 IRKT 2 days ago doit@weekly.timer doit@weekly.target
Mon 2018-01-01 00:00:00 IRKT 1 weeks 4 days left Fri 2017-12-01 00:00:00 IRKT 2 weeks 5 days ago doit@monthly.timer doit@monthly.target
Mon 2018-01-01 00:00:00 IRKT 1 weeks 4 days left n/a n/a doit@quarterly.timer doit@quarterly.target
Mon 2018-01-01 00:00:00 IRKT 1 weeks 4 days left n/a n/a doit@semi-annually.timer doit@semi-annually.target
Mon 2018-01-01 00:00:00 IRKT 1 weeks 4 days left Tue 2017-10-24 18:17:11 IRKT 1 months 26 days ago doit@yearly.timer doit@yearly.target
9 timers listed.
Pass --all to see loaded but inactive timers, too.
Можно выключить крон:
# systemctl disable --now crond
Не беда, что реально работает меньше половины таймеров: один предустановленный и три добавленных, а остальные пять — пустышки. Лишние можно отключить либо рассматривать их как заготовки — вроде пустых каталогов из /etc/cron.<…>ly/
.
Другие примеры
Само собой, для сложных конструкций взамен кронтабов из /etc/cron.d/
придётся создавать отдельные таймеры, которые, однако, справятся и там, где крон нужно подпирать костылём — вроде чистки ZFS по вторым воскресеньям ежемесячно:
24 0 8-14 * * root [ $(date +\%w) -eq 0 ] && [ -x /usr/lib/zfs-linux/scrub ] && /usr/lib/zfs-linux/scrub
или проверки программного RAID-массива:
57 0 * * 0 root [ $(date +\%d) -le 7 ] && [ -x /usr/share/mdadm/checkarray ] && /usr/share/mdadm/checkarray --cron --all --idle --quiet
Из чего видно, что в кроновском «моменте запуска» нельзя сочетать диапазон чисел месяца с днями недели, так что без скрипта никак. Вот и второй пример его автор в вольном переводе комментирует так:
Запускать задачу следует по воскресеньям, но не позже 7 числа — то есть, лишь в первое воскресенье каждого месяца, в каковой ситуации кронтаб беспомощен, так что приходится костылить хак.
А вот пример таймера last-sunday.timer
для запуска в последнее воскресенье каждого месяца без скриптов:
[Unit]
Description = Last Sunday of month timer
[Timer]
OnCalendar = Sun *~07/1
AccuracySec = 2h
Persistent = true
Unit = last-sunday.target
[Install]
WantedBy = timers.target
…и его таргета last-sunday.target
:
[Unit]
Description = Last Sunday timer target
StopWhenUnneeded = yes
Остаётся создать сервис-юниты с соответствующими строками запуска:
[Unit]
Description = Описание в зависимости от запускаемой службы
[Service]
# Для чистки ZFS (zfs-scrub.service):
ExecStart = /usr/lib/zfs-linux/scrub
# Для проверки рейд-массива (mdadm-check.service):
ExecStart = /usr/share/mdadm/checkarray --cron --all --idle --quiet
[Install]
WantedBy = last-sunday.target
Всё это может выглядеть громоздко, однако:
- таймер с таргетом создаются однократно, а для каких-то сервисов таргет вовсе без надобности — достаточно, чтоб имена таймера и сервиса совпадали;
- юниты нужны лишь под отдельные службы;
- всегда можно посмотреть результат отработки таких служб через
journalctl -u <service>.service
.