ka.da

Aller au contenu | Aller au menu | Aller à la recherche

mardi, avril 6 2010

Héberger des projets Django avec Nginx et Gunicorn

Mise à jour : j'ai finalement décidé d'utiliser supervisor pour gérer/monitorer ce genre de services,voyez ce billet.

Je continue l'optimisation de mon serveur web (cf le premier épisode) avec cette fois-ci mes projets Django que j'ai migré vers Nginx et Gunicorn. (veuillez noter que Gunicorn permet également de faire tourner d'autres types d'applications Python que Django)

Virtualenv

Pour gérer mes projets Django, j'utilise virtualenv et toute cette doc part donc du principe que vous l'utilisez également. Si ce n'est pas le cas 1. je vous encourage à le faire ;) 2. vous devrez adapter cette doc en conséquence.

Gunicorn

On commence par installer Gunicorn dans chacun des environnements virtuels hébergeant vos projets Django (cf plus bas pour les détails)

$ pip install gunicorn

ou

$ easy_install gunicorn

Ensuite pour chaque projet Django qui sera propulsé par Gunicorn, le plus simple est de créer un fichier de configuration gunicorn.conf.py pour indiquer les options du serveur à son lancement. Voici un exemple de fichier :

backlog = 2048
bind = "unix:/var/run/gunicorn/monprojet.sock"
pidfile = "/var/run/gunicorn/monprojet.pid"
daemon = True
debug = False
workers = 2
logfile = "/opt/django/www_domain_tld/log/gunicorn.log"
loglevel = "info"

Pour bind vous pouvez soit spécifier un socket Unix soit une adresse et un port TCP/IP. Veuillez consulter la doc configuration pour les détails.

Vous pouvez alors lancer gunicorn avec la commande suivante :

$ gunicorn_django -c gunicorn.conf.py

Bien sûr, comme pour l'article précédent que se passe-t-il en cas de redémarrage du serveur ? Il faut relancer les processus gunicorn un par un. Pour éviter ce genre de désagréments j'ai donc créé un script destiné au système de démarrage sysvinit de Debian.

Ce script a comme prérequis :

  • d'avoir pour chaque projet Django
    • un virtualenv dédié dans un répertoire www_domaine_tld
    • dans ce virtualenv un répertoire nommé monprojet avec les sources de celui-ci et notamment les fichiers importants pour Django (settings.py, urls.py...)
  • d'avoir un répertoire /etc/gunicorn/sites
  • dans ce répertoire vous faites un lien vers le virtualenv de chaque projet en lui donnant le nom du répertoire du projet, exemple :
# cd /etc/gunicorn/sites
# ln -s /opt/django/www_domaine_tld monprojet
  • le fichier gunicorn.conf.py est situé dans le virtualenv (donc dans le répertoire parent de celui des sources du projet)
  • il faut que le pid spécifié dans ce fichier soit de la forme monprojet.pid puisque son nom est ainsi extrapolable par rapport au nom du lien dans /etc/gunicorn/sites

Ce qui donne (pour éclaircir tout ça) une arborescence comme suit :

/etc/gunicorn/sites/
                    monprojet1 -> /opt/django/www_domaine1_tld      # lien vers /opt/django/www_domaine1_tld
                    monprojet2 -> /opt/django/www_domaine2_tld      # lien vers /opt/django/www_domaine2_tld

/opt/django/
            www_domaine1_tld/
                             bin/                         #
                             include/                     # répertoires créés par virtualenv
                             lib/                         # 
                             monprojet1/                  # répertoire contenant les sources du projet
                             gunicorn.conf.py             # fichier de configuration de gunicorn
            www_domaine2_tld/
                             bin/                         #
                             include/                     # répertoires créés par virtualenv
                             lib/                         # 
                             monprojet2/                  # répertoire contenant les sources du projet
                             gunicorn.conf.py             # fichier de configuration de gunicorn

Et voici le script /etc/init.d/gunicorn

#!/bin/sh 

### BEGIN INIT INFO
# Provides:       gunicorn
# Required-Start: $local_fs $syslog
# Required-Stop:  $local_fs $syslog
# Default-Start:  2 3 4 5
# Default-Stop:   0 1 6
# Short-Description: Gunicorn processes
### END INIT INFO

USER=www-data
NAME="gunicorn"
DAEMON="gunicorn_django"
CONFDIR=/etc/gunicorn/sites
PIDDIR=/var/run/gunicorn
VENV_ACTIVATION="source ../bin/activate"
CONFFILE="../gunicorn.conf.py"
OPTIONS="-c $CONFFILE"
RETVAL=0

# source function library
. /lib/lsb/init-functions

# pull in default settings
[ -f /etc/default/gunicorn ] && . /etc/default/gunicorn

start()
{
    echo $"Starting $NAME."
    cd $CONFDIR;
    for d in *; do
        echo -n $d;
        PIDFILE=$PIDDIR/$d.pid
        if [ -f $PIDFILE ]; then
            echo ": already started!"
        else
            cd $d;
            cd $d;
            su -c "$VENV_ACTIVATION; $DAEMON $OPTIONS" $USER && echo ": OK";
        fi
    done
    echo "done"
}

stop()
{
    echo $"Stopping $NAME:"
    cd $CONFDIR
    for d in *; do
        echo -n $d;
        if [ -f $PIDDIR/$d.pid ]
        then kill -QUIT `cat $PIDDIR/$d.pid` && echo ": OK" || echo ": failed";
        fi
    done
    echo "done"
}

reload()
{
    echo $"Reloading $NAME:"
    cd $CONFDIR
    for d in *; do
        echo -n $d;
        if [ -f $PIDDIR/$d.pid ]
        then kill -HUP `cat $PIDDIR/$d.pid` && echo ": OK" || echo ": failed";
        fi
    done
    echo "done"
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        reload
        ;;
    reload)
        reload
        ;;
    force-reload)
        stop && start
        ;;
    *)
        echo $"Usage: $0 {start|stop|restart}"
        RETVAL=1
esac
exit $RETVAL

Nginx

Pour Nginx, on retrouve un fichier de configuration semblable à celui-ci :

upstream www_domaine_tld {
    server      unix:/var/run/gunicorn/monprojet.sock fail_timeout=0;
}

server {
    listen      80;
    server_name www.domaine.tld;

    access_log  /var/log/nginx/www.domaine.tld.access.log;
    error_log   /var/log/nginx/www.domaine.tld.error.log;

    location /media {
        root    /opt/django/www.domaine.tld/lib/python2.5/site-packages/django/contrib/admin/;
      expires       30d;
    }

    root            /opt/django/www.domaine.tld/monprojet/media;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;       

        if (!-f $request_filename) {
            proxy_pass http://www_domaine_tld;
            break;
        }
    }
}

Et voili.

samedi, avril 3 2010

Faire tourner des applications Ruby on Rails avec Nginx et Unicorn

Mise à jour : j'ai finalement décidé d'utiliser supervisor pour gérer/monitorer ce genre de services, voyez ce billet.

Afin d'optimiser mon petit serveur virtuel chez Gandi qui, à force de lui ajouter des fonctions, des sites web, des utilisateurs avait de petits problèmes de charge au point de temps en temps de ne plus répondre, je migre du serveur web Apache vers Nginx. Étant donné que je fais tourner de tout et de rien sur Apache : du php, des cgi, du Ruby on Rails et du Django, l'opération s'avère un poil complexe, et je vais essayer de présenter petit à petit les solutions retenues pour servir chacune de ces technologies avec Nginx, en commençant par les applications Ruby on Rails.

Unicorn

Avec Apache, pour faire tourner mon application Ruby on Rails (Redmine) j'utilisais Passenger/mod_rails, parce qu'après moults recherches c'est la solution que j'avais trouvé. Le passage à Nginx m'a permis de me pencher à nouveau sur ce problème et j'ai décidé de me tourner vers Unicorn[1].

Pour installer unicorn le plus simple est de passer par l'outil de gestionnaire de packages Ruby gem :

# gem install unicorn

Afin de servir vos projets Rails, il faut lancer un processus Unicorn par projet, en spécifiant les options nécessaires. Le plus simple pour spécifier ces options est de créer un fichier de configuration pour Unicorn directement dans chaque projet Rails. Ce fichier doit être nommé unicorn.rb et situé dans le répertoire config du RAILS_ROOT c'est-à-dire du répertoire racine de votre projet.

Voici un exemple de fichier sur lequel vous pouvez vous baser pour créer votre propre configuration.

# configuration file for Unicorn (not Rack)
#
# See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete
# documentation.

# Use at least one worker per core if you're on a dedicated server,
# more will usually help for _short_ waits on databases/caches.
worker_processes 2

# Help ensure your application will always spawn in the symlinked
# "current" directory that Capistrano sets up.
working_directory "/opt/redmine" # available in 0.94.0+

# listen on both a Unix domain socket and a TCP port,
# we use a shorter backlog for quicker failover when busy
listen "/var/run/unicorn/redmine.sock", :backlog => 64
#listen 8080, :tcp_nopush => true

# nuke workers after 30 seconds instead of 60 seconds (the default)
timeout 30

# feel free to point this anywhere accessible on the filesystem
pid "/var/run/unicorn/redmine.pid"

# some applications/frameworks log to stderr or stdout, so prevent
# them from going to /dev/null when daemonized here:
stderr_path "/opt/redmine/log/unicorn.stderr.log"
stdout_path "/opt/redmine/log/unicorn.stdout.log"

# combine REE with "preload_app true" for memory savings
# http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
preload_app true
GC.respond_to?(:copy_on_write_friendly=) and
  GC.copy_on_write_friendly = true

before_fork do |server, worker|
  # the following is highly recomended for Rails + "preload_app true"
  # as there's no need for the master process to hold a connection
  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.connection.disconnect!

  # The following is only recommended for memory/DB-constrained
  # installations.  It is not needed if your system can house
  # twice as many worker_processes as you have configured.
  #
  # # This allows a new master process to incrementally
  # # phase out the old master process with SIGTTOU to avoid a
  # # thundering herd (especially in the "preload_app false" case)
  # # when doing a transparent upgrade.  The last worker spawned
  # # will then kill off the old master process with a SIGQUIT.
  # old_pid = "#{server.config[:pid]}.oldbin"
  # if old_pid != server.pid
  #   begin
  #     sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
  #     Process.kill(sig, File.read(old_pid).to_i)
  #   rescue Errno::ENOENT, Errno::ESRCH
  #   end
  # end
  #
  # # *optionally* throttle the master from forking too quickly by sleeping
  # sleep 1
end

after_fork do |server, worker|
  # per-process listener ports for debugging/admin/migrations
  # addr = "127.0.0.1:#{9293 + worker.nr}"
  # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)

  # the following is *required* for Rails + "preload_app true",
  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.establish_connection

  # if preload_app is true, then you may also want to check and
  # restart any other shared sockets/descriptors such as Memcached,
  # and Redis.  TokyoCabinet file handles are safe to reuse
  # between any number of forked children (assuming your kernel
  # correctly implements pread()/pwrite() system calls)
end

Éditez les premières variables pour correspondre à votre configuration : nombre de processus au démarrage, répertoire de travail, écoute sur un socket ou un port TCP/IP, emplacement du fichier pid et des logs de capture de stdout et stderr.

Vous pouvez alors vous mettre dans le répertoire en question et lancer Unicorn comme suit :

/opt/redmine$ unicorn_rails  -c config/unicorn.rb -E production -D

Je n'ai malheureusement pas trouvé comment spécifier l'environnement à utiliser (le RAILS_ENV, ici production) autrement que dans la ligne de commande : l'indiquer dans le fichier de configuration ne marche pas... mais le premier qui trouve/sais merci de laisser un commentaire !

Tout ça c'est bien sympa mais ce n'est pas pratique de devoir lancer à la main chacun des processus unicorn nécessaire ni de devoir les relancer à la main si le serveur redémarre.

Pour pallier à ce problème, il faut créer un script de démarrage de Unicorn. Vu que ma Debian utilise un init à la Système V j'ai préféré créer un script pour ce système plutôt qu'utiliser un autre outil pour cela. Celui que j'utilise est adapté de ce site. Il suffit de vérifier que la partie des variables (notamment le RAILS_ENV, USER, DAEMON) correspond à votre configuration.

#!/bin/sh 

### BEGIN INIT INFO
# Provides:       unicorn
# Required-Start: $local_fs $syslog
# Required-Stop:  $local_fs $syslog
# Default-Start:  2 3 4 5
# Default-Stop:   0 1 6
# Short-Description: Unicorn processes
### END INIT INFO

RAILS_ENV="production"
USER=www-data
DAEMON="/var/lib/gems/1.8/bin/unicorn_rails"

NAME="unicorn"
CONFDIR=/etc/unicorn/sites
PIDDIR=/var/run/unicorn
CONFFILE=config/unicorn.rb
RETVAL=0

OPTIONS="-c $CONFFILE -D -E $RAILS_ENV"
# source function library
. /lib/lsb/init-functions

# pull in default settings
[ -f /etc/default/unicorn ] && . /etc/default/unicorn

test -x $DAEMON || exit 0

start()
{
    echo $"Starting $NAME."
    cd $CONFDIR;
    for d in *; do
        echo -n $d;
        cd $d;
        PIDFILE=$PIDDIR/$d.pid
        [ -f $PIDFILE ] && echo ": already started!"
        [ ! -f $PIDFILE ] && su -c "$DAEMON $OPTIONS" $USER && echo ": OK";
    done
    echo "done"
}

stop()
{
    echo $"Stopping $NAME:"
    cd $CONFDIR
    for d in *; do
        echo -n $d;
        if [ -f $PIDDIR/$d.pid ]
        then kill -QUIT `cat $PIDDIR/$d.pid` && echo ": OK" || echo ": failed";
        fi
    done
    echo "done"
}

reload()
{
    echo $"Reloading $NAME:"
    cd $CONFDIR
    for d in *; do
        echo -n $d;
        if [ -f $PIDDIR/$d.pid ]
        then kill -USR2 `cat $PIDDIR/$d.pid` && echo ": OK" || echo ": failed";
        fi
    done
    echo "done"
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        reload
        ;;
    reload)
        reload
        ;;
    force-reload)
        stop && start
        ;;
    *)
        echo $"Usage: $0 {start|stop|restart}"
        RETVAL=1Unicorn se lance en utilisant des sockets mais peut également utiliser TCP/IP.
esac
exit $RETVAL

Ce script est orienté Debian, et devra probablement être adapté pour d'autres distributions.

Pour le faire tourner il vous faut :

  • un répertoire /etc/unicorn/sites dans lequel vous mettez des liens symboliques vers vos projets Rails (le RAILS_ROOT plus exactement de chacun)
  • dans chacun des RAILS_ROOT de vos projets un fichier config/unicorn.rb comme indiqué au-dessus

Nginx

Ensuite pour utiliser Unicorn depuis Nginx, il vous suffit d'avoir une déclaration de virtualhost du type :


upstream redmine_domain_tld {
    server      unix:/var/run/unicorn/redmine.sock fail_timeout=0;
}

server {
    listen      80;
    server_name redmine.domain.tld;

    access_log  /var/log/nginx/redmine.domain.tld.access.log;
    error_log   /var/log/nginx/redmine.domain.tld.error.log;

    location /images {
        root    /opt/redmine/public/images;
    }

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;       

        if (!-f $request_filename) {
            proxy_pass http://redmine_domain_tld;
            break;
        }
    }
}

Et voilà. La suite de ma migration de Apache vers Nginx dans un prochain épisode.

Notes

[1] merci à Olivier Meunier de m'avoir rappeler l'existence de ce soft... auquel j'aurais dû penser tout de suite en connaissant gunicorn