Всем привет! В последнее время я вплотную занимаюсь исследованием возможностей 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
OnActiveSec=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

Список статей серии

  1. Почему хабражители предпочитают велосипеды, вместо готовых решений? Или о systemd, part 0

  2. Systemd для продолжающих. Part 1 — Запуск юнитов по временным событиям

PS: Добавлена команда по выводу списка таймеров и плюсы-минусы таймеров, по сравнению с cron, в начале статьи. Спасибо @gecube!

PPS: Добавлены ссылки на ресурсы.

PPPS: Добавлено описание FixedRandomDelay= Довольно важный параметр о котором я успешно забыл.

Ресурсы

  • systemd.io — Статьи по внутренней кухне systemd. Частенько упоминается в манах.

  • systemd @ freedesktop.org — Основная страница с манами, документацией, видео, блогами и прочими ссылками на ресурсы.

  • @ru_systemd — Русскоязычный чат в Telegram. У нас тепло и лампово.