19 de Abril, 2012

Aplicaciones Django con nginx

Cuando trabajaba con Cherokee (que ya no, pero eso es otra historia), ponía en producción mis experimentos usando FastCGI con Django. No está mal, es una solución rápida (solo hay que instalar flup), fácil de configurar para que funcione, y a otra cosa.

Se puede ejecutar en una linea, además con un usuario sin privilegios, usando algo como:

su -c "python $PROJ_DIR/manage.py \
    runfcgi method=prefork maxchildren=10 minspare=2 maxspare=6 host=127.0.0.1 \
    port=8001 pidfile=$PROJ_DIR/run/django_fcgi.pid \
    outlog=$PROJ_DIR/log/out.log errlog=$PROJ_DIR/log/err.log daemonize=yes" $USERNAME

Así he tenido funcionando todas mis aplicaciones hechas con Django, aunque tiene algunas pegas.

Arrancar el servicio

Con Cherokee casi está resuelto: lo hace por nosotros (más o menos podemos usar la linea anterior como orden para ejecutar, sin las variables), aunque nos es óptimo porque no podemos implementar adecuadamente separación de privilegios (ejecutar el servidor web como root es malo, pero correr servidor y aplicaciones bajo el mismo usuario tampoco me convence).

Con nginx no es posible ni siquiera eso, y no hay cosa más ingrata que escribir nuestro propio script para init.d ;).

Al final he llegado incluso a usar con prisas un @reboot en el crontab del usuario ejecutando la aplicación.

Monitorización

En el caso de Cherokee está razonablemente cubierto, porque si el proceso no está disponible, lo reiniciará; pero la separación de privilegios me parece importante y ya he comentado que no es posible. Tuve mis experimentos con suidexec, pero no es la situación ideal.

Algún watchdog he hecho con BASH, en plan comprobar si el proceso sigue ahí; pero es una solución un poco en la linea del hack del crontab.

Rendimiento

Lo peor configurar FastCGI es que recuerda mucho a Apache: ¿qué valores empleamos para maxchildren, mispare y maxspare? Si no tienes mucho tráfico dará un poco igual, pero esos ajustes sumados a la carga que añade el protocolo usado por FastCGI (que a fin de cuentas lleva dando vueltas desde 1996), empieza a ser un engorro cuando corres varias aplicaciones en el mismo servidor. Al final no es una mala opción si tenemos recursos abundantes (no es el caso) o no podemos usar algo mejor.

Experimento: nginx + gunicorn + supervisor

Tenía claro que la solución no iba a venir necesariamente de un solo proyecto, así que aprovechando que estoy migrando de OVH a Memset (esto también es historia para otro día), he buscado hacer las cosas algo más ordenadas que hasta ahora.

Me he decidido por lo siguiente:

  • nginx: bueno, esto lo tenía claro. Me permite hacer todo lo que necesito, y es excelente como proxy HTTP (soportando sockets UNIX, genial para montajes locales) y sirviendo contenido estático.
  • Gunicorn: es un servidor ligero en recursos y bastante rápido, con soporte WSGI y específico para algunos frameworks (como Django). Es fácil de configurar y, aunque tiene la pega de que hay que indicar el número de workers, aporta mucho más rendimiento que FastCGI (con 2 workers es suficiente para aplicaciones con cierto tráfico).
  • Supervisor: permite lanzar las aplicaciones, con separación de privilegios y monitorización. Hay otros proyectos que pueden ofrecernos lo mismo. Este está desarollado en Python y parece potente (me he quedado en la superficie), aunque resulta muy sencillo de configurar y utilizar.

La configuración depende un poco de cómo quieras desplegar las aplicaciones, por ejemplo: usando un subdominio.

Empezamos con nginx. Trabajando con Debian, ponemos la configuaración en /etc/nginx/sites-available/app.usebox.net para luego hacer un enlace simbólico a /etc/nginx/sites-enabled/:

upstream app {
        server unix:/tmp/django-app.sock fail_timeout=0;
}

server {
	listen   80;

	server_name  app.usebox.net;
	access_log  /var/log/nginx/app.usebox.net.access.log;

	location /static {
		alias   /home/app/project/static;
	}

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

                proxy_pass http://app;
	}
}

Para configurar gunicorn solo hay que crear un fichero Python, por ejemplo en la raíz de nuestro proyecto como gunicorn.py:

bind = "unix:/tmp/django-app.sock"  # el socket que hemos indicado en nginx
workers = 2  # se recomienda empezar con cores+1, y luego ir ajustando
django_settings = 'project.settings'  # el projecto Django se llama project
pythonpath = '/home/app/'  # directorio superior a donde instalamos la app

Finalmente escribimos la configuración del proceso en supervisor, que en Debian va en /etc/supervisor/conf.d/, por ejemplo app.conf:

[program:app]
command=/usr/bin/gunicorn_django -c /home/app/project/gunicorn.py
directory=/home/app/project/
user=app
autostart=true
autorestart=true
redirect_stderr=true

En Debian el supervisor se ejecuta como root al arranque del sistema, con lo que puede ejecutar los procesos con el usuario que indiquemos (app en este ejemplo).

Solo hay que recargar la configuración de nginx y ejecutar algo como un supervisorctl reload.

Con supervisorctl podemos ejecutar operaciones sobre la aplicación, como por ejemplo:

# supervisorctl status app
app                     RUNNING    pid 11682, uptime 1:42:01

Están disponibles las típicas órdenes para gestionar los procesos (stop, start, restart, etc), y en /var/log/supervisor/ tenemos logs que pueden ser bastante interesantes (sobretodo si tenemos algún problema configurando el invento, porque captura la salida de gunicorn).

He llamado la propuesta experimento porque estoy en pruebas, aunque si no hay problemas, parece una solución definitiva y mucho más mantenible y ordenada que lo que venía haciendo hasta ahora ;).

Anotación por Juan J. Martínez, clasificada en: python, django.

Hay 4 comentarios

Gravatar

Interesante, llevo tiempo queriendo probar nginx. En el curro tenemos una aplicación (MapProxy) desplegada con WSGI en gunicorn (con apache delante de proxy inverso) y funciona de lujo sin pedir demasiado pan.

Eso sí, me tuve que hacer un init-script, lo tuyo parece que mola más :-)

por Jorge, en 2012-04-20 22:24:03

Gravatar

Aquí también usamos gunicorn+supervisord+nginx, después de haber probado otras configuraciones. Cumple de sobras.

El único “problema” es al reiniciar, si tienes muchas apps. Es mejor reiniciarlas una por una, así el downtime es escalonado (y mucho menor).

Saludos,

por DZPM, en 2012-04-23 10:47:33

Gravatar

Puedes reiniciar cualquier aplicación sin reiniciarlas todas… supervisorctl restart app. No se trata de reiniciar supervisor ;)

Por cierto, no me había dado cuenta que supervisor tiene un API vía XML-RPC:

http://supervisord.org/api.html

En teoría se puede “enganchar” a un interfaz web cualquiera. Sexy ;)

por Juanjo, en 2012-04-23 11:17:42

Gravatar

Sí, eso quería decir: que las reinicio una por una, en vez de reiniciar supervisord.

En vez de API uso Fabric, y así puedo hacer más cosillas de manera fácil.

Saludos,

por DZPM, en 2012-04-23 14:49:35

Los comentarios están cerrados: los comentarios se cierran automáticamente una vez pasados 30 días. Si quieres comentar algo acerca de la anotación, puedes hacerlo por e-mail.

Algunas anotaciones relacionadas: