Автоматический (пере)запуск сервисов супервизором runit

Статья предназначена для системных администраторов, не использующих супервизоры и желающих попробовать что-то простое, эффективное и не требующее внесения в систему серьёзных изменений.

Наверняка каждому системному администратору unix-подобных операционных систем приходилось сталкиваться с ситуацией, когда по тем или иным причинам важный (или не очень) сервис, работающий на сервере, падал и его приходилось перезапускать заново. Многие так или иначе решали задачу по автоматизации поднятия сервисов в случае их нештатного завершения: справиться с этим может как простой скрипт, запускаемый по cron, отдельный watchdog-процесс, так и супервизор.

Остановлюсь вкратце на том, что из себя представляет супервизор в моём понимании. Итак, супервизор – это сервис, берущий на себя роль управления другими сервисами, а именно сохранением сервиса в том состоянии, в котором хочет его видеть системный администратор. Как правило, самой полезной ролью супервизора видится возможность автоматического поднятия сервиса в случае его нештатного завершения. Само это понятие далеко не ново и программ подобного рода существует множество, например: upstart, systemd, daemontools, runit, launchd, SysV-init.

Дальнейший рассказ пойдёт о супервизоре runit, созданного на базе идей более старого проекта daemontools.

Почему runit?

Несмотря на то, что тенденциями последних лет является смещение внимания в сторону init-процессов нового поколения, таких как upstart и systemd, не всем необходим весь спектр предоставляемых ими возможностей, кроме того, их использование подразумевает замену init-процесса и серьёзные изменения в системе, что может быть невозможно или нежелательно при использовании не самых новых версий mainstream-дистрибутивов. runit, напротив, включён во многие дистрибутивы достаточно давно1.

Принципы работы

Весьма неочевидным фактом является то, что в состав большинства 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. Так как процесс не демонизируется, а другими словами, не отрывается от своего родителя2, runit может следить за его состоянием, задействуя библиотечную функцию waitpid.

Сам супервизор runit состоит из нескольких взаимодействующих между собой программ:

Прежде, чем перейти к дальнейшему описанию настройки сервиса и управления им, приведу пример иерархии процессов, при использовании 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>

Наиболее часто используемые действия:

Полный перечень поддерживаемых действий и соответствующих им сигналов можно найти в 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-скриптами, дополнительные обработчики команд.


  1. В Debian runit появился в ноябре 2001 года.
  2. Демонизация – гарантированный разрыв связи с контролирующим терминалом, освобождение таких ресурсов как рабочий каталог, файловые дескрипторы. Подробнее см. http://www.steve.org.uk/Reference/Unix/faq_2.html#SEC16 (англ.)