Автоматический (пере)запуск сервисов супервизором runit
Статья предназначена для системных администраторов, не использующих супервизоры и желающих попробовать что-то простое, эффективное и не требующее внесения в систему серьёзных изменений.
Наверняка каждому системному администратору unix-подобных операционных систем приходилось сталкиваться с ситуацией, когда по тем или иным причинам важный (или не очень) сервис, работающий на сервере, падал и его приходилось перезапускать заново. Многие так или иначе решали задачу по автоматизации поднятия сервисов в случае их нештатного завершения: справиться с этим может как простой скрипт, запускаемый по cron, отдельный watchdog-процесс, так и супервизор.
Остановлюсь вкратце на том, что из себя представляет супервизор в моём понимании. Итак, супервизор – это сервис, берущий на себя роль управления другими сервисами, а именно сохранением сервиса в том состоянии, в котором хочет его видеть системный администратор. Как правило, самой полезной ролью супервизора видится возможность автоматического поднятия сервиса в случае его нештатного завершения. Само это понятие далеко не ново и программ подобного рода существует множество, например: upstart, systemd, daemontools, runit, launchd, SysV-init.
Дальнейший рассказ пойдёт о супервизоре runit, созданного на базе идей более старого проекта daemontools.
Почему runit?
Несмотря на то, что тенденциями последних лет является смещение внимания в сторону init-процессов нового поколения, таких как upstart и systemd, не всем необходим весь спектр предоставляемых ими возможностей, кроме того, их использование подразумевает замену init-процесса и серьёзные изменения в системе, что может быть невозможно или нежелательно при использовании не самых новых версий mainstream-дистрибутивов. runit, напротив, включён во многие дистрибутивы достаточно давно.
Принципы работы
Весьма неочевидным фактом является то, что в состав большинства Linux-дистрибутивов давно включён супервизор, и имя ему – SysV init (/sbin/init
). Если вспомнить определение супервизора, данное выше, а также заглянуть в файл /etc/inittab
, станет всё понятно. Действительно, в /etc/inittab
содержатся инструкции для процесса init по запуску и перезапуску определённых сервисов. Например, приведённая ниже строка отвечает за перезапуск процесса /sbin/getty
в случае его завершения – ну чем не наглядное представление работы супервизора?
1:2345:respawn:/sbin/getty 38400 tty1
К сожалению, хотя SysV init и можно использовать в качестве супервизора для запуска сервисов, это довольно неудобно, т.к. формат inittab всё-таки ориентирован на базовые сервисы, позволяющие обеспечить к ней доступ, а также передать управление загрузкой более продвинутым механизмам (rc.d/init.d скрипты).
Для того, чтобы runit мог выполнять свои функции по слежению за сервисами, есть, пожалуй, лишь одно требование к такому сервису: процесс не должен демонизироваться. К счастью, большинство сервисов поддерживают запуск без демонизации (во многих это называется foreground режимом, или отладочным режимом). Путём простейшей конфигурации подобный процесс может быть запущен средствами супервизора runit. Так как процесс не демонизируется, а другими словами, не отрывается от своего родителя, runit может следить за его состоянием, задействуя библиотечную функцию waitpid.
Сам супервизор runit состоит из нескольких взаимодействующих между собой программ:
- процесс
runsvdir
запускается init-процессом (предполагается, что запуск runsvdir
будет настроен в файле /etc/inittab
– об этом дальше) и следит за служебным каталогом, в подкаталогах которого находятся настроенные сервисы; при появлении нового сервиса, для него запускается программа runsv
;
runsv
– индивидуальный процесс-супервизор конкретного сервиса, выполняет запуск сервиса, слежение за его состоянием, обеспечивает отправку сигналов процессу, поданную при помощи управляющей программы sv
;
sv
– консольная программа, через которую предполагается изменение состояния сервисов системным администратором.
Прежде, чем перейти к дальнейшему описанию настройки сервиса и управления им, приведу пример иерархии процессов, при использовании runit:
init,1
`-runsvdir,1464 -P /etc/service...
`-runsv,1469 openid
`-python,1480 /usr/lib/cgi-bin/openidserver.fcgi /tmp/openid.socket
На иллюстрации очевидна иерархия процессов: init
запустил runsvdir
, который запустил runsv
для сервиса openid, а runsv
, в свою очередь, уже запустил процесс самого сервиса (python
). Все перечисленные процессы имеют строго определённый родительский процесс, который может контролировать выполнение дочернего процесса.
Знакомимся на практике
Одним из преимуществ runit является простота запуска под ним сервисов. В то время как обычный управляющий SysV-init скрипт сервиса (загляните в /etc/init.d
) включает в себя несколько условно испольняемых блоков кода, а также магию разных оттенков для работы с pid-файлами, скрипт запуска сервиса под супервизором runit выглядит как обычный shell-скрипт с указанием бинарника сервиса и параметров его запуска.
Как вы могли заметить в приведённом ранее примере, контролирующий процесс runsv
был запущен для сервиса openid. Если подняться в этом примере строкой выше, становится видно, что runsvdir
запущен с параметром -P /etc/service
. Таким образом задаётся служебный каталог, содержащий сервисы – за ним runsvdir
следит и, при появлении в этом каталоге описания нового сервиса, запускает его.
Установка runit
Установка специфична для конкретного дистрибутива, я рассмотрю установку на базе Debian. Установить runit можно стандартным образом из официальных репозиториев:
apt-get install runit
В процессе установки в файл /etc/inittab
будет автоматически добавлена следующая строка:
SV:123456:respawn:/usr/sbin/runsvdir-start
Файл runsvdir-start
представляет собой обычный shell-скрипт, в нём выполняется установка минимального набора переменных окружения и запуск runsvdir -P /etc/service
. В актуальных дистрибутивах Debian именно /etc/service
принимается за каталог для размещения описаний сервисов, в старых версиях дистрибутива (Etch и ранее), этим каталогом был /var/service
. Если вы используете другой дистрибутив, каталог может использовать отличный от указанных.
Создание сервиса
Хорошей практикой является создание сервисов для runit в отдельном каталоге, а затем “включение” их в runit при помощи создания симлинка в /etc/service
(или того каталога, за которым следит runsvdir
). В Debian для этих целей принято использовать /etc/sv
.
Описание сервиса представляет собой отдельный каталог с находящимся в нём исполняемом файлом run
. Это минимальная (а зачастую и вполне достаточная) конфигурация.
Создать тестовый сервис можно следующим образом:
mkdir -p /etc/sv/dummy
cat <<EOF > /etc/sv/dummy/run
#!/bin/sh
exec /bin/sleep 15
EOF
chmod +x /etc/sv/dummy/run
Вот и всё – полученный каталог /etc/sv/dummy
с содержащимся в нём скриптом run
уже является полноценным сервисом runit, но ещё не запущенным.
Прежде, чем мы активируем этот сервис, акцентирую внимание на использованную конструкцию exec
: мы не порождаем отдельный дочерний процесс от интерпретатора /bin/sh
, а именно заменяем текущий процесс shell процессом /bin/sleep
– так непосредственным наследником процесса runsv
будет именно /bin/sleep
, а не /bin/sh
. Это особо актуально из-за того, что процесс-супервизор runsv
будет отправлять опекаемому им сервису различные сигналы, а нам необходимо, чтобы сигнал получил именно сам сервис (в нашем случае /bin/sleep
), а не запустивший его /bin/sh
.
Давайте запустим созданный сервис, для этого лишь создадим симлинк:
ln -s /etc/sv/dummy /etc/service
Понаблюдав за списком процессов (pstree
, ps fax
), через несколько секунд мы увидим, что runsvdir
запустил runsv dummy
, который запустил /bin/sleep 15
.
Управление сервисом
Управление сервисом традиционно выполняется отправкой процессу сигналов, runsv
значительно упрощает эту задачу – больше не потребуется искать правильный процесс, читая его pid из файла и идти на другие подобные ухищрения.
Интерфейс отправки сигналов / управления сервисом следующий:
sv <action> <service>
Наиболее часто используемые действия:
- status – показать текущее состояние сервиса;
- up – запустить сервис, если он остановлен, перезапускать в случае его завершения;
- down – корректно завершить и остановить работающий сервис (посылает TERM и CONT, после завершения процесса не перезапускает его);
- once – запустить сервис единоразово, если он остановлен, не перезапускать, если завершится;
- terminate – корректно завершить сервис (отправить TERM);
- kill – принудительно завершить сервис (отправить KILL);
- pause – поставить на паузу выполнение сервиса (отправляет STOP);
- continue – продолжить выполнение поставленного на паузу сервиса (CONT);
- hangup – отправить HUP;
- interrupt – отправить INT.
Полный перечень поддерживаемых действий и соответствующих им сигналов можно найти в man 8 sv
и man 8 runsv
(). Поддерживается также указание действий в сокращённом виде – по минимальному уникальному префиксу, которые в большинстве случаев составляет одну букву: sv st service
означает то же самое, что и sv s service
и sv status service
.
Пример использования:
# sv status nginx
run: nginx: (pid 27945) 6883886s; run: log: (pid 27861) 6884566s
# sv h nginx
# sv s nginx
run: nginx: (pid 27945) 6884013s; run: log: (pid 27861) 6884693s
Запись логов
Обратите внимание на предыдущий пример с просмотром состояния сервиса и на разницу с нашим dummy-сервисом:
# sv s dummy
run: dummy: (pid 27921) 4s
# sv s nginx
run: nginx: (pid 27945) 6884360s; run: log: (pid 27861) 6885040s
Для сервиса nginx формат вывода отличается наличием там дополнительной записи log
. Разберёмся, что это такое.
Помимо управления сервисом, супервизор runit
может взять на себя также задачи по записи и ротации логами. Зачем это может понадобиться, когда есть стандартный syslog в каждой системе? Логирование через runit
значительно проще настраивается и обладает большим плюсом: позволяет иметь предсказуемый размер логов.
В простейшем случае мы будем записывать в лог stdout и stderr сервиса. В соответствии с идеологией unix, runit
решает задачу логирования с помощью отдельного процесса svlogd
, который принимает поток данных (лог) на stdin и пишет его в лог-файлы в указанном каталоге, при необходимости выполняя ротацию логов, открывая/закрывая файлы и удаляя те из них, которые слишком стары (в соответствии с настройками).
Взглянём ещё раз на следующий вывод:
# sv s nginx
run: nginx: (pid 27945) 6884360s; run: log: (pid 27945) 6885040s
То, что мы здесь видим, следует интерпретировать следующим образом: работает сервис nginx с pid 27945 и соответствующий ему сервис логирования с pid 27945.
Сервис логирования в runit
привязывается к основному сервису созданием каталога сервиса логирования внутри каталога самого сервиса:
# find /etc/sv/nginx -name run
/etc/sv/nginx/log/run
/etc/sv/nginx/run
Здесь /etc/sv/nginx/run
– скрипт запуска сервиса nginx, /etc/sv/nginx/log/run
скрипт запуска логирующего сервиса для него. Рассмотрим этот пример подробнее.
Скрипт запуска сервиса (/etc/sv/nginx/run
):
#!/bin/sh
exec 2>&1
exec /usr/sbin/nginx -g 'daemon off;'
Обращаем внимание на перенаправление stderr в stdout – по-умолчанию, лог будет брать данные из stdin.
Скрипт запуска логирующего сервиса (/etc/sv/nginx/log/run
):
#!/bin/sh
exec chpst -u www-data svlogd -tt /var/log/nginx-svlog
Здесь мы запускаем процесс svlogd
, который принимает данные из stdin и пишет их в лог-файлы в каталоге /var/log/nginx-svlog
.
После добавления созданного сервиса в супервизор (ln -s /etc/sv/nginx /etc/services
) runit
автоматически запустит сервис nginx
, соответствующий ему сервис nginx/log
и перенаправит stdout первого на stdin второго.
Иерархия процессов будет следующая:
# sv s nginx
run: nginx: (pid 27945) 6886060s; run: log: (pid 27861) 6886740s
# pstree -Asp 27945
init(1)---runsvdir(1464)---runsv(27860)---nginx(27945)
# pstree -Asp 27861
init(1)---runsvdir(1464)---runsv(27860)---svlogd(27861)
Продолжение следует. В следующей части статьи будут описаны дополнительные возможности супервизора runit: поддержка переменных окружения, автоматическая ротация логов, совместимость с init.d-скриптами, дополнительные обработчики команд.