10. Экземпляры служб

Большинство служб в Linux/Unix являются одиночными (singleton): в каждый момент времени на данном хосте работает только один экземпляр службы. В качестве примера таких одиночных служб можно привести Syslogd, Postfix, Apache. Однако, существуют службы, запускающие по несколько экземпляров себя на одном хосте. Например, службы наподобие Dovecot IMAP запускают по одному экземпляру на каждый локальный порт и/или IP-адрес. Другой пример, который можно встретить практически во всех системах — getty, небольшая служба, запускающаяся на каждом TTY (от tty1 до tty6). На некоторых серверах, в зависимости от сделанных администратором настроек или параметров загрузки, могут запускаться дополнительные экземпляры getty, для подключаемых к COM-портам терминалов или для консоли системы виртуализации.
Еще один пример службы, работающей в нескольких экземплярах (по крайней мере, в мире systemd) — fsck, программа проверки файловой системы, которая запускается по одному экземпляру на каждое блочное устройство, требующее такой проверки. И наконец, стоит упомянуть службы с активацией в стиле inetd — при обращении через сокет, по одному экземпляру на каждое соединение. В этой статье я попытаюсь рассказать, как в systemd реализовано управление «многоэкземплярными» службами, и какие выгоды системный администратор может извлечь из этой возможности.

Если вы читали предыдущие статьи из этого цикла, вы, скорее всего, уже знаете, что службы systemd именуются по схеме foobar.service, где foobar — строка, идентифицирующая службу (проще говоря, ее имя), а .service — суффикс, присутствующий в именах всех файлов конфигурации служб. Сами эти файлы могут находиться в каталогах /etc/systemd/systemd и /lib/systemd/system (а также, возможно, и в других). Для служб, работающих в нескольких экземплярах, эта схема становится немного сложнее: foobar@quux.service, где foobar — имя службы, общее для всех экземпляров, а quux — идентификатор конкретного экземпляра. Например, serial-gett@ttyS2.service — это служба getty для COM-порта, запущенная на ttyS2.
При необходимости, экземпляры служб можно легко создать динамически. Скажем, вы можете, безо всяких дополнительных настроек, запустить новый экземпляр getty на последовательном порту, просто выполнив systemctl start для нового экземпляра:


# systemctl start serial-getty@ttyUSB0.service


Получив такую команду, systemd сначала пытается найти файл конфигурации юнита с именем, точно соответствующим запрошенному. Если такой файл найти не удается (при работе с экземплярами служб обычно так и происходит), из имени файла удаляется идентификатор экземпляра, и полученное имя используется при поиске шаблона конфигурации. В нашем случае, если отсутствует файл с именем serial-getty@ttyUSB0.service, используется файл-шаблон под названием serial-getty@.service. Таким образом, для всех экземпляров данной службы, используется один и тот же шаблон конфигурации. В случае с getty для COM-портов, этот шаблон, поставляемый в комплекте с systemd (файл /lib/systemd/system/serial-getty@.service) выглядит примерно так:


[Unit]
Description=Serial Getty on %I
BindTo=dev-%i.device
After=dev-%i.device systemd-user-sessions.service
[Service]
ExecStart=-/sbin/agetty -s %I 115200,38400,9600
Restart=always
RestartSec=0


Заметим, что приведенная здесь версия немного сокращена, по сравнению с реально используемой в systemd. Удалены не относящиеся к теме нашего обсуждения параметры конфигурации, обеспечивающие совместимость с SysV, очистку экрана и удаление предыдущих пользователей с текущего TTY.
Этот файл похож на обычный файл конфигурации юнита, с единственным отличием: в нем используются спецификаторы %I и %i. В момент загрузки юнита, systemd заменяет эти спецификаторы на идентификатор экземпляра службы. В нашем случае, при обращении к экземпляру serial-getty@ttyUSB0.service, они заменяются на «ttyUSB0». Результат такой замены можно проверить, например, запросив состояние для нашей службы:


$ systemctl status serial-getty@ttyUSB0.service
serial-getty@ttyUSB0.service - Getty on ttyUSB0
        Loaded: loaded (/lib/systemd/system/serial-getty@.service; static)
        Active: active (running) since Mon, 26 Sep 2011 04:20:44 +0200; 2s ago
     Main PID: 5443 (agetty)
        CGroup: name=systemd:/system/getty@.service/ttyUSB0
        5443 /sbin/agetty -s ttyUSB0 115200,38400,9600


Собственно, это и есть ключевая идея организации экземпляров служб. Как видите, systemd предоставляет простой в использовании механизм шаблонов, позволяющих динамически создавать экземпляры служб. Добавим несколько дополнительных замечаний, позволяющих эффективно использовать этот механизм.
Вы можете создавать дополнительные экземпляры таких служб, просто добавляя символьные ссылки в каталоги *.wants/. Например, чтобы обеспечить запуск getty на ttyUSB0 при каждой загрузке, достаточно создать такую ссылку:


# ln -s /lib/systemd/system/serial-getty@.service \
/etc/systemd/system/getty.target.wants/serial-getty@ttyUSB0.service


При этом файл конфигурации, на который указывает ссылка (в нашем случае serial-getty@.service), будет вызван с тем именем экземпляра, которое указанно в названии этой ссылки (в нашем случае — ttyUSB0). Вы не сможете обратиться к юниту-шаблону без указания идентификатора экземпляра. В частности, команда systemctl start serial-getty@.service завершится ошибкой.
Иногда возникает необходимость отказаться от использования общего шаблона для конкретного экземпляра (т.е. конфигурация данного экземпляра настолько сильно отличается от параметров остальных экземпляров данной службы, что механизм шаблонов оказывается неэффективен). Специально для таких случаев, в systemd и заложен предварительный поиск файла с именем, точно соответствующим указанному (прежде чем использовать общий шаблон). Таким образом, вы можете поместить файл с именем, точно соответствующим полному титулу экземпляра, в каталог /etc/systemd/system/ — и содержимое этого файла, при обращении к выбранному экземпляру, полностью перекроет все настройки, сделанные в общем шаблоне.
В приведенном выше файле, в некоторых местах используется спецификатор %I, а в других — %i. У вас может возникнуть закономерный вопрос — чем они отличаются? %i всегда точно соответствует идентификатору экземпляра, в то время, как %I соответствует неэкранированной (unescaped) версии этого идентификатора. Когда идентификатор не содержит спецсимволов (например, ttyUSB0), результат в обоих случаях одинаковый. Но имена устройств, например, содержат слеши («/»), которые не могут присутствовать в имени юнита (и в имени файла на Unix). Поэтому, перед использованием такого имени в качестве идентификатора устройства, оно должно быть экранировано — «/» заменяются на «-», а большинство других специальных символов (включая «-») заменяются последовательностями вида \xAB, где AB — ASCII-код символа, записанный в шестнадцатеричной системе счисления. Например, чтобы обратиться к последовательному USB-порту по его адресу на шине, нам нужно использовать имя наподобие serial/by-path/pci-0000:00:1d.0-usb-0:1.4:1.1-port0. Экранированная версия этого имени — serial-by\x2dpath-pci\x2d0000:00:1d.0\x2dusb\x2d0:1.4:1.1\x2dport0. %I будет ссылаться на первую из этих строк, %i — на вторую. С практической точки зрения, это означает, что спецификатор %i можно использовать в том случае, когда надо сослаться на имена других юнитов, например, чтобы описать дополнительные зависимости (в случае с serial-getty@.service, этот спецификатор используется для ссылки на юнит dev-%i.device, соответствующий одноименному устройству). В то время как %I удобно использовать в командной строке (ExecStart) и для формирования читабельных строк описания. Рассмотрим работу этих принципов на примере нашего юнит-файла (Прим. перев.: как видно из нижеприведенного примера, в командной строке systemctl используется экранированное имя юнита, что создает определенные трудности даже при наличии в оболочке «умного» автодополнения. Однако, начиная с systemd v186, при работе с systemctl можно указывать неэкранированные имена юнитов.):


# systemctl start ’serial-getty@serial-by\x2dpath-pci\x2d0000:00:1d.0\x2dusb\x2d0:1.4:1.1\x2dport0.service’
# systemctl status ’serial-getty@serial-by\x2dpath-pci\x2d0000:00:1d.0\x2dusb\x2d0:1.4:1.1\x2dport0.service’
serial-getty@serial-by\x2dpath-pci\x2d0000:00:1d.0\x2dusb\x2d0:1.4:1.1\x2dport0.service - Serial Getty on serial/by-path/pci-0000:00:1d.0- sb-0:1.4:1.1-port0
        Loaded: loaded (/lib/systemd/system/serial-getty@.service; static)
        Active: active (running) since Mon, 26 Sep 2011 05:08:52 +0200; 1s ago
      Main PID: 5788 (agetty)
        CGroup: name=systemd:/system/serial-getty@.service/serial-by\x2dpath-pci\x2d0000:00:1d.0\x2dusb\x2d0:1.4:1.1\x2dport0
                5788 /sbin/agetty -s serial/by-path/pci-0000:00:1d.0-usb-0:1.4:1.1-port0 115200 38400 9600


Как видите, в качестве идентификатора экземпляра используется экранированная версия, в то время как в строке описания и в командной строке getty фигурирует неэкранированный вариант, как и предполагалось.
Небольшое замечание: помимо %i и %I, существует еще несколько спецификаторов, и большинство из них доступно и в обычных файлах конфигурации юнитов, а не только в шаблонах. Подробности можно посмотреть на странице руководства, содержащей
полный перечень этих спецификаторов с краткими пояснениями.

Содержание
Вперед - Службы с активацией в стиле inetd
Назад - О судьбе /etc/sysconfig и /etc/default