01/03/2010

nginx et python – le perfect setup

Python est devenu assez à la mode, pour le développement web, ces dernières années avec l’arrivée de frameworks web comme Django, Pylons et web.py. On le voit souvent utilisé sur apache2, avec mod_python, ou sur des serveurs Python dediés à ça (CherryPy, etc)

Je cherche depuis longtemps un moyen de faire du Python sur HTTP. J’ai mis pas mal de temps mais je pense avoir trouvé une configuration sympa.

Je vous propose ici d’essayer nginx, un serveur HTTP (entre autres), très performant et  beaucoup plus léger/rapide que apache2. Il est très efficace pour faire du reverse-proxy (c’est à dire être utilisé en frontal/load-balancer devant un serveur HTTP, et lui transmettre les requêtes en faisant du cache et le cas échéant du load-balancing).

La configuration de nginx est très simple, et la documentation est très complète : wiki.nginx.org. L’installation de nginx est facile, il est inclus dans la plupart des distributions actuelles, donc à vos emerge/apt-get.

apt-get install nginx

nginx ne gère pas le python nativement. Il y a plusieurs solutions cependant :

  • Faire du proxy vers un serveur qui gère le python, comme apache2. On perd la majeure partie de l’intêret de nginx, cependant.
  • Faire du FastCGI vers un serveur FastCGI. C’est la solution la plus fréquente, en utilisant fcgi.py ou flup
  • Faire du WSGI. C’est un protocole de communication entre serveurs HTTP et applications Python. Solution aussi assez fréquente.
  • Faire du proxy vers un serveur qui gère le python de manière native. C’est une solution moins fréquente mais très pratique.

Avant même de penser à choisir une des solutions ici présentes, il faut les comparer entre elles.


nginx => apache2 => python

L’ennui de cette solution c’est : apache2. Utiliser nginx pour forwarder à apache2 est presque inutile. Presque ? Oui, car on peut dire à nginx de garder les fichiers statiques pour lui. Cependant, le gain de cette méthode est assez minime, et ce n’est intéressant que pour faire du load balancing sur plusieurs apaches.


le FastCGI

FastCGI c’est une norme, pour faire traiter du contenu dynamique par une application externe. Donc le support du FastCGI tout seul est inutile, il faut aussi quelque chose pour gérer derrière.

J’ai testé flup, un framework Python léger. Le résultat : ça marche, méééé c’est lent. Je tournais autour de 35/50 requêtes par seconde, et l’utilisation en mémoire vive était phénoménale, 150 mégas pour 1000 requêtes. (bien sur ces chiffres n’auront de valeur que lorsqu’ils seront comparés aux autres).

FastCGI est sympa pour faire une configuration “vite”. C’est d’ailleurs la seule solution viable, à ma connaissance pour faire du PHP sur nginx, à part le proxy vers un autre serveur HTTP.

Bref, FastCGI ne m’a pas convaincu. Et mes tests avec les autres solutions m’ont donné raison. Toujours avec moi ? On passe au WSGI !


le WSGI

WSGI is the Web Server Gateway Interface. It is a specification for web servers and application servers to communicate with web applications (though it can also be used for more than that). It is a Python standard, described in detail in PEP 333.

le WSGI, c’est le standard pour le Web Python. Il n’y a qu’à consulter wsgi.org pour se rendre compte que c’est tout un monde. Il consiste en un ensemble de normes (nom de variables, par exemple) pour permettre au serveur d’exécuter  un fichier codé en Python.

Le standard pour le WSGI sur les serveurs HTTP grand public, c’est le mod_wsgi de apache2. Il a été porté sur nginx, mais c ‘est une #@!## infame, qui ne marche que sur les vieilles versions et qui n’a pas été mise à jour depuis bientôt 2 ans et demi (cf. hg.mperillo.ath.cx/nginx/mod_wsgi/). J’ai obtenu des performances d’environ 133 requêtes par seconde avec, ce qui est déjà mieux que FastCGI, pour ceux qui suivent :). Par contre, dès que j’ai voulu avancer un peu avec (toucher à la configuration, et surtout, passer à une version supérieure de nginx), j’ai été confronté à ses limites (et aux #@!## de faute de frappe dans le code source)

D’autant plus que l’auteur de mod_wsgi pour apache2 et l’auteur de mod_wsgi pour nginx reconnaissent ses limitations, respectivement ici et ici. Le premier document est très intéressant pour comprendre un peu l’architecture de nginx et ses différences avec apache2. En effet, le mod_wsgi pour nginx a été codé à partir de celui pour apache2, et il subit les différences d’architecture entre apache2 et nginx.

Attention, je ne remet absolument pas à cause le WSGI ici, juste l’implémentation de mod_wsgi dans nginx.

Parce que justement, un projet super récent, c’est…. uWSGI ! uWSGI sert à combler le manque de support du WSGI dans nginx, en rajoutant un module, meilleur que mod_wsgi. Il nécessite donc de recompiler nginx. Il y a encore une autre solution, nommée gunicorn, qui ne nécessite pas de recompiler nginx. C’est un serveur HTTP minimal qui gère le WSGI nativement. uwsgi offre plus de fonctionalitées que gunicorn, mais gunicorn a l’air assez dynamique (dans le développement), et est plus facile à utiliser (pas de recompilation, support de Django natif). Au niveau des performances, je me suis amusé à faire cette comparaison :

 

Comparaison des performances entre uwsgi et gunicorn en fonction du niveau de parallélisme

Comparaison des performances entre uwsgi et gunicorn en fonction du niveau de parallélisme

 

On constate plusieurs choses :

  • Les performances augmentent en utilisant 2 workers à la place d’un, mais au delà, le gain est ridicule, puis inexistant. Le nombre 2 est dépendant du nombre de coeurs de la machine, et le nombre de workers à utiliser ne sera pas le même sous un bi-socket octocore, bien évidemment. Les meilleures performances étaient obtenues en utilisant gunicorn+3workers.
  • La différence est vraiment minime, gunicorn est en moyenne légèrement au dessus de uwsgi.
  • Ces tests sont réalisés sur une application Django. Les performances augmentent en utilisant des frameworks plus légers comme web.py, bien évidemment.

uwsgi dispose de plus de fonctionalitées, comme par exemple une interface d’administration de serveur wsgi dans Django. Je vais ici expliquer l’installation et la configuration des deux solutions.

Ici je vais couvrir l’installation et la configuration de uwsgi et gunicorn avec Django et web.py, même si c’est bien sur adaptable avec d’autres frameworks supportant le wsgi (liste sur wsgi.org/wsgi/Frameworks).

Installation et configuration de uwsgi


Sous Debian :

1) Rajouter les dépots source, par exemple :

deb-src http://ftp.gr.debian.org/debian/ squeeze main contrib non-free

2) Télécharger la source

apt-get source nginx
tar -zxvf nginx_*.orig.tar.gz
cd nginx-*
tar -zxvf nginx_*.debian.tar.gz

2) Télécharger uwsgi

cd ..

wget http://projects.unbit.it/downloads/uwsgi-0.9.4.2.tar.gz

tar -zxvf uwsgi-0.9.4.2.tar.gz

3) Modifier le paquet source de nginx pour y ajouter uwsgi. Il faut éditer le fichier nginx-*/debian/rules, trouver le bloc qui commence par ./configure et ou se trouvent toutes les options de compilation, et y ajouter

--add-module=$(CURDIR)/../uwsgi-0.9.4.2/nginx/

$(CURDIR)/../uwsgi-0.9.4.2/nginx/ est le chemin vers le dossier du module dans la source de uwsgi

4) maintenant, dans le dossier de la source de nginx, faites (pas besoin de root) :

dpkg-buildpackage

5) installer le .deb généré ainsi (en tant que root maintenant)

dpkg -i ../nginx-*.deb

6) Dernière étape : compiler uWSGI (le binaire en lui-même, qui va communiquer avec nginx par le biais du module)

cd ../uwsgi-0.9.4.2/ && make && make install && cp uwsgi_params /etc/nginx/

Cette dernière commande (le cp uwsgi_params…) sert à placer un fichier de configuration du module uwsgi exemple dans le repertoire de nginx.

Sous les autres distributions :

Il faut télécharger la source de nginx et de uwsgi, compiler nginx avec le module uwsgi (dans le répertoire nginx de la source de uwsgi), puis compiler uwsgi.

On a maintenant un serveur uWSGI et un nginx avec  le module uwsgi. Il va maintenant falloir dire à nginx de transférer les requêtes au serveur uWSGI, avec une commande uwsgi_pass. Par exemple, dans la section server d’un de vos vhost (/etc/nginx/sites-enabled/default, par exemple, sous Debian) :

location / {
include uwsgi_params;
uwsgi_pass unix:///tmp/uwsgi.sock;
}

Il faut ensuite créer une application WSGI, par exemple, pour le framework web.py :

import web

urls = (
    '/.*', 'hello',
    )

class hello:
    def GET(self):
        return "Hello, world."

application = web.application(urls, globals()).wsgifunc()
applications = {'/': application }

Pour Django :

import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler()
applications = {'/': application }

Nommez ce fichier wsgi.py et placez le dans le repertoire ou vous allez placer votre code (pour Django, dans le repertoire du projet, évidemment). Si vos nerfs n’ont pas encore lâché ici, allez prendre une petite pause de 5 minutes à titre préventif, et reprenez. Maintenant, on va démarrer un serveur uWSGI. Placez vous dans le répertoire de votre fichier wsgi.py, et faites :

uwsgi -s /tmp/uwsgi.sock -C -w wsgi -p2 -L

Explications : le -s indique ou placer le socket (ça pourrait aussi être une adresse au format hôte:port, pour un usage distant). Le -w wsgi indique de charger le fichier wsgi(.py), le -p2 indique de démarrer 2 processus, et le -L désactive l’envoi des requêtes à stdout (=>flood).

Si vous allez à l’adresse de votre serveur web, vous devriez voir votre application Python en face de vous ! Vous voilà en train d’utiliser nginx+uwsgi+python.

En annexe, je rappelle que uwsgi est aussi utilisable avec apache et lighttpd.

Aussi, dans le dossier source de uwsgi, vous trouverez des templates pour un module d’administration de Django, à installer dans le repertoire des templates de l’administration de Django (si, si).

Installation et configuration de gunicorn

gunicorn est carrèment plus facile à installer. Il est codé en python, donc pas de compilation, seulement un easy_install :

easy_install gunicorn

La configuration est tout aussi facile, il suffit de cela dans la configuration du vhost dans nginx :

location / {
proxy_pass http://unix:/tmp/gunicorn.sock
}

On démarre ensuite gunicorn. Pour Django, gunicorn dispose d’un support intégré, il vous suffit de vous placer dans le répertoire de votre projet, puis :

gunicorn_django -b unix:/tmp/gunicorn.sock --workers=2

Pour web.py, il faut créer un module :

import web

urls = (
    '/.*', 'hello',
    )

class hello:
    def GET(self):
        return "Hello, world."

application = web.application(urls, globals()).wsgifunc()

Il faut ensuite démarrer le serveur Gunicorn. En étant dans le repertoire ou se trouve le module web.py (le fichier donc le code vient avant, là, au-dessus, oui), :

gunicorn -b unix:/tmp/gunicorn.sock --workers=2 wsgi

Je vous invite à consulter ce lien : gunicorn.org/tuning.html, pour vous permettre d’augmenter les performances de gunicorn.

Annexe

Fichiers statiques

location /admin-media {alias /usr/lib/pymodules/python2.5/django/contrib/admin/media;}

location /static {root /var/www/byteflow;}

Ces deux lignes, à rajouter après le “location /” avec uwsgi_pass ou proxy_pass (selon votre configuration) indiquent à nginx de traiter les fichiers statiques directement. Il faut bien évidemment adapter à votre configuration.

Munin

Vous pouvez trouver des plugins nginx pour munin ici. Je n’ai pas trouvé de plugin munin pour uwsgi/gunicorn, mais je pourrais peut-être en faire un si je trouve le temps.

Plusieurs serveurs

Si par hasard vous avez plusieurs serveurs gunicorn/uwsgi répartis sur plusieurs machines , vous pouvez les utiliser en déclarant une section

<span style="font-weight: normal;">upstream <em>nom</em> {} </span>

dans nginx, contenant les adresses des serveurs distant (hôte:port ou emplacement du socket) et en utilisant “nom” comme argument à uwsgi_pass et proxy_pass.

Conclusion

Ces deux architectures sont très pratiques pour faire tourner du python dans un serveur web, ici nginx. uwsgi dispose de plus de fonctionalités, mais la plupart (template d’admin Django, memory profiling) pourraient facilement être ajoutées à gunicorn, qui est beaucoup plus léger à installer/configurer/maintenir (2 commandes et 3 lignes de configuration dans nginx). J’ai les deux personellement sur mon serveur, j’ai eu à alterner entre les deux pour écrire cet article, et c’est pas bien dur, un daemon à arrêter, un autre à démarrer, et changer une ligne dans la configuration de nginx.

Liens

wsgi.org

gunicorn.org/

projects.unbit.it/uwsgi/

irc.freenode.org/#gunicorn

nginx.org/

djangoadvent.com/

www.django-fr.org/

www.djangoproject.com/

webpy.org/

Je tiens à remercier la communauté Geek{node|fault}, le chan irc #gunicorn, les développeurs de gunicorn qui sont très aimables, ainsi que Amandarn qui m’a aidé pour certains détails.

  1. | #1

    Merci beaucoup pour ce super tuto qui m’a permis d’installer nginx+gunicorn très rapidement !

    Juste une petite erreur :
    location /static {root /var/www/byteflow;} devrait être alias et non root.

  2. | #2

    @David, biologeek
    David, je ne vois pas pourquoi ça devrait être alias, en fait. En effet la différence de root par rapport à alias, c’est que (pour location / root va envoyer les requêtes vers site.tld/url à root+url, et que alias va envoyer les requêtes vers site.tld/url à alias. Donc si tu utilises alias, les requêtes vers /static seront envoyées dans /var/www/byteflow, et non pas dans /var/www/byteflow/static, ce qui devrait être le bon comportement.

    Content d’avoir pu t’aider, David

    pistache,

  3. | #3

    Quelle méthodologie pour les tests de perf ?

    Article intéressant sinon, merci !

  4. | #4

    @Jocelyn Delalande

    J’ai utilisé ab (Apache Benchmark), car même si c’est un “mauvais” benchmark (pas représenatif des conditions d’utilisations normales), c’est un bon moyen de *comparer* les performances de deux systèmes assez proches.

  5. Sébastien
    | #5

    Merci pour cet article !

    🙂

  6. | #6

    z67i4b

  1. | #1
  2. | #2