Этот пост о том, как реализовать многоворкерное приложение средствами 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)
Shamov
23.06.2016 17:59Увы, слушать на одном порту внесколькером нельзя. Даже с опцией SO_REUSEPORT. Но это не закрывает саму возможность использовать отдельные процессы в качестве worker'ов. Можно сделать предельно простой менеджер, который будет принимать все соединения сам и сразу же передавать уже открытые сокеты worker'ам через shared memory.
mr_elzor
23.06.2016 18:01Еще удобно использовать monit для этих целей. Поднимет, если упадет и отправит уведомление админам.
lolipop
23.06.2016 23:49а еще можно использовать runit. к тому же, к нему есть отличный веб-интерфейс runit-man. пользуюсь им с 10-го года, он очень простой и этим очень подкупает. с тех пор ни разу меня еще не подвел.
к сожалению, без внешнихкостылейскриптов повторить то, что описано в статье — нельзя.grossws
24.06.2016 00:10Когда использовал runit в прошлый раз — он не умел в зависимости от слова совсем. Что-нибудь изменилось на этом фронте?
Upstart, конечно, в этом плане тоже не сахар, если конкретный сервис зависит от более чем одного стороннего, но существенно лучше чем отсутствие поддержки совсем.
lolipop
24.06.2016 00:32автор предлагает такой вариант. не очень изящно :)
Upstart, конечно, в этом плане тоже не сахар, если конкретный сервис зависит от более чем одного стороннего
там вроде есть броадкаст-сигналы(emits), давно это было.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.lolipop
24.06.2016 11:13мы используем runit для сервисов, котрые всегда работают(около 16 тысяч машин) и он себя зарекомендовал очень хорошо. ну и вообще, удобно, когда мухи от котлет, в смысле системные сервисы и конторские отдельно лежат, можно, например, сделать sv status /etc/service/*, чтобы получить полное предствление о происходящем на машине.
icoz
23.06.2016 23:59А можно подробности про написание сервисов. В частности, интересно как в сервисе прописать последовательность действий? (а не одну команду)
surefire
24.06.2016 00:54+2Если нужно запустить команды до или после старта, то есть `ExecStartPre=` `ExecStartPost=` и тому подобное для остановки и других действий. Совсем сложную логику лучше вынести наружу в обычный shell скрипт. Systemd — это дирижер, он не должен играть за музыкантов.
kstep
24.06.2016 01:31+2Ещё есть зависимости вида
After
иBefore
, которые в сочетании сRequires
позволяют запускать сервисы после и до заданного.
amarao
24.06.2016 13:22Последнее, что я видел — обычный хак с bash -c «foo --one|bar --two && three».
Akdmeh
Подскажите, пожалуйста, какая разница между этим решением и supervisord?
Или эти приложения используются для разных целей?
kstep
Супервизорд написан на питоне и надо ставить отдельно.
Системд доступен на большинстве современных дистрибутивов из коробки и написан на Си.
Системд, кроме всего прочего, управляет всей системой, а не только одним сервисом, то есть более универсален,
плюс предоставляет гораздо более широкие возможности по тонкой настройке и изоляции доступных ресурсов.
Вообще супервизорд был написан в стародавние времена убогого SysV, который только запускает-останавливает сервисы,
не давая таких широких возможностей для настройки среды процессов, их перезапуска и т.п.
Так что в наше время, когда есть апстарт и системд, которые уже умеют всё это, он выглядит просто древним костылём.
zelenin
ну видимо теперь это из коробки
UPD: не обновил комментарии.
amarao
supervisord — посторонний софт, которого даже в штатных репозиториях нет. systemd уже есть и (после выхода ubuntu 16.04) можно сказать, что есть всюду.
estin
Про различия ответили, но хочу немного добавить.
Пользуюсь supervisord уже более 5 лет, и соглашусь со многими что — он с костылями, в этом нет ничего плохого )
Хотя бы то что он не может "убить" дочерние процессы запущенные не им
Сам не однократно испытывал проблемы при stop all и start all.
Так что если есть возможность, то лучше избегать supervisord.