Всем привет! В последнее время я вплотную занимаюсь исследованием возможностей systemd и решил поделиться результатом исследований с сообществом, в виде небольшого (или большого, как пойдёт цикла статей. Итак первым (уже нет) номером нашей программы будет запуск юнитов по различным событиям происходящим во время работы ОС. В качестве исследовательской платформы будет выступать Manjaro Linux c systemd v247.2. И... да. Некоторые события, вынудили меня написать внеочередную статью, которая «взлетела на вершину хит-парада», а опрос показал, что тема актуальна и вызывает интерес, так что погнали!
Пролог
Systemd — система инициализации большинства современных систем на основе ядра Linux, обладает просто безграничными возможностями и не ограничивается обычным запуском демонов. Достаточно посмотреть на объёмы штатной документации, описывающей её возможности:
pacman -Ql $(pacman -Qsq systemd|xargs)|egrep '^systemd\s|^systemd-sysvcompat\s'|egrep "man/man[1|5|8]/[[:print:]]*\.gz"|wc -l
278
И это только маны описывающие конфиги, пользовательские и администраторские утилиты systemd. Если же не ограничивать поиск практической частью, то цифра будет ещё более «устрашающей»:
pacman -Ql $(pacman -Qsq systemd|xargs)|egrep '^systemd\s|^systemd-sysvcompat\s'|wc -l
1852
Большинство администраторов и разработчиков просто не представлют какие возможности таятся в недрах той системы, в окружении которых приходится работать их сервисам. Ну что-ж, пришлось время узнать, насколько глубока кроличья нора!
Disclamer: Хоть в официальной документации и манах почти не используется такое понятие как триггеры (хотя и используется «triggered by»), но все те штуки которые описаны в этой и следующей статье, по сути, именно ими и являются. Это сущности которые срабатывают по каким-либо событиям, поэтому не удивляйтесь, если я, авторским произволом, буду использовать этот термин.
Часть первая, очевидная. Таймеры.
Все мы знаем старый, добрый cron
, во всех его проявлениях. Созданный ещё в 80-х, он, в том или ином виде, дожил до нашего времени облачных сервисов. Так же мы все знаем его ограничения. Например одной строчкой невозможно заставить крон запускать произвольный бинарник/скрипт раз в полтора часа, начиная с часа ночи, приходится описывать такое событие двумя строчками. Crontab файлы могут находиться в куче мест (/etc/crontab
, /etc/cron.d/
, /var/spool/cron
). Cron
не понимает нужно ли было стартовать сервис, если сервер был выключен и.т.д. Что-бы обойти ограничения классического крона, в systemd были придуманы такие триггеры как таймеры (юниты с окончанием *.timer
) умеющие запускать произвольные сервисы (*.service
) или группы сервисов (*.target
) периодически; по наступлении какого-либо времени; по выходу системы из спящего режима; по календарному событию (наподобие того как это делает другой ветеран Unix утилит, команда at
), а так же по другим событиям, не связанными, напрямую, со временем. К заметному минусу, по сравнению с кроном, пожалуй можно указать то, что задание хранится в двух разных файлах (*.timer
+ *.service
/ *.target
). С другой стороны это-же является и плюсом, ибо позволяет разделить логику запуска и логику времени срабатывания. Ну ладно, переходим к самому интересному.
Для начала… что запускаем. Возьмём, для примера, таймер man-db.timer из комплекта поставки одноимённого пакета:
$ cat /usr/lib/systemd/system/man-db.timer
[Unit]
Description=Daily man-db regeneration
Documentation=man:mandb(8)
[Timer]
OnCalendar=daily
AccuracySec=12h
Persistent=true
[Install]
WantedBy=timers.target
Простой, коротенький таймер. Но в чём-же дело, почему не указано что мы запускаем? Всё нормально! По умолчанию, если в секции [Timer]
отсутствует параметр Unit=
, с указанием запускаемого юнита, systemd будет искать одноимённый *.service
юнит. Проверяем!
$ cat /usr/lib/systemd/system/man-db.service
[Unit]
Description=Daily man-db regeneration
Documentation=man:mandb(8)
ConditionACPower=true
[Service]
Type=oneshot
# Recover from deletion, per FHS.
ExecStart=+/usr/bin/install -d -o root -g root -m 0755 /var/cache/man
# Expunge old catman pages which have not been read in a week.
ExecStart=/usr/bin/find /var/cache/man -type f -name *.gz -atime +6 -delete
# Regenerate man database.
ExecStart=/usr/bin/mandb --quiet
User=root
Nice=19
IOSchedulingClass=idle
IOSchedulingPriority=7
Да, вот он сервис который ежедневно пересоздаёт базу данных страниц руководства. Сервис стартует начиная с 00:00 (OnCalendar=daily
) , с точностью 12 часов (AccuracySec=12h
), то-есть он может сработать в любой момент между полуночью и полднем, в зависимости от загрузки системы:
$ systemctl status man-db.timer
● man-db.timer - Daily man-db regeneration
Loaded: loaded (/usr/lib/systemd/system/man-db.timer; disabled; vendor preset: disabled)
Active: active (waiting) since Thu 2020-12-31 23:18:59 MSK; 1 day 19h ago
Trigger: Sun 2021-01-03 00:00:00 MSK; 5h 30min left
Triggers: ● man-db.service
Docs: man:mandb(8)
дек 31 23:18:59 dell-lnx systemd[1]: Started Daily man-db regeneration.
Минимальная точность у параметра AccuracySec=
— 1us! Чем больше этот параметр, тем меньше нагрузка на систему. Если параметр отсутствует, то по умолчанию (указано в /etc/systemd/system.conf
: DefaultTimerAccuracySec=
) он равен одной минуте. Ладно, это всё лирика, давайте быстренько пробежимся по другим возможным параметрам секции [Timer]
, а на сладкое оставим параметры задания времени в OnCalendar=
и других «временнЫх» параметрах.
Событийные таймеры
Таймеры привязанные к каким-либо событиям в системе.
OnBootSec=
Таймер сработает через указанное время после старта системы.OnStartupSec=
Для системных таймеров действие аналогично предыдущему, для пользовательских таймеров, это время после первого логина пользователя в систему.OnActiveSec=
Через какое время, после активации таймера системным менеджером, запускать юнит (пример) .
«Монотонные» таймеры
Периодически запускаемые таймеры.
OnUnitActiveSec=
Триггер будет ориентироваться на время последнего запуска целевого юнита.OnUnitInactiveSec=
Триггер будет ориентироваться на последнее время завершения работы целевого юнита. Хорошо для долгоиграющих сервисов. Бэкапы и вот это вот всё.Все вышеперечисленные таймеры можно комбинировать между собой и с таймером
OnCalendar=
.
Прочие параметры
RandomizedDelaySec=
Этакий рандомный джиттер. Перед срабатыванием добавляется случайный таймаут от нуля, до заданного значения. По умолчанию -- отключено.FixedRandomDelay=
Связанный с предыдущим параметром булевый параметр. Если включено, то при первом срабатывании таймера, джиттер запомнится (и для этого таймера станет постоянным), но запомнится хитро. Сама генерация рандома будет основана на имени пользователя, имени таймера, а самое главное MachineID, о котором будет рассказано в одной из следующих статей и который гарантированно разный, на разных хостах. Для чего это нужно? Например имеем сеть с кучей хостов, которые, например в начале рабочего дня, запускают таймеры, юниты которых ломятся на сервер, устраивая шторм запросов. Что-бы таймеры гарантированно срабатывали в разное время и следует использовать этот параметр.OnClockChange=, OnTimezoneChange=
Булевые параметры, определяющие будет ли таймер реагировать на перевод системных часов или смену временной зоны. По умолчанию, оба параметра,false
.Persistent=
Записывать-ли на диск состояние таймера сразу после запуска юнита. Актуально для параметраOnCalendar=
. По умолчанию —false
.WakeSystem=
Ещё один логический параметр. Действует на монотонные таймеры. По умолчанию отключён. Логика следующая. При отключённом параметре все монотонные таймеры запоминают своё состояние, перед уходом системы в спящий режим и встают на паузу. После выхода системы из спящего режима, отсчёт продолжается с того момента с которого система «ушла в спячку». Если-же параметр поставить вtrue
, то таймеры продолжают работать и в спящем режиме (должно поддерживаться и железом) и по наступлении события выводят систему из спячки и запускают юнит.RemainAfterElapse=
Последняя крутилка, по умолчаниюtrue
Смысл этого параметра примерно следующий, После срабатывания таймера он остаётся загруженным, но если поставитьfalse
, то после срабатывания таймер выгружается и перестаёт отслеживать время. Хорошо для одноразовых юнитов (Transient Units) о которых мы поговорим в одной из следующих статей. Или для таймеров которые должны сработать один раз, как это делают задания старой, добройat
.
Таймстампы, диапазоны, тестирование, примеры
Ну и наконец зачем это всё. Чем хороши таймеры, так это тем, что всяческие сложные временные метки задаются гораздо проще чем в cron и задача запуска сервиса раз в полтора часа, начиная с часа ночи, выглядит куда приятнее. В самом простом случае она будет выглядеть так:
[Unit]
Description=Test timer
[Timer]
OnCalendar=01:00
OnUnitActiveSec=1.5h
Ну это слишком просто. Например мы хотим что-б наш юнит запускался каждую пятницу 13-е… OnCalendar=Fri *-*-13 12:00:00
Полный формат календарной формы выглядит так: Mon 2025-12-01 12:00:00.000000 Europe/Moscow
Поэтому мы можем запускать таймер по времени другого часового пояса (по умолчанию текущий) Например хотим что-б таймер прислал нам уведомление, что Камчатка уже отпраздновала Новый год: OnCalendar=yearly Asia/Kamchatka
Нормализованная форма будет выглядеть так (эти строчки указывают на одно и то-же время):
OnCalendar=*-01-01 00:00:00 Asia/Kamchatka
Алиасы (и их эквиваленты в нормализованной форме) могут быть такими:
minutely → *-*-* *:*:00
hourly → *-*-* *:00:00
daily → *-*-* 00:00:00
monthly → *-*-01 00:00:00
weekly → Mon *-*-* 00:00:00
yearly → *-01-01 00:00:00
quarterly → *-01,04,07,10-01 00:00:00
semiannually → *-01,07-01 00:00:00
Примеры валидных таймстампов:
таймстамп с @ — epoch time
Fri 2012-11-23 11:12:13 → Fri 2012-11-23 11:12:13
2012-11-23 11:12:13 → Fri 2012-11-23 11:12:13
2012-11-23 11:12:13 UTC → Fri 2012-11-23 19:12:13
2012-11-23 → Fri 2012-11-23 00:00:00
12-11-23 → Fri 2012-11-23 00:00:00
11:12:13 → Fri 2012-11-23 11:12:13
11:12 → Fri 2012-11-23 11:12:00
now → Fri 2012-11-23 18:15:22
today → Fri 2012-11-23 00:00:00
today UTC → Fri 2012-11-23 16:00:00
yesterday → Fri 2012-11-22 00:00:00
tomorrow → Fri 2012-11-24 00:00:00
tomorrow Pacific/Auckland → Thu 2012-11-23 19:00:00
+3h30min → Fri 2012-11-23 21:45:22
-5s → Fri 2012-11-23 18:15:17
11min ago → Fri 2012-11-23 18:04:22
@1395716396 → Tue 2014-03-25 03:59:56
Здесь представлены таймстампы как для OnCalendar=
, так и для монотонных таймеров.
Перечисления и диапазоны:
Боольшой список примеров
Sat,Thu,Mon..Wed,Sat..Sun → Mon..Thu,Sat,Sun *-*-* 00:00:00
Mon,Sun 12-*-* 2,1:23 → Mon,Sun 2012-*-* 01,02:23:00
Wed *-1 → Wed *-*-01 00:00:00
Wed..Wed,Wed *-1 → Wed *-*-01 00:00:00
Wed, 17:48 → Wed *-*-* 17:48:00
Wed..Sat,Tue 12-10-15 1:2:3 → Tue..Sat 2012-10-15 01:02:03
*-*-7 0:0:0 → *-*-07 00:00:00
10-15 → *-10-15 00:00:00
monday *-12-* 17:00 → Mon *-12-* 17:00:00
Mon,Fri *-*-3,1,2 *:30:45 → Mon,Fri *-*-01,02,03 *:30:45
12,14,13,12:20,10,30 → *-*-* 12,13,14:10,20,30:00
12..14:10,20,30 → *-*-* 12..14:10,20,30:00
mon,fri *-1/2-1,3 *:30:45 → Mon,Fri *-01/2-01,03 *:30:45
03-05 08:05:40 → *-03-05 08:05:40
08:05:40 → *-*-* 08:05:40
05:40 → *-*-* 05:40:00
Sat,Sun 12-05 08:05:40 → Sat,Sun *-12-05 08:05:40
Sat,Sun 08:05:40 → Sat,Sun *-*-* 08:05:40
2003-03-05 05:40 → 2003-03-05 05:40:00
05:40:23.4200004/3.1700005 → *-*-* 05:40:23.420000/3.170001
2003-02..04-05 → 2003-02..04-05 00:00:00
2003-03-05 05:40 UTC → 2003-03-05 05:40:00 UTC
2003-03-05 → 2003-03-05 00:00:00
03-05 → *-03-05 00:00:00
hourly → *-*-* *:00:00
daily → *-*-* 00:00:00
daily UTC → *-*-* 00:00:00 UTC
monthly → *-*-01 00:00:00
weekly → Mon *-*-* 00:00:00
weekly Pacific/Auckland → Mon *-*-* 00:00:00 Pacific/Auckland
yearly → *-01-01 00:00:00
annually → *-01-01 00:00:00
*:2/3 → *-*-* *:02/3:00
Да. Микро и наносекунды тоже поддерживаются, а ещё очень удобная функция конца месяца и счётчик:
*-*~01
— Первый день с конца каждого месяца (он-же последний день месяца).*-05~05
— 27-e мая каждого года (31-5).Mon *-12~07/1
— Последний понедельник декабря.Mon *-12-01/3
— Третий понедельник декабря.
Проверять таймстампы на валидность можно при помощи утилиты systemd-analyze
:
$ systemd-analyze calendar 'Mon *-12-01/1'
Original form: Mon *-12-01/1
Normalized form: Mon *-12-01/1 00:00:00
Next elapse: Mon 2021-12-06 00:00:00 MSK
(in UTC): Sun 2021-12-05 21:00:00 UTC
From now: 11 months 2 days left
$ systemd-analyze timespan 1.5h
Original: 1.5h
μs: 5400000000
Human: 1h 30min
$ systemd-analyze timestamp 01:00:30.9999
Original form: 01:00:30.9999
Normalized form: Sat 2021-01-02 01:00:30 MSK
(in UTC): Fri 2021-01-01 22:00:30 UTC
UNIX seconds: @1609538430.999900
From now: 18h ago
Очень удобно реализован показ списка имеющихся в системе таймеров. Штатная утилита systemctl
позволяет вывести список как активных, так и всех имеющихся в системе (ключик --all
) таймеров.
$ systemctl list-timers
NEXT LEFT LAST PASSED UNIT ACTIVATES
Sun 2021-01-03 14:01:00 MSK 7s left Sun 2021-01-03 14:00:09 MSK 43s ago cron-minutely.timer cron-minutely.target
Sun 2021-01-03 15:00:00 MSK 59min left Sun 2021-01-03 14:00:09 MSK 43s ago cron-hourly.timer cron-hourly.target
Sun 2021-01-03 23:35:59 MSK 9h left Sat 2021-01-02 23:35:59 MSK 14h ago systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Mon 2021-01-04 00:00:00 MSK 9h left Sun 2021-01-03 00:00:09 MSK 14h ago atop-rotate.timer atop-rotate.service
Mon 2021-01-04 00:00:00 MSK 9h left Sun 2021-01-03 00:00:09 MSK 14h ago cron-daily.timer cron-daily.target
Mon 2021-01-04 00:00:00 MSK 9h left Mon 2020-12-28 00:00:35 MSK 6 days ago cron-weekly.timer cron-weekly.target
Mon 2021-01-04 00:00:00 MSK 9h left Sun 2021-01-03 00:00:09 MSK 14h ago logrotate.timer logrotate.service
Mon 2021-01-04 00:00:00 MSK 9h left Sun 2021-01-03 00:00:09 MSK 14h ago man-db.timer man-db.service
Mon 2021-01-04 00:00:00 MSK 9h left Sun 2021-01-03 00:00:09 MSK 14h ago pkgfile-update.timer pkgfile-update.service
Mon 2021-01-04 00:00:00 MSK 9h left Sun 2021-01-03 00:00:09 MSK 14h ago shadow.timer shadow.service
Mon 2021-01-04 00:00:00 MSK 9h left Sun 2021-01-03 00:00:09 MSK 14h ago updatedb.timer updatedb.service
Thu 2021-01-07 10:58:18 MSK 3 days left Thu 2020-12-31 19:29:18 MSK 2 days ago pamac-mirrorlist.timer pamac-mirrorlist.service
Mon 2021-02-01 00:00:00 MSK 4 weeks 0 days left Fri 2021-01-01 00:00:18 MSK 2 days ago cron-monthly.timer cron-monthly.target
Sat 2021-02-06 15:00:00 MSK 1 months 3 days left Sat 2021-01-02 15:00:00 MSK 23h ago pamac-cleancache.timer pamac-cleancache.service
Thu 2021-04-01 00:00:00 MSK 2 months 26 days left Fri 2021-01-01 00:00:18 MSK 2 days ago cron-quarterly.timer cron-quarterly.target
Thu 2021-07-01 00:00:00 MSK 5 months 26 days left Fri 2021-01-01 00:00:18 MSK 2 days ago cron-semi-annually.timer cron-semi-annually.target
Sat 2022-01-01 00:00:00 MSK 11 months 27 days left Fri 2021-01-01 00:00:18 MSK 2 days ago cron-yearly.timer cron-yearly.target
n/a n/a Thu 2020-12-31 23:19:21 MSK 2 days ago cron-boot.timer cron-boot.target
18 timers listed.
Pass --all to see loaded but inactive timers, too.
Вот так, в принципе, всё просто, логично и красиво. И разумеется напочитать:
man systemd.timer
man systemd.time
man systemd-system.conf
man systemd-analyze
man tzselect
Список статей серии
Почему хабражители предпочитают велосипеды, вместо готовых решений? Или о systemd, part 0
Systemd для продолжающих. Part 1 — Запуск юнитов по временным событиям
Systemd для продолжающих. Part 2 — Триггеры на различные события
PS: Добавлена команда по выводу списка таймеров и плюсы-минусы таймеров, по сравнению с cron, в начале статьи. Спасибо @gecube!
PPS: Добавлены ссылки на ресурсы.
PPPS: Добавлено описание FixedRandomDelay=
Довольно важный параметр о котором я успешно забыл.
Ресурсы
systemd.io — Статьи по внутренней кухне systemd. Частенько упоминается в манах.
systemd @ freedesktop.org — Основная страница с манами, документацией, видео, блогами и прочими ссылками на ресурсы.
@ru_systemd — Русскоязычный чат в Telegram. У нас тепло и лампово.