После выхода 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'ов готов запуску/перезапуску как единое целое, плюс его будут запускать при старте нашего компьютера/сервера.
Автор: amarao