Systemd-timers как способ запускать своих скриптов в нужное время. Можно конечно пользоваться старым добрым cron, но таймеры гибче позволяют настроить время и периодичность запуска.

Почему

Для периодического запуска скриптов есть cron, но у него есть несколько проблем, которые могут мешать:

  • крон может быть только периодическим, не получится полноценно описать “календарный” или событийный вариант запуска ( разве что внутри скрипта проверять дату-вермя);
  • крон срабатывает раз в минуту, то есть минимальный период времени - 1 минута (есть всякого уровня “хаки”, вроде вызова с предварительным sleep 15, но это хаки);
  • крон просто запускает скрипт, as is (проверка того что скрипт еще работает, все то что можно сделать в декларативной форме в unit-файле systemd - нужно делать самому);
  • крон скрипты не умеют в “зависимости”;
  • крон скрипты обычно хранятся внутри /etc, а не все могут туда писать.

Может еще есть какие мелочи, но это те ограничения с которыми приходилось сталкиваться. Для таймеров же для меня есть два основных недостатка:

  • необходимо два разных файла (unit - что выполнить, timer - когда выполнить), за исключением временных задач;
  • нет отправки email уведомлений (EMAILTO из cron), тут нужно подумать об этом самому.

Для меня минусы не столь значительны и перекрываются плюсами. Поэтому приступим.

Хранение таймеров

Итак, что бы создать timer - нужен unit файл. Как он создается, какой он может быть и тд оставим за рамками этого текста. Для нашего пример это будет task.service. Таймеры по месту хранения будут двух типов.

Проверить таймеры в системе

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

Для просмотра всех таймеров в системе можно воспользоваться командой (для всех таймеров текущего пользователя добавить ключ --user):

1
systemctl list-timers

В полученной таблице будут следующие столбцы:

  • NEXT - дата и время следующего запуска таймера;
  • LEFT - время до следующего запуска таймера;
  • LAST - дата и время последнего запуска таймера;
  • PASSED - время с последнего запуска таймера;
  • UNIT - имя timer-файла;
  • ACTIVATES - имя unit-файла для запуска.

По умолчанию имя timer и unit файлов должны быть одинаковыми (за исключением этих суффиксов), но есть возможность указать кастомное имя timer-файла и привязать его к стандартному unit-файлу. Пример такой записи:

1
2
3
4
5
6
7
8
9
[Unit]
Description="Таймер вне зависимости от названия файла запустит task.service"

[Timer]
OnStartupSec=1min
Unit=task.service

[Install]
WantedBy=multi-user.target

Где искать файлы

Все таймеры хранятся в 3 основных местах:

  • /lib/systemd/system - “встроенные” таймеры;
  • /etc/systemd/system - кастомные системные таймеры;
  • ~/.config/systemd/user - кастомные таймеры пользователя.

Типы таймеров по времени запуска

По времени запуска есть два типа таймеров:

  • Realtime timers - таймеры реального времени привязанные к датам;
  • Monotonic timers - монотонные таймеры привязанные к периодам времени.

При работе с таймерами нужно иметь в виду, что со временем в системе могут происходить разного рода особенности, например:

  • сон компьютера, при этом время не останавливается;
  • может поменяться временная зона;
  • само время может “ускорится” или “замедлиться” для синхронизации.

Таймеры реального времени

В таймере реального времени главный параметр - OnCalendar внутри блока [Timer]. Параметры может быть:

  • OnCalendar=daily - ежедневный запуск (в 00:00:00);
  • OnCalendar=monday *-10-* 17:00 - каждый понедельник октября в 17 часов;
  • OnCalendar=08:00:00 - каждый день в 8 утра.

Полное описание формата можно посмотреть в systemd.time(7). Для локального тестирования можно воспользоваться командой:

1
systemd-analyze calendar "[дата для теста]"

Ответ получается такого вида:

1
2
3
4
5
6
❯ systemd-analyze calendar "monday *-10-* 17:00"
  Original form: monday *-10-* 17:00
Normalized form: Mon *-10-* 17:00:00
    Next elapse: Mon 2023-10-16 17:00:00 +04
       (in UTC): Mon 2023-10-16 13:00:00 UTC
       From now: 15h left

Так же для такого типа таймера есть несколько вспомогательных параметров (основные для использования мной):

  • Persistent - значение true или false, при отсутствии времени предыдущего запуска сразу запускает таймер;
  • RandomizedDelaySec - позволяет задать “промежуток” времени в который можно выполнить таймер, если на одно и то же время ставится много таймеров, то это позволяет “размазать” их запуск по этому промежутку;
  • FixedRandomDelay - “запоминает” промежуток для таймера с параметром выше;
  • AccuracySec - это попытка “собрать похожие по времени таймеры”, что не “будить” процессор лишний раз. По умолчанию значение здесь 1 минута, для рандомизированно запущенных таймеров лучше всего проставить значение 1us.

Монотонные таймеры

Основные параметры монотонных таймеров:

  • OnActiveSec - период времени с момента активации таймера;
  • OnBootSec - время после загрузки (для контейнеров этот параметр будет эквивалентом OnStartupSec), не работает для пользовательских таймеров;
  • OnStartupSec - время со старта сервис-менеджера, для системных таймеров очень близок к OnBootSec, для контейнеров - это время запуска контейнера, для пользователя - время входа в систему;
  • OnUnitActiveSec - определяет промежуток времени с последней активации таймера, в который таймер должен быть повторно активирован (если за сеанс нужно выполнять задачу несколько раз);
  • OnUnitInactiveSec - то же самое что выше, но учитывает время деактивации.

Дополнительные же параметры тут такие:

  • OnClockChange - выполнить таймер при смещении времени (синхронизация, установка времени руками);
  • OnTimezoneChange - выполнить таймер при изменении часового пояса.

Пример из моей жизни

Подгрузка базы паролей pass

У pass, который используется для хранения паролей, есть возможность хранить БД под версионированием в git. Но стандартные механизмы умеют только автоматом пушить изменения. Так как у меня есть несколько машин, нужно время от времени эти изменения на каждую из них пулить. Для этого был написан простой скрипт и запущен таймер по нему, который через минуту после включения пулит свежую БД для pass, и далее проверяет обновления каждые 8 часов.

1
2
3
4
5
6
7
8
9
[Unit]
Description=Pass pull git updates

[Timer]
OnStartupSec=1min
OnUnitActiveSec=8h

[Install]
WantedBy=timers.target

Отложенный старт Docker

Появилась необходимость отсрочить старт сервиса Docker (и всех контейнеров). Для этого всего лишь убрал сервис докера из автозагрузки:

1
systemctrl disable docker

И добавил таймер для запуска Docker через 5 минут после загрузки системы.

1
2
3
4
5
6
7
8
[Unit]
Description=Docker wait to start

[Timer]
OnBootSec=5min

[Install]
WantedBy=timers.target

Таймер для certbot идущий в комплекте с пакетом

Запускается каждый день два раза в сутки в произвольное время в пределах от 00:00 до 12:00 и от 12:00 до 00:00.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[Unit]
Description=This is the timer to set the schedule for automated renewals

[Timer]
OnCalendar=*-*-* 00/12:00:00
RandomizedDelaySec=12hours
Persistent=true

[Install]
WantedBy=timers.target