January 3, 2020

Полезный демон. Как перестать бояться systemd и сделать свой сервис для Linux

Источник: t.me/USBKiller

Содержание статьи

  • Основы
  • Сервисы типа oneshot — долой rc.local
  • Делаем сервис из любой программы
  • Зависимости и порядок запуска
  • Порядок запуска сервисов
  • Зависимости
  • Внедряемся в зависимости к чужим сервисам
  • Зависимости по умолчанию
  • Заключение

Основы

Если ты еще никогда не делал свои сервисы, начнем с основ. Systemd оперирует абстрактными единицами (unit), которые бывают разных типов, могут предоставлять различные ресурсы (процессы, сокеты, абстрактные «цели») и требовать других ресурсов для запуска.

Самый распространенный вид ресурса — сервис (service). Файлы с описаниями сервисов и всего прочего лежат в каталоге /lib/systemd/system/. Чтобы systemd нашел новый сервис, достаточно положить в этот каталог свой файл. Если этот сервис ранее не существовал, systemd прочитает файл и загрузит его в память. Однако, если ты редактируешь файл ранее запущенного сервиса, не забудь заставить systemd перечитать файлы командой sudo systemctl daemon-reload!

Сервисы типа oneshot — долой rc.local

Когда-то стандартным способом добавить выполнение команд в загрузку системы было дописать их в /etc/rc.local. Очевидный недостаток — нет способов следить, насколько успешно они выполнились. В systemd легко создать для такой цели свой сервис типа oneshot, и им можно будет управлять через systemctl, как любым другим. В этом случае systemd выполнит команду и посчитает запуск сервиса успешным, если она завершилась с кодом ноль.

Сохраним следующий файл в /lib/systemd/system/dumb-test.service:

[Unit]
  Description=Dumb test

[Service]
  ExecStart=/bin/true
  Type=oneshot

[Install]
  WantedBy=multiuser.target

Дополнительных действий не требуется, и теперь ты можешь делать с ним все то же, что с системными сервисами: запустить с помощью sudo systemctl start dumb-test.service, поставить на загрузку с помощью sudo systemctl enable dumb-test.service и так далее.

Делаем сервис из любой программы

Любой долгоживущий процесс можно легко превратить в сервис с помощью опции Type=idle. В этом случае systemd перехватит стандартные потоки ввода-вывода и будет следить за жизнью процесса.

Для демонстрации напишем программу на Python, которая просто выводит сообщение в бесконечном цикле. Сохраним в /usr/local/bin/test.py следующее:

import time

while True:
    print("Test service is alive")
    time.sleep(5)

Затем создадим для нее файл сервиса в /lib/systemd/system/smart-test.service:

[Unit]
  Description=Smart test

[Service]
  ExecStart=/usr/bin/python3 -u /usr/local/bin/test.py
  Type=idle
  KillMode=process

  SyslogIdentifier=smart-test
  SyslogFacility=daemon

  Restart=on-failure

[Install]
  WantedBy=multiuser.target

Теперь можно запустить наш сервис и убедиться, что он работает:

$ sudo systemctl status smart-test.service
 smart-test.service - Smart test
   Loaded: loaded (/usr/lib/systemd/system/smart-test.service; static; vendor preset: disabled)
   Active: active (running) since Fri 2019-10-25 16:25:18 +07; 1s ago
 Main PID: 19893 (python3)
    Tasks: 1 (limit: 4915)
   Memory: 3.5M
   CGroup: /system.slice/smart-test.service
           └─19893 /usr/bin/python3 -u /usr/local/bin/test.py

Стандартный вывод программы пишется в journald, и его можно увидеть в journalctl -u smart-test. Ради интереса посмотрим на работу опции Restart=on-failure. Остановим наш процесс с помощью kill -9 ${Main PID} и заглянем в логи:

systemd[1]: Started Smart test.

Test service is alive

Test service is alive

smart-test.service: Main process exited, code=killed, status=9/KILL

smart-test.service: Failed with result 'signal'.

smart-test.service: Service RestartSec=100ms expired, scheduling restart.

smart-test.service: Scheduled restart job, restart counter is at 1.

Stopped Smart test.

Started Smart test.

Test service is alive

Для настоящих демонов нужно использовать тип forking, но мы не будем вдаваться в детали — авторы таких пакетов наверняка все уже знают сами.

Зависимости и порядок запуска

Опций для настройки зависимости в systemd очень много. Прежде всего нужно отметить, что в нем есть два независимых механизма для указания порядка запуска сервисов и зависимостей между ними.

Порядок запуска сервисов

Порядок запуска сервисов определяется опциями Before и After. Если в настройках сервиса foo написано After=bar.service и оба сервиса должны запуститься, то systemd сначала выполнит попытку запустить bar, а затем foo.

Однако опция After=bar.service сама по себе не поставит сервис на загрузку. Более того, она никак не повлияет на решение запускать foo, даже если запуск bar завершится неудачей.

Причина существования этих опций — способность systemd запускать сервисы параллельно.

Для примера возьмем типичный веб-сервер с набором из веб-приложения FCGI, СУБД и обратного прокси. В каком порядке запускать процесс FCGI и обратный прокси, не так важно. Запросы будут работать, только когда они оба запущены, но «неверный порядок» никак не помешает им запуститься.

Если веб-приложение требует данных из базы для инициализации, то мало убедиться, что и процесс FCGI, и СУБД запущены, — приложение нужно запускать только после полного запуска СУБД. Именно для этих случаев и предназначены опции Before/After.

Зависимости

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

Мягкие зависимости указываются с помощью опции Wants= в секции [Unit]. Пример из sshd.service:

[Unit]
Description=OpenSSH server daemon
Documentation=man:sshd(8) man:sshd_config(5)
After=network.target sshd-keygen.target
Wants=sshd-keygen.target

Цель sshd-keygen.target, очевидно, генерирует ключ хоста, если он отсутствует. Технически sshd не сможет запуститься без ключа хоста, поэтому, почему авторы решили сделать зависимость мягкой, можно только догадываться. Возможно, они посчитали, что в большинстве случаев ключ уже существует и устаревший ключ лучше неработающего SSH.

Жесткие зависимости можно указать с помощью опции Requires. К примеру, в systemd-journal-flush.service есть опция Requires=systemd-journald.service — очевидно, отправить команду journald невозможно, пока он не запущен.

У этой опции существуют вариации, например RequiresMountsFor. Посмотрим в файл logrotate.service:

[Unit]
Description=Rotate log files
Documentation=man:logrotate(8) man:logrotate.conf(5)
RequiresMountsFor=/var/log

Для работы logrotate нужен доступ к каталогу с логами и больше ничего. Опция RequiresMountsFor=/var/log позволяет выразить именно это: сервис запустится, как только будет примонтирован каталог, содержащий путь /var/log, даже если он находится не в корневом разделе.

Внедряемся в зависимости к чужим сервисам

В системах с System V init добавить что-то в зависимости к чужому сервису можно было, лишь отредактировав его скрипт. Такие изменения, очевидно, не переживут обновления системы, поэтому сделать их постоянными можно было бы только пересборкой пакета.

В systemd есть несколько способов решить эту проблему. Если нужно именно внедриться к кому-то в зависимости, можно попробовать опции обратных зависимостей: WantedBy и RequiredBy. Они должны находиться в секции [Install], а не [Unit]. Подводных камня здесь два: они обрабатываются только при установке сервиса с помощью systemctl enable и, как все в systemd, не всегда нормально работают во всех версиях.

Второй вариант, который позволяет менять любые настройки: скопировать файл сервиса в /etc/systemd/system/. Если один файл присутствует в обоих каталогах, то файл из /etc/systemd/system имеет приоритет.

Третий, менее радикальный вариант: создать файл вида /etc/systemd/system/${unit}.d/local.conf и прописать туда только нужные настройки.

Для примера притворимся, что наш сервис smart-test вовсе и не наш, и добавим ему в зависимости sshd.service третьим способом. Создадим файл /etc/systemd/system/smart-test.service.d/local.conf со следующим содержанием:

[Unit]
  Requires=sshd.service

Теперь sshd.service будет запущен вместе со smart-test.service, даже если раньше был выключен.

Если важен не только сам факт запуска обоих сервисов, но и порядок их запуска, не забудь указать его с помощью опций Before или After.

Зависимости по умолчанию

Нужно отметить, что systemd создает для каждого сервиса зависимости по умолчанию. В большинстве случаев это скорее благо, потому что пользовательские сервисы обычно требуют полностью инициализированной системы для своей работы. Но если ты хочешь добавить новый шаг в процесс загрузки системы, тебе нужно избавиться от этих зависимостей. Это можно сделать опцией DefaultDependencies=no.

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

[Unit]
Description=My early boot step

DefaultDependencies=no

After=systemd-remount-fs.service

Просмотреть зависимости сервиса и убедиться, что там нет лишнего, можно командой systemctl list-dependencies ${unit}.

Заключение

Systemd можно любить или ненавидеть, но игнорировать его невозможно — нужно уметь с ним работать. Надеюсь, эти знания помогут тебе обратить systemd себе на пользу.

Читайте ещё больше платных статей бесплатно:

ПОДПИСАТЬСЯ - USBKiller