You are on page 1of 4

Python: Gevent y Django DESARROLLO

Gevent y Django

CHATEANDO SIN MESSENGER


Los bucles de eventos estn cambiando las reglas en el desarrollo de sistemas web, y Gevent nos ofrece toda su potencia de forma simple y directa POR JOS MARA RUZ
bash$ cd lm-chat bash$ source bin/activate bash$ bin/pip install U gevent django

l rendimiento nunca ha sido un punto fuerte de Python. Bueno, para ser sinceros, tampoco ha sido uno de sus objetivos. El lenguaje Python fue creado con el fin de facilitar y simplificar la creacin de scripts en un sistema operativo experimental llamado Amoeba. Para aumentar el rendimiento, siempre se poda reescribir el cdigo en C e invocarlo desde Python. Con la entrada de Python en el mundillo de los framework web rpidamente apareci la necesidad de contar con algn sistema que permitiese a una pgina web en Python escalar. La memoria nunca ha sido un problema en Python, por lo que el foco siempre ha estado en la capacidad de Python de responder a una gran cantidad de eventos o peticiones por segundo. No hay nada peor en el mundo que ser incapaz de responder a tus usuarios cuando se satura el sistema. La solucin tradicional a este problema siempre se ha basado en el uso del multiproceso o la multihebra. Por cada peticin que recibamos se genera un proceso o hebra que la atender y que se eliminar tan pronto como se termine de responder. Esta solucin suele ir bien pero requiere cantidades ingentes

de recursos (memoria y cpu) y se agota rpidamente. Tener miles de hebras o procesos saturar rpidamente nuestro sistema. Adems, no debemos olvidar que el sistema de hebras de Python est herido de muerte desde su creacin: el GIL obliga al programa a bloquear todas las hebras cuando una de ellas entra en el cdigo del intrprete de Python algo muy normal eliminando la posibilidad de aprovechar los varios ncleos con los que cuentan las cpus modernas No hay solucin a este problema? S, existe una solucin y adems se est poniendo de moda: emplear un bucle de eventos (Event Loop). Vamos a analizar cmo funciona un pequeo chat html que viene como ejemplo en el cdigo fuente de Gevent (Recurso 1), una de las libreras que implementan un bucle de eventos en Python.

Gevent hace uso de la librera C libevent, por lo que deberemos instalarla, as como el paquete de desarrollo (normalmente llamado libevent-dev), con nuestro sistema de paquetes (rpm, apt-get). El paquete gevent posee componentes en C, de modo que veremos cmo se compilan algunos ficheros durante el proceso de instalacin con pip. Una vez tenemos nuestros paquetes instalados, deberemos descargar el cdigo fuente del ejemplo de Gevent. Para ello vamos a descargar Gevent con hg (mercurial):
bash$ hg clone U https://bitbucket.org/denisU /gevent

El cdigo fuente que nos interesa se encuentra en el directorio examples/webchat y es una aplicacin django. Para arrancarla necesitaremos ejecutar:
bash$ cd gevent/examples/webchat bash$ python run.py

Webchat
Comencemos por instalar todo lo necesario para ejecutar el chat. Si tenemos instalado en nuestro sistema virtualenv, los pasos que debemos seguir son:
bash$ virtualenv U --no-site-packages U lm-chat

Se arrancar el servidor web en el puerto 8088, as que podremos acceder a l si ponemos en nuestro navegador http://127.0.0.1:8088; veremos entonces una pgina como la que aparece en la Figura 1. En la parte inferior se encuentra una caja de texto en la que ser posible escribir mensajes. Si ahora conectamos desde otro ordenador a la misma pgina podremos ver la magia de Gevent en funcionamiento. Cuando escribimos

WWW.LINUX- MAGAZINE.ES

Nmero 71

53

DESARROLLO Python: Gevent y Django

Figura 1: Escena nocturna con estrellas que caen.

un mensaje desde un ordenador aparece inmediatamente en el otro y viceversa. En teora podemos aadir a nuestro chat tantos ordenadores como queramos. Veamos cmo funciona este programa.

Monkey Patching
Comencemos por el fichero run.py (ver Listado 1) porque es ah donde comienza el trabajo de Gevent. A diferencia de otras libreras de bucle de eventos como Twisted, Gevent trata de no interferir con el programador. Para ello emplea una tcnica, muy famosa en el mundo de Ruby, denominada Monkey Patching. Consiste en sobreescribir mtodos, clases y funciones de las libreras base de Python, respetando su interfaz pero cambiando la forma en la que trabajan. De esta forma podemos beneficiarnos de

Gevent sin prcticamente tener que modificar nuestro cdigo fuente. Una vez hemos importado de la librera gevent el mdulo monkey, ejecutamos el mtodo patch_all(). Este mtodo reemplaza un gran nmero de llamadas importantes de la librera estndar de Python, por ejemplo: La funcin os.fork Funciones de thread Funciones de socket Funciones de ssl Funciones de httplib Funciones de select Las mayora de estas funciones bloquean el intrprete de Python mientras esperan algn tipo de respuesta. Gevent pasa a gestionar estas funciones mediante eventos, ejecutndolas y aadiendo un callback que se disparar cuando el evento de turno (por ejemplo, que ha llegado

informacin desde la red) se dispare, permitiendo mientras tanto la ejecucin de otro cdigo. Gevent no nos permite ejecutar ms cdigo a la vez, sino que se encarga de gestionar aquel cdigo que normalmente se bloquea esperando algn tipo de recurso. Como podemos ver en el Listado 1, Gevent tambin nos proporciona un servidor WSGI que podemos usar para ejecutar la aplicacin Django. Este servidor tambin funciona empleando el bucle de eventos de Gevent, lo que implica que no se bloquear. Y esto qu significa? Pues que, por ejemplo, podremos responder a cientos, o incluso miles, de peticiones por segundo sin que la cpu apenas se esfuerce y sin consumir una cantidad de memoria excesiva. El resto del cdigo en este fichero simplemente arranca la aplicacin Django dentro del servidor que nos provee Gevent. Ya tenemos arrancado el servidor pero, cmo hace uso de Gevent para implementar el chat?

La Sala de Chat
El objeto ChatRoom, ver Listado 2, es el encargado de gestionar nuestro chat. La pgina que se ve en la Figura 1 trabaja tal como se muestra en la Figura 2. En el navegador se ejecutan una serie de funciones javascript que hacen uso de la librera JQuery. Su misin es la de recoger el texto que escribamos en el campo de texto, enviarlo a nuestro servidor Gevent y mostrar las respuestas dentro de la pgina. Para poder mostrar los mensajes que otros escriben en la pgina, el cdigo javascript consultar nuestra pgina cada 500 milisegundos. Si tratramos de hacer esto con una aplicacin Django normal y corriente colapsaramos el ser-

Listado 1: El Fichero run.py


01 #!/usr/bin/python 02 from gevent import monkey; monkey.patch_all() 03 from gevent.wsgi import WSGIServer 04 import sys 05 import os 06 import traceback 07 from django.core.handlers.wsgi import WSGIHandler 08 from django.core.management import call_command 09 from django.core.signals import got_request_exception 10 11 sys.path.append(..) 12 os.environ[DJANGO_SETTINGS_MODULE] = webchat.settings 13 14 def exception_printer(sender, **kwargs): 15 traceback.print_exc() 16 17 got_request_exception.connect(exception_printer) 18 19 call_command(syncdb) 20 print Serving on 8088... 21 WSGIServer((, 8088), WSGIHandler()).serve_forever()

54

Nmero 71

WWW.LINUX- MAGAZINE.ES

Python: Gevent y Django DESARROLLO

vidor rpidamente. Pero gracias a Gevent podremos gestionar esta carga de trabajo sin demasiados problemas. Aunque ChatRoom slo tiene dos mtodos, su cdigo est tan compactado que vamos a analizarlos con detalle por separado.

message_update()
Lo que viene a continuacin es un poco difcil de entender la primera vez, pero una vez comprendido, los bucles de eventos en Python dejarn de tener misterios. El secreto de los bucles de eventos consiste en que el cdigo se divide entre generadores de eventos y consumidores de eventos. Los bucles de eventos nos ofrecen mecanismos mediante los cuales podemos hacer que una ejecucin se pare, se bloquee, esperando a que un determinado evento aparezca en escena, liberando al programa para que mientras tanto ejecute otro cdigo. El cdigo comienza rescatando de la sesin que el navegador tiene en nuestro servidor el valor para la llave cursor. Lo que el programa hace es emplear una variable en la sesin para controlar cul ha sido el ltimo mensaje que ha recibido ese navegador. En el caso de que no haya mensajes o el ltimo mensaje enviado sea el que aparece en la sesin del navegador bloqueamos con:

Figura 2: Esquema de funcionamiento del chat.

self.new_message_event.wait()

Lo que ocurre en ese preciso momento es lo que hace tan especiales a los bucles de eventos. Gevent deja bloqueado ese hilo de ejecucin y almacena tanto el entorno como la posicin en el programa en una lista, a la espera de que el evento self.new_message_event se active. O sea, mientras no haya informacin nueva para el navegador cuya sesin tenemos en ese instante, no continuaremos ejecu-

tando el cdigo. Si el evento aparece, se contina la ejecucin del cdigo. Usando enumerate se genera una lista con ndices a partir de la cach, y si alguno de los mensajes de la cach se corresponde al cursor, entonces devolvemos en formato JSON todos los mensajes desde ese punto hasta el final de la cach:
>>> l = [a,b,c,d] >>> datos = enumerate(l) >>> for indice,e in datos:

Listado 2: Objeto ChatRoom


01 class ChatRoom(object): 02 cache_size = 200 03 04 def __init__(self): 05 self.cache = [] 06 self.new_message_event = Event() 07 08 def main(self, request): 09 if self.cache: 10 request.session[cursor] = self.cache[-1][id] 11 return render_to_response(index.html, 12 {MEDIA_URL: settings.MEDIA_URL, 13 messages: self.cache}) 14 15 def message_new(self, request): 16 name = request.META.get(REMOTE_ADDR) or Anonymous 17 forwarded_for = request.META.get(HTTP_X_FORWARDED_FOR) 18 if forwarded_for and name == 127.0.0.1: 19 name = forwarded_for 20 msg = create_message(name, request.POST[body]) 21 self.cache.append(msg) 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 if len(self.cache) > self.cache_size: self.cache = self.cache[-self.cache_size:] self.new_message_event.set() self.new_message_event.clear() return json_response(msg)

def message_updates(self, request): cursor = request.session.get(cursor) if not self.cache or cursor == self.cache[-1][id]: self.new_message_event.wait() assert cursor != self.cache[-1][id], cursor try: for index, m in enumerate(self.cache): if m[id] == cursor: return json_response({messages: self.cache[index + 1:]}) 37 return json_response({messages: self.cache}) 38 finally: 39 if self.cache: 40 request.session[cursor] = self.cache[-1][id] 41 else: 42 request.session.pop(cursor, None)

WWW.LINUX- MAGAZINE.ES

Nmero 71

55

DESARROLLO Python: Gevent y Django

Figura 3: La ejecucin en message_update se bloquea hasta que message_new lo libera.

... if e == b: ... print l[indice:] ... [b, c, d] >>>

Por ltimo, actualizamos nuestro cursor a la ltima posicin de la cach en ese momento.

message_new()
El cdigo de message_new() en el Listado 2 comienza por averiguar quin ha mandado el mensaje. Toda peticin HTTP posee usa serie de campos que identifican a la mquina que la ha realizado, pero es posible que esa mquina decida no poner esa informacin, por lo que el cdigo se cura en salud, y en lugar de tratar de cargar el campo REMOTE_ADDR (direccin remota) hace uso del mtodo get() de los diccionarios, que devuelve None en el supuesto de que no exista la llave. Si fuese ese el caso, llamaremos a nuestro usuario Anonymous. El campo HTTP_X_FORWARDED_FOR sirve para que el paquete HTTP nos indique si la mquina que ha originado la peticin se encuentra detrs

de un proxy, y por tanto, tiene una IP diferente a la que aparece en REMOTE_ADDR. Vale, ya tenemos identificado al usuario, ahora generamos la estructura de datos que contendr su mensaje con la funcin create_message() y la almacenamos en nuestra cach. Como la cach puede haber alcanzado un tamao superior al permitido, comprobamos si esto ocurre y eliminamos de la misma los mensajes sobrantes. Para ello hacemos uso de una caracterstica de las listas en Python, los ndices inversos. Son algo liosos, as que un par de ejemplos aclararn rpidamente cmo funcionan:
>>> l = [1,2,3,4,5] >>> l[:3] [1, 2, 3] >>> l[-3:] [3, 4, 5] >>> l[-1] 5 >>> l[-30:] [1,2,3,4,5]

zado desde atrs, as que empleamos self.cache[-self.cache_size:] para obtenerlos. Todo est listo para el momento crucial, ese que hemos estado esperando. Como la lista de mensajes self.cache est preparada y podemos tener unos cuantos message_update() bloqueados esperando, mandamos una seal indicando que hay nueva informacin mediante la llamada self.new_message_event.set(). Todos los puntos del cdigo fuente que se bloquearon en el evento self.new_message_event con wait() pasan a desbloquearse y a ejecutarse uno a uno. En nuestro caso esto implica que un gran nmero de navegadores, cuyo cdigo javascript estaba tratando de obtener informacin en la direccin messages/update, de pronto encuentran que se les responde con datos, y se actualizarn con los ltimos mensajes del chat (ver Figura 3). En cuanto la lista de bloqueo se vace llamamos a self.new_message_event.clear() para volver a poner el evento a False. Si no lo hicisemos, la llamada a self.new_message_event.wait() no bloqueara el cdigo y se pasara a la siguiente instruccin inmediatamente. Nuestro sistema vuelve a estar listo para un nuevo mensaje desde el chat y todas las llamadas a message/updates se bloquern.

Conclusin
Los bucles de eventos estn ganando la partida a las hebras como modelo para sistemas concurrentes de alto rendimiento. El xito sin precedentes de Nginx (Recurso 2) un servidor web basado en eventos o la popularidad de Node.js ( Recurso 3) se la debemos a esta tecnologa. Como hemos podido ver en este artculo, Python no slo cuenta con varias implementaciones de bucles de eventos, sino que adems algunas como Gevent son muy sencillas de usar y nos permiten generar comportamientos muy complejos con menos de 100 lneas de cdigo. I

RECURSOS
[1] Gevent: http://www.gevent.org/ [2] Servidor web Nginx: http://nginx.org/ [3] Servidor basado en eventos Node.js: http://nodejs.org/

En nuestro caso slo queremos los primeros 200 elementos de la lista comen-

56

Nmero 71

WWW.LINUX- MAGAZINE.ES

You might also like