From: kmeaw
Date: 2020-09-11 21:13:28Z
Сталкивался с той же проблемой несколько лет назад, когда писал
cgroup manager, и хотел уметь запускать его из sysvinit, runit и
upstart.
Концептуально проблема с том, что сервис может или сам заниматься
демонизацией, или отдать эту задачу супервизору.
В первом случае это усложняет работу супервизора:
У Upstart есть хак, который подключается через ptrace к процессу и ждёт,
пока он сделает fork или double fork. В systemd есть Type=forking, но
деталей реализации я не знаю.
У runit вообще нет никакого адекватного способа супервайзить такие
процессы. Приходится писать бесконечный цикл, который раз в секунду
проверяет, запущен ли процесс, и при этом не забывать о возможных гонках
за pid.
Также программа самостоятельно решает, куда она запишет pid-файл.
Первый случай хорош только для init-скриптов. В этом случае не надо
использовать start-stop-daemon --background --make-pidfile, а достаточно
просто запустить программу, а дальше она всё сделает сама.
Ещё далеко не все разработчики умеют правильно демонизировать процессы.
Кто-то забудет файловые дескрипторы позакрывать или ещё что-нибудь.
Кажется в nginx была проблема, которая проявлялась, если после
демонизации поменять размер окна эмулятора терминала - рабочему процессу
nginx прилетал SIGWINCH, и он завершался.
Во втором случае это усложняет процедуру оповещения пользователя об
успешном старте:
Пользователь ожидает, что запуск service XXX start выдаст ошибку, если
что-то где-то неправильно сконфигурировано, и нормальная работа сервиса
невозможна.
Для этого процесс сначала готовит себе окружение (читает/парсит конфиг,
загружает секреты и биндит сокеты, требующие root/capabilities, готовит
сетевые интерфейсы и так далее). В тот момент, когда сервис (почти)
готов работать, он сообщает супервизору о готовности и переходит в
рабочий режим.
Upstart предполагает, что процесс пошлёт себе SIGSTOP. Мне это решение
очень нравится — его легко использовать даже в ограниченных окружениях
(chroot, namespaces), и процессы обычно так не делают в любых других
ситуациях.
В systemd есть шина, на которую сервис должен послать сообщение о своей
готовности. Для этого нужна библиотека, которая умеет к ней подключаться
и доступ к сокету шины. Я хотел затащить вариант Upstart в systemd, но
нашёл тикет, в котором кто-то уже предлагал такую возможность, и Леннарт
Поттеринг закрыл его с WONTFIX с мотивировкой "не вижу, чем вариант с
raise(SIGSTOP); проще варианта с sd_notify("READY=1");".
Кажется ещё в systemd и upstart есть возможность подождать указанное в
конфиге время. Если сервис упадёт раньше, чем это время пройдёт, значит
он сломался.
В случае с init-скриптами обычно вставляют цикл, который проверяет
живость процесса и готовность сервиса работать — либо по маркерному
файлу, либо попыткой послать в него ping-запрос.
С runit я нормального решения не придумал. В итоге пошёл на
TOCTOU-компромисс, и просто вставил вызов configtest перед запуском.
Правда это никак не решает проблему с тем, что пользователь делает up на
сервис. Приходится делать обёртку, которая позволяет донести до
пользователя ошибку.
Наконец, есть альтернативный подход - переложить всю подготовительную
работу на супервизор. Пусть он кладёт секреты в environment, открывает
сокеты и запускает configtest. Увы, он не обладает должной
универсальностью — нельзя предсказать все хотелки сервиса при разработке
супервизора. Хотя runit/daemontools и systemd пытаются, предоставляя
tcpsvd (socket activation), envdir (EnvironmentFile=), chpst (куча
всего в sytsemd.exec) и прочее.
Из всего этого можно сделать вывод, что проблема вполне реальна. И
приходится либо решать её каждый раз (или в коде демонизации сервиса,
или в init-скрипте, проверяющим успешность старта), что часто приводит к
ошибкам, либо положиться на умный супервизор. Ни sysvinit, ни runit не
предоставляют удобных механизмов для уведомления пользователя. А Upstart
и systemd требуют кооперации со стороны сервиса, что не всегда приемлимо
для разработчика, особенно в случае необходимости работать на широком
спектре систем.
From: Sergey Matveev
Date: 2020-09-13 13:13:26Z
*** kmeaw [2020-09-12 00:10]:
>Концептуально проблема с том, что сервис может или сам заниматься
>демонизацией, или отдать эту задачу супервизору.
Лично я с ходу даже не помню как корректно нужно демонизироваться. Почту
что закрывать за собой дескрипторы, как минимум. Есть ещё double fork.
Когда-то знал для чего он нужен, но, так как на таком уровне не работал
толком, то и забывается оно. В Go вроде как проблематично сделать double
fork/whatever подходы для демонизации (чтобы легко, без хаков) и я был
немного разочарован. А с daemontools наоборот выясняется что лучше чтобы
демонизацией службы самостоятельно не занимались :-)
Насколько понимаю, грубо говоря, в идеале софт или не должен
демонизироваться сам, либо он должен давать отдельную опцию для этого.
Но многие так и делают. Одно удобнее для init-скриптов, другой для
супервизоров уже. Хотя далеко не каждый демон то состоит из одного
процесса.
>Upstart предполагает, что процесс пошлёт себе SIGSTOP.
>[...]
>В systemd есть шина, на которую сервис должен послать сообщение о своей
>готовности.
Ух, жесть. systemd не перестаёт меня удивлять :-). Но мне просто нужно
перестать воспринимать systemd-based системы как что-то UNIX-way-ное :-)
>С runit я нормального решения не придумал. В итоге пошёл на
>TOCTOU-компромисс, и просто вставил вызов configtest перед запуском.
Я вот недавно перевёл nsd и unbound под daemontools и у меня всё очень
тупо: вызывается отдельно проверка, дальше exec на демона:
# cat /var/service/nsd/run
#!/bin/sh
cfg=/usr/local/etc/nsd/nsd.conf
/usr/local/sbin/nsd-checkconf $cfg
exec /usr/local/sbin/nsd -d -c $cfg
# cat /var/service/unbound/run
#!/bin/sh
cfg=/usr/local/etc/unbound/unbound.conf
/usr/local/sbin/unbound-checkconf $cfg
exec /usr/local/sbin/unbound -c $cfg
>Наконец, есть альтернативный подход - переложить всю подготовительную
>работу на супервизор.[...]
Ага, в daemontools и особенно в runit заметил кучу подобных утилит.
Безусловно, как вы и пишете, предугадать все хотелки невозможно. Но я за
то чтобы была маленькая небольшая компактная система, удовлетворяющая
99% потребностей (определённого класса пользователей, конечно же). А там
где что-то не укладывается... ну или писать обёртки или вспомогательные
программы. Если кто-то насильно демонизируется и это мешает, то сделать
патч для программы, попробовать уговорить её разраба сделать возможно
оставаться в foreground.
>Из всего этого можно сделать вывод, что проблема вполне реальна. И
>приходится либо решать её каждый раз (или в коде демонизации сервиса,
>или в init-скрипте, проверяющим успешность старта), что часто приводит к
>ошибкам, либо положиться на умный супервизор. Ни sysvinit, ни runit не
>предоставляют удобных механизмов для уведомления пользователя. А Upstart
>и systemd требуют кооперации со стороны сервиса, что не всегда приемлимо
>для разработчика, особенно в случае необходимости работать на широком
>спектре систем.
Всё так, всё верно! Но... для меня факт остаётся фактом: одного из самых
популярных демонов я не смог запустить под systemd :-), с родным пакетом
из CentOS. Ну точнее он падал с тем что не может сделать bind и выходил
с ошибкой, а я не получал даже плохой return code. Просто уже сколько
лет CentOS/systemd существуют, но до сих пор так и не нашлись люди
которые бы "подружили" nsd с systemd в этом дистрибутиве.
Спасибо за массу интересной информации!