Deploy de Django con Circus, Chaussette y Nginx

Aunque hay un pequeño mito al respecto, poner en producción una aplicación web hecha en Python no es tan complejo. Esa facilidad se debe a la estandarización de la pasarela WSGI, que define cómo se comunica (o se debería comunicar) una "app" (sea hecha con un framework o no) con un servidor web.

Si bien Nginx, el servidor web que está desplazando a Apache como el más popular tiene un módulo que implementa el estándar WSGI de manera nativa, la arquitectura más típica es utilizarlo como proxy reverso, conectado a un servidor WSGI especializado como Gunicorn que interactua con la aplicación web (posiblemente a través de multiples instancias o workers). Como queremos que nuestra app funcione permanentemente, el proceso WSGI y otros que sean necesarios (por ejemplo Redis) se demonizan de manera que sepan restaurarse automáticamente si mueren y sea posible monitorearlos: para eso suele usarse supervisor.

https://raw.githubusercontent.com/mozilla-services/circus/dff6cf3a348fecc0b58bd08cae91b1508aed14c2/docs/source/classical-stack.png

La arquitectura de deployment más común para una aplicación web Python

Si bien esta arquitectura está bastamente probada, hay una opción mejor.

https://circus.readthedocs.org/en/0.11.1/_images/circus-medium.png

El circo y el soquete

Circus y Chaussette son proyectos desarrollados por Tarek Ziadé y el equipo de Mozilla Sevices.

Una arquitectura de producción análoga a la descripta arriba, pero basada en Circus, se ve así:

https://raw.githubusercontent.com/mozilla-services/circus/dff6cf3a348fecc0b58bd08cae91b1508aed14c2/docs/source/circus-stack.png

Circus maneja procesos demonizados igual que Supervisor, pero además puede bindear sockets y manejarlos de la misma manera. Este desacople de la gestión de sockets del webserver WSGI permite muchas posibilidades, tanto en la gestión como en la escalabilidad de la arquitectura.

La capa WSGI en este esquema la aporta Chaussette, que tiene la particularidad que, en vez de abrir un socket nuevo, utiliza el que Circus abrió (y controla) para el worker. Además, aunque trae una implementación de WSGI built-in, puede usar muchos backends más eficientes o con características particulares como meinheld, gevent, gevent-socketio, entre muchos otros.

A diferencia de Supervisor que se basa en el protocolo XML-RPC para inspeccionar los procesos que controla, Circus utiliza un canal pub/sub basado en el mucho más moderno ZeroMQ (lo mismo que usa IPython Notebook) que permite un monitoreo realtime y una API de plugins mucho más simple. Otra diferencia, nada menor, es que Circus funciona con Python 2 o 3 (supervisor no es compatible con Python 3).

Y de yapa: Circus se puede usar como una biblioteca de muy alto nivel para la gestión no bloqueante de procesos. Se puede pensar con un wrapper de subprocess y/o multiprocess, que aporta información de monitoreo y estadísticas, control de flujo, una capa de señales (hooks) muy completa y más.

Desplegando Django

Para ejemplificar, voy utilizar un proyecto Django que estoy desarrollando (muy lentamente): nikolahub.

Circus se configura con un archivo con formato .ini. El mio, que bauticé circus.ini quedó así:

[circus]
check_delay = 5
endpoint = tcp://127.0.0.1:5555
pubsub_endpoint = tcp://127.0.0.1:5556
stats_endpoint = tcp://127.0.0.1:5557

[socket:nikolahub]
host = 127.0.0.1
port = 8080

[watcher:nikolahub]
cmd = /virtualenvs/nikolahub/bin/chaussette --fd $(circus.sockets.nikolahub) nikolahub.wsgi.application
use_sockets = True
numprocesses = 3

[env:nikolahub]
PYTHONPATH = /projects/nikolahub

La sección watcher indica lanza el comando a controlar, en este caso levantando 3 workers de la aplicación -django. Notar que como tengo instalado Chaussette dentro del virtualenv, uso el path absoluto al ejecutable. El fragmento --fd $(circus.sockets.nikolahub) se expande implícitamente asignando el pid que obtuvo el fork (el proceso hijo) de circus.

Si quisieramos usar otro servidor web, sólo hay que indicar cual con el parámetro --backend Por ejemplo:

cmd = /virtualenvs/nikolahub/bin/chaussette --backend gevent --fd $(circus.sockets.nikolahub) nikolahub.wsgi.application

Podemos probar si todo funciona:

(nikolahub)tin@morochita:$ circusd circus.ini
2014-06-12 04:36:16 circus[1141] [INFO] Starting master on pid 1141
2014-06-12 04:36:16 circus[1141] [INFO] sockets started
2014-06-12 04:36:16 circus[1141] [INFO] Arbiter now waiting for commands
2014-06-12 04:36:16 circus[1141] [INFO] nikolahub started
2014-06-12 04:36:16 circus[1141] [INFO] circusd-stats started
2014-06-12 04:36:17 circus[1150] [INFO] Starting the stats streamer
2014-06-12 04:36:17 [1149] [INFO] Application is <django.core.handlers.wsgi.WSGIHandler object at 0xa06f60c>
2014-06-12 04:36:17 [1149] [INFO] Serving on fd://5
2014-06-12 04:36:17 [1149] [INFO] Using <class chaussette.backend._wsgiref.ChaussetteServer at 0x9f2d6ec> as a backend
2014-06-12 04:36:17 [1148] [INFO] Application is <django.core.handlers.wsgi.WSGIHandler object at 0x939b60c>
2014-06-12 04:36:17 [1148] [INFO] Serving on fd://5
2014-06-12 04:36:17 [1148] [INFO] Using <class chaussette.backend._wsgiref.ChaussetteServer at 0x92596ec> as a backend

Tendremos la aplicación servida en el puerto 8080 de localhost. Demonizarlo es sólo un flag:

(nikolahub)tin@morochita:$ circud --daemon circus.ini

Para implementar nginx como proxy reverso armé un archivo nginx.conf:

server {
    listen 80;
    server_name nikolahub.nqnwebs.com;

    location /static/ {
            alias /projects/nikolahub/static/;
    }

    location /media/ {
        alias /projects/nikolahub/media/;
    }

    location / {
        proxy_pass http://localhost:8080/;
        proxy_pass_header Server;
        proxy_set_header Host $host;
        proxy_redirect off;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Scheme $scheme;
        proxy_connect_timeout 600;
        proxy_send_timeout 600;
        proxy_read_timeout 600;
    }
}

Luego agregamos el sitio:

$ ln -s /etc/nginx/sites-available/nikolahub nginx.conf
$ ln -s /etc/nginx/sites-enable/nikolahub nginx.conf
$ sudo service nginx restart
http://upload.wikimedia.org/wikipedia/commons/thumb/6/67/Reverse_proxy_h2g2bob.svg/400px-Reverse_proxy_h2g2bob.svg.png

Esto pone a nginx como "frontend" de la aplicación, sirviendo los directorios con contenido estático y pasando el resto de las peticiones al puerto 8080 que administra Circus.

Ya tenemos nuestro sitio en producción.

El dueño del circo y los monitos

De ahora en más, podremos usar las herramientas que provee Circus.

circusctl es el dueño del circo. Puede parar, reiniciar, cambiar la cantidad de workers, abrir una consola ipython para interactuar o inspeccionar y mucho mas. Se puede usar como subcomandos (circusctl <subcmd> <watcher>) o usar la consola interactiva.

Por ejemplo, si quisiera ver cuantos procesos workers tengo y agregar uno más, podría hacer así:

(nikolahub)tin@morochita:$ circusctl numprocesses nikolahub
3
(nikolahub)tin@morochita:$ circusctl incr nikolahub
ok
(nikolahub)tin@morochita:$ circusctl numprocesses nikolahub
4

Lo mismo y más se puede hacer desde una consola IPython.

(nikolahub)tin@morochita:$ circusctl ipython
Python 2.7.4 (default, Apr 19 2013, 18:32:33)
Type "copyright", "credits" or "license" for more information.

IPython 2.1.0 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: arbiter.numprocesses()
Out[1]: 4

circus-top es un monitor realtime, estilo top. Escucha las estadísticas que produce circusd-stats.

(nikolahub)tin@morochita:$ circus-top
/images/circus-top.png

circus-top en acción. Muestra los procesos watcher y los recursos que cosumen.

Todo esto puede verse y manejarse cómodamente a través de circus-web, un dashboard web que permite monitorear y administrar circus, con gráficos realtime y muy fácil de usar.

https://circus.readthedocs.org/en/0.11.1/_images/web-watchers.png

Desde las últimas versiones, circus-web se refactorizó para basarla en Tornado (originalmente usaba bottle) y hay que instalarlo aparte.

$ pip install circus-web

Conclusiones

Circus es una herramienta que simplifica el stack de deployment de una aplicación web WSGI. La API de alto nivel, una arquitectura mucho más moderna y simple y el aval de ser desarrollada (y usada exhaustivamente) por Mozilla, son avales poderosos para probarla.

Como escribió el Marcos Luc "la función ya debería empezar (...) Bueno nena, buena suerte, cada vez la red te teme más..."

Comentarios

Comments powered by Disqus