Dans cet article, je vais vous présenter supervisor un système de supervision et gestion de processus. Je l'utilise pour gérer les différents processus utilisés pour servir des applications Django, Ruby on Rails ou WSGI. Je vais vous présenter l'outil en lui-même, ses avantages, comment l'installer et le configurer, comment définir les processus qu'on veut gérer avec supervisor en donnant quelques exemples, la commande de gestion supervisorctl et son utilisation possible pour des déploiements automatisés (avec Fabric par exemple).

Introduction

Dans deux précédents articles, j'ai parlé de ma migration de Apache vers Nginx. Ainsi que de l'utilisation de gunicorn et unicorn pour "servir" mes projets Django et Ruby on Rails respectivement. J'avais créé deux scripts init.d pour gérer le lancement automatique de ceux-ci.

Mais cette solution n'était pas entièrement satisfaisante et on m'a alors parlé de supervisor, runit, god... qui sont des logiciels permettant de gérer le lancement de processus personnalisés avec un énorme plus : la supervision des processus lancés avec ce type de programmes : si un processus crashe il est relancé automatiquement !

J'ai donc choisi de tester supervisor et, vu qu'il m'a convaincu, je l'ai gardé. Et en plus il est écrit en Python :)

Installation et configuration

Pour l'installation, vous avez le choix entre les outils fournis par votre distribution (# aptitude install supervisor pour moi) ou bien avec pip install supervisor. Je pars du principe que vous allez utiliser la version fournie par votre distribution (et que vous utilisez Debian/Ubuntu bien sûr !) : en soi l'installation avec pip n'aurait un intérêt que pour avoir une version plus récente du logiciel or supervisor est stable, et ne dois donc plus évoluer beaucoup. L'avantage d'utiliser la version Debian est que tout est déjà prêt : les fichiers init pour le lancement automatique du démon supervisord au démarrage du serveur, l'arborescence dans /etc/supervisord et même une configuration minimale suffisante pour démarrer.
Le seul hic c'est que Debian Lenny (l'actuelle stable) n'a pas supervisor dans ses dépôts, mais une version existe dans testing/Squeeze et est parfaitement installable sur Lenny.

La configuration de supervisor en lui-même se fait dans le fichier supervisord.conf dont la configuration par défaut suffit pour un fonctionnement standard, mais n'hésitez pas à regarder si vous voulez changer quelques options (comme lancer un mini serveur web de contrôle de supervisor).

Définition des processus et exemples

La configuration de chaque processus supervisé se fait par contre via un fichier distinct pour chaque processus et ceux-ci sont stockés sous Debian dans /etc/supervisor/conf.d avec un nom terminant par .conf.

Je ne vais pas entrer dans le détail du fonctionnement de supervisor et donc de la syntaxe et des possibilités des fichiers de configuration mais plutôt vous donnez quelques exemples en expliquant les points intéressants.

application WSGI sous Gunicorn (ici l'interface web de Mercurial)

[program:code_domain_tld]
command=/usr/bin/gunicorn code_domain_tld:application -c /var/www/code_domain_tld/gunicorn.conf.py
directory=/var/www/code_domain_tld
user=www-data
autostart=true
autorestart=true
startsecs=10
redirect_stderr=true
stdout_logfile=/var/log/supervisor/code_domain_tld.gunicorn.log

program est le nom du service pour supervisor, c'est celui qui est utilisé ensuite pour le contrôle (cf plus bas). J'ai tendance à mettre le nom de domaine qui est servi par ce service[1]
command est le nom de la commande qui va être lancée et contrôlée par supervisor, ici c'est simplement gunicorn en indiquant le chemin du fichier de configuration. Pour le code_domain_tld.application c'est l'application WSGI qui va être lancée par gunicorn. Pour ce cas précis, c'est un fichier code_domain_tld.py contenant

#!/usr/bin/env python

import os
import sys
from mercurial.hgweb.hgwebdir_mod import hgwebdir
from mercurial.hgweb.request import wsgiapplication

os.environ["HGENCODING"] = "UTF-8"

def make_web_app():
    return hgwebdir("/etc/mercurial/hgweb.code.domain.tld.config")

def application(environ, start_response):
    environ['wsgi.url_scheme'] = environ.get('HTTP_X_URL_SCHEME', 'http')
    app = wsgiapplication(make_web_app)
    return app(environ, start_response)

directory est le répertoire de travail de la commande précédente
user permet de spécifier l'utilisateur qui sera utilisé pour lancer le programme : veuillez à ce qu'il ait les droits sur les fichiers nécessaires et surtout évitez root !
autostart et autorestart me semblent assez clairs : indiquer si le programme (la commande) doit être lancé automatiquement et relancé automatiquement en cas de crash
startsecs est le nombre de secondes à partir desquelles supervisor va vérifier l'état du programme pour voir si il s'est lancé correctement. Si un programme met beaucoup de temps à se lancer on peut jouer sur cette valeur pour que supervisor attende avant de le considérer comme crashé
les options suivantes concernent la journalisation (logs) de ce programme par supervisor

application Django sous Gunicorn

[program:domain_tld]
command=/opt/django/domain_tld/_venv/bin/python /opt/django/domain_tld/_venv/bin/gunicorn_django -c /opt/django/domain_tld/gunicorn.conf.py
directory=/opt/django/domain_tld/projet
user=www-data
autostart=true
autorestart=true
startsecs=10
redirect_stderr=true
stdout_logfile=/var/log/supervisor/domain_tld.gunicorn.log

command la différence se situe ici avec l'utilisation d'un interpréteur Python dans un virtualenv. Le reste est similaire à l'exemple au-dessus.

application Ruby on Rails sous Thin

[program:dev_domain_tld
command=/var/lib/gems/1.8/bin/thin start -C /opt/redmine/config/thin.yml
directory=/opt/redmine
user=www-data
autostart=true
autorestart=true
startsecs=10
redirect_stderr=true
stdout_logfile=/var/log/supervisor/dev_domain_tld.thin.log

Ici c'est pareil, seule la commande change pour lancer un programme différent Thin[2] pour faire tourner une application Ruby on Rails.

Il est très important de noter que pour que supervisor puisse contrôler vos processus (leur état, les arrêter, les redémarrer) il faut que ceux-ci ne partent pas en tâche de fond ('daemonize'). Pour gunicorn par exemple, il faut spécifier daemon=False dans le fichier de configuration.

L'application de gestion supervisorctl

Supervisor apporte aussi un outil de grande utilité : sa commande supervisorctl qui permet de vérifier l'état des différents processus ainsi que de les contrôler. Voyons quelques exemples de son utilisation en mode interactif.

$ supervisorctl
code_domain_tld                RUNNING    pid 2143, uptime 18 days, 18:13:41
domain_tld                     RUNNING    pid 2140, uptime 18 days, 18:13:41
dev_domain_tld                 RUNNING    pid 2139, uptime 18 days, 18:13:41
supervisor>

On voit que quand on lance la commande, on a tout de suite l'information sur l'état des processus gérés par supervisor.

supervisor> status
code_domain_tld                RUNNING    pid 2143, uptime 18 days, 18:13:41
domain_tld                     RUNNING    pid 2140, uptime 18 days, 18:13:41
dev_domain_tld                 RUNNING    pid 2139, uptime 18 days, 18:13:41

La commande status renvoie cette même information, mais on peut utiliser status <nom d'un processus> pour avoir uniquement l'état d'un processus

supervisor> stop code_domain_tld
code_domain_tld: stopped
supervisor> start code_domain_tld
code_domain_tld: started
supervisor> restart code_domain_tld
code_domain_tld: stopped
code_domain_tld: started

Assez logiquement les commandes start, stop, restart suivies du nom d'un processus agissent sur le processus en question en l'arrêtant, le démarrant ou le redémarrant.

D'autres commandes peuvent être utiles, tapez help dans supervisorctl pour en savoir d'avantage.

L'énorme avantage de supervisorctl c'est qu'en plus d'être utilisable en mode interactif comme on l'a vu au dessus il est également utilisable en mode non-interactif. Vous prenez les commandes évoquées au-dessus et les indiquez directement en tant qu'arguments à la commande supervisorctl : la commande est lancée par supervisorctl qui vous rend la main aussitôt. Exemple :

$ supervisorctl status
code_domain_tld                RUNNING    pid 2143, uptime 18 days, 18:13:41
domain_tld                     RUNNING    pid 2140, uptime 18 days, 18:13:41
dev_domain_tld                 RUNNING    pid 2139, uptime 18 days, 18:13:41
$

J'utilise cette possibilité notamment dans mes scripts Fabric pour déployer rapidement des projets Django et les redémarrer. Un bout de code comme suit mettra à jour la configuration supervisor du projet si nécessaire, et relancera le processus en utilisant la nouvelle configuration :

def wsgiserver():
    filename = '%s.conf' % env.app_name
    dir = '/etc/supervisor/conf.d'
    if exists('%s/%s' % (dir, filename)):
        choice = prompt('supervisord conf file already exists, overwrite (Y/N)?', default='N', validate=r'[YyNn]')
        if choice.upper() == 'N':
            return
    put(filename, '%s/%s' % (dir, filename))
    run('sudo supervisorctl reread')
    run('sudo supervisorctl update')

Liens

Notes

[1] en substituant les . par des _ comme vous l'avez sûrement remarqué : je ne me souviens plus pourquoi mais je pense que à l'origine ça m'était nécessaire pour Python ou Gunicorn et j'ai donc gardé cette habitude

[2] oui avant j'utilisais Unicorn mais j'avais des problèmes qui se sont résolus en changeant de serveur