После выхода Ubuntu 16.04 (новый LTS релиз), systemd стал реальностью всех основных дистрибутивов Linux, использующихся на серверах. Это означает, что можно закладываться на расширенные возможности systemd, не рискуя оставить часть пользователей приложения «за бортом».

Этот пост о том, как реализовать многоворкерное приложение средствами systemd.

Abstract: Использование шаблонов сервисов и target'ов для запуска нескольких инстансов сервиса (реализация «воркеров»). Зависимость PartOf. Немного про [install] секцию у unit'ов.

Вступление


Многие языки программирования с плохой или никакой многопоточностью (Python, Ruby, PHP, довольно часто C/C++) используют концепцию «воркера». Вместо того, чтобы городить сложные отношения между тредами внутри приложения, они запускают несколько однопоточных копий приложения, каждое из которых берёт на себя кусок нагрузки. Благодаря опции SO_REUSEPORT есть даже возможность «вместе» слушать на одном и том же порту, что покрывает большинство задач, в которых возникает потребность в воркерах (собственно, обычные серверные приложения, реализующие API или обслуживающие веб-сайт).

Но такой подход требует наличия «супервизора», который отвечает за запуск копий, следит за их состоянием, обрабатывает ошибки, завершает при всякого рода stop/reload и т.д. При кажущейся тривиальности — это совершенно не тривиальная задача, полная нюансов (например, если один из воркеров попал в TASK_UNINTERRUPTIBLE или получил SIGSTOP, то могут возникнуть проблемы при restart у не очень хорошо написанного родителя).

Есть вариант запуска без супервизора, но в этом случае задача reload/restart перекладывается на администратора. При модели «один процесс на ядро» перезапуск сервиса на 24-ядерном сервере становится кандидатом в автоматизацию, которая в свою очередь требует обработки всех тех же самых SIGSTOP и прочих сложных нюансов.

Одним из вариантов решения проблемы является использование шаблонов сервисов systemd вместе с зависимостью от общего target'а.

Теория


Шаблоны


systemd поддерживает «шаблоны» для запуска сервисов. Эти шаблоны принимают параметр, который потом можно вставить в любое место в аргументах командной строки (man systemd.service). Параметр передаётся через символ '@' в имени сервиса. Часть после '@' (но до точки) называется 'instance name', кодируется %i или %I. Полный список параметров — www.freedesktop.org/software/systemd/man/systemd.unit.html#Specifiers. Наличие '@' в имени сервиса (перед точкой) указывает на то, что это шаблон.

Попробуем написать простейший template:

/etc/systemd/system/foobar-worker@.service
[Unit]
Description=Foobar number %I
[Service]
Type=simple
ExecStart=/bin/sleep 3600 %I


И запустим несколько таких:

systemctl start foobar-worker@1
systemctl start foobar-worker@2
systemctl start foobar-worker@300


Смотрим:
ps aux|grep sleep
root     13313  0.0  0.0   8516   748 ?        Ss   17:29   0:00 /bin/sleep 3600 1
root     13317  0.0  0.0   8516   804 ?        Ss   17:29   0:00 /bin/sleep 3600 2
root     13321  0.0  0.0   8516   764 ?        Ss   17:29   0:00 /bin/sleep 3600 300


Теперь мы хотим каким-то образом запускать всех их общим образом. Для этого существуют target'ы

Target'ы


Target — это такой юнит systemd, который ничего не делает, но может использоваться как элемент зависимостей (target может зависеть от нескольких сервисов, или сервисы могут зависеть от target'а, который так же зависит от сервисов).

target'ы имеют расширение .target.

Напишем наш простейший target:
vim /etc/systemd/system/foobar.target
[Unit]
Wants=foobar-worker@1.service foobar-worker@2.service
Wants=foobar-worker@300.service

(внимание на .service, оно обязательно!)
Про 'Wants' мы поговорим чуть ниже.

Теперь мы можем запускать все три foobar-worker одновременно:
systemctl start foobar.target
(внимание на target — в случае с .service его можно опускать, в случае с .target — нет).

В списке процессов появилось три sleep'а. К сожалению, если мы сделаем systemctl stop foobar.target, то они не исчезнут, т.е. на «worker'ов» они мало похожи. Нам надо как-то объединить в единое целое target и worker'ов. Для этого мы будем использовать зависимости.

Зависимости


Systemd предоставляет обширнейший набор зависимостей, позволяющий описать что именно мы хотим. Нас из этого списка интересует 'PartOf'. До этого мы использовали wants.
Сравним их поведение:
Wants (который мы использовали) — упомянутый сервис пытается стартовать, если основной юнит стартует. Если упомянутый сервис упал или не может стартовать, это не влияет на основной сервис. Если основной сервис выключается/перезапускается, то упомянутые в зависимости сервисы остаются незатронутыми.
PartOf — Если упомянутый выключается/перезапускается, то основной сервис так же выключется/перезапускается.

Как раз то, что нам надо.

Добавляем зависимость в описание воркера:

<pre>
[Unit]
Description=Foobar number %I
PartOf=foobar.target
[Service]
Type=simple
ExecStart=/bin/sleep 3600 %I


Всё. Если мы сделаем systemd stop foobar.target, то все наши воркеры остановятся.

Install-зависимости


Ещё одна интереснейшая фича systemd — install-зависимости. В sysv-init была возможность enable/disable сервисов, но там было очень трудно объяснить, как именно надо делать enable. На каких runlevel'ах? С какими зависимостями?

В systemd всё просто. Когда мы используем команду 'enable', то сервис «добавляется» (через механизм slice'ов) в зависимость к тому, что мы указали в секции [install]. Для нашего удобства есть зависимость WantedBy, которая по смыслу обратная к Wanted.

Есть куча стандартных target'ов, к которым мы можем цепляться. Вот некоторые из них (все — man systemd.special):
* multi-user.target (стандартное для «надо запуститься», эквивалент финального runlevel'а для sysv-init).
* default.target — алиас на multi-user
* graphical.target — момент запуска X'ов

Давайте прицепимся к multi-user.target.

Новое содержимое foobar.target:
[Unit]
Wants=foobar-worker@1.service foobar-worker@2.service
Wants=foobar-worker@300.service
[install]
WantedBy=multi-user.target


Теперь, если мы его сделаем enable:
# systemctl enable foobar.target
Created symlink /etc/systemd/system/multi-user.target.wants/foobar.target > /etc/systemd/system/foobar.target.


Всё, наш сервис, слепленный из нескольких worker'ов готов запуску/перезапуску как единое целое, плюс его будут запускать при старте нашего компьютера/сервера.
Поделиться с друзьями
-->

Комментарии (20)


  1. Akdmeh
    23.06.2016 16:59

    Подскажите, пожалуйста, какая разница между этим решением и supervisord?
    Или эти приложения используются для разных целей?


    1. kstep
      23.06.2016 17:56
      +6

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


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


    1. zelenin
      23.06.2016 18:28

      ну видимо теперь это из коробки
      UPD: не обновил комментарии.


    1. amarao
      23.06.2016 18:28
      +1

      supervisord — посторонний софт, которого даже в штатных репозиториях нет. systemd уже есть и (после выхода ubuntu 16.04) можно сказать, что есть всюду.


    1. estin
      24.06.2016 12:27

      Про различия ответили, но хочу немного добавить.


      Пользуюсь supervisord уже более 5 лет, и соглашусь со многими что — он с костылями, в этом нет ничего плохого )
      Хотя бы то что он не может "убить" дочерние процессы запущенные не им
      Сам не однократно испытывал проблемы при stop all и start all.


      Так что если есть возможность, то лучше избегать supervisord.


  1. Shamov
    23.06.2016 17:59

    Увы, слушать на одном порту внесколькером нельзя. Даже с опцией SO_REUSEPORT. Но это не закрывает саму возможность использовать отдельные процессы в качестве worker'ов. Можно сделать предельно простой менеджер, который будет принимать все соединения сам и сразу же передавать уже открытые сокеты worker'ам через shared memory.


  1. mr_elzor
    23.06.2016 18:01

    Еще удобно использовать monit для этих целей. Поднимет, если упадет и отправит уведомление админам.


    1. kstep
      23.06.2016 19:04
      +2

      Системдешные атрибуты сервисов OnFailure, Restart и RestartSec сделают то же самое.


      1. justabaka
        23.06.2016 22:36

        Да, но systemd все-таки (пока что) не умеет в e-mail, очень вкусные сервис-чеки и mmonit.


        1. kstep
          24.06.2016 00:41

    1. lolipop
      23.06.2016 23:49

      а еще можно использовать runit. к тому же, к нему есть отличный веб-интерфейс runit-man. пользуюсь им с 10-го года, он очень простой и этим очень подкупает. с тех пор ни разу меня еще не подвел.
      к сожалению, без внешних костылей скриптов повторить то, что описано в статье — нельзя.


      1. grossws
        24.06.2016 00:10

        Когда использовал runit в прошлый раз — он не умел в зависимости от слова совсем. Что-нибудь изменилось на этом фронте?


        Upstart, конечно, в этом плане тоже не сахар, если конкретный сервис зависит от более чем одного стороннего, но существенно лучше чем отсутствие поддержки совсем.


        1. lolipop
          24.06.2016 00:32

          автор предлагает такой вариант. не очень изящно :)

          Upstart, конечно, в этом плане тоже не сахар, если конкретный сервис зависит от более чем одного стороннего

          там вроде есть броадкаст-сигналы(emits), давно это было.


          1. grossws
            24.06.2016 01:34

            автор предлагает такой вариант. не очень изящно :)


            Этот костыль я видел, но он не связывает состояния сервисов: при запуске cron'а в этом примере будет выполнена попытка запустить socklog-unix. При останове cron'а не произойдёт ничего, и, хуже того, при останове socklog-unix не будет остановлен cron, если он не упадёт сам.
            там вроде есть броадкаст-сигналы(emits), давно это было.


            И плюс сигналы started/stopping, но делать зависимость от двух сервисов там геморрой.
            Допустим, что s1 зависит от s2 и s3 и требуется работа обоих для работы s1. Тогда можно написать start on started s2 and started s3, stop on stopping s2 or stopping s3. Допустим, всё запущенно и необходимо перезапустить s2. При restart s2 произойдёт останов s1 (по событию stopping s2), а запуск s1 не произойдёт, т. к. было starting s2, но не starting s3.
            В случае systemd будет достаточно двух зависимостей типа requires.


            1. lolipop
              24.06.2016 11:13

              мы используем runit для сервисов, котрые всегда работают(около 16 тысяч машин) и он себя зарекомендовал очень хорошо. ну и вообще, удобно, когда мухи от котлет, в смысле системные сервисы и конторские отдельно лежат, можно, например, сделать sv status /etc/service/*, чтобы получить полное предствление о происходящем на машине.


  1. IvanPanfilov
    23.06.2016 19:44
    +2

    В кои то веки — годная статья на хабре.


  1. icoz
    23.06.2016 23:59

    А можно подробности про написание сервисов. В частности, интересно как в сервисе прописать последовательность действий? (а не одну команду)


    1. surefire
      24.06.2016 00:54
      +2

      Если нужно запустить команды до или после старта, то есть `ExecStartPre=` `ExecStartPost=` и тому подобное для остановки и других действий. Совсем сложную логику лучше вынести наружу в обычный shell скрипт. Systemd — это дирижер, он не должен играть за музыкантов.


      1. kstep
        24.06.2016 01:31
        +2

        Ещё есть зависимости вида After и Before, которые в сочетании с Requires позволяют запускать сервисы после и до заданного.


    1. amarao
      24.06.2016 13:22

      Последнее, что я видел — обычный хак с bash -c «foo --one|bar --two && three».