• slidebg1

Celery y Django: Puesta a punto para ‘dummies’


 

Django es un robusto framework para el desarrollo de aplicaciones de backend sobradamente conocido. Nos ofrece un entorno de desarrollo sólido, y nos permite realizar casi cualquier cosa que nos propongamos. No obstante, cuando desarrollamos una API, en ocasiones nos encontramos con funciones que queremos ejecutar de forma asíncrona tras llamar a un endpoint, sin bloquear el proceso de respuesta del mismo. El ejemplo más claro sería el envío de un correo de bienvenida tras un registro. En este caso, el envío del correo introduce un retardo que no queremos trasladar al procesado de la respuesta. Por otra parte, también habrá tareas que querremos ejecutar en instantes precisos en el tiempo: en una aplicación de venta de entradas, justo cuando se alcanza la hora de finalización de un evento. Finalmente, hay tareas que pueden consumir muchos recursos para su ejecución, y preferimos modular los recursos destinados a las mismas con una expectativa de resultados.

 

Para resolver todas estas situaciones tenemos a Celery: un motor de colas de trabajos asíncronos, basado en protocolos de paso de mensajes. Celery nos permite compactar una función en una tarea procesada en una cola.

 

logo django + celery 

 

Cada cola es procesada por uno o más workers que se encargarán de ejecutar dicha tarea. Además, cada worker puede disponer de varios hilos para ejecutar tareas. De esta forma vemos cómo podemos dar solución a esas situaciones con las que nos encontrábamos.

 

Haciendo un breve resumen Celery nos sirve para:

  • Ejecutar tareas de forma asíncrona, sin bloquear el hilo de ejecución del endpoint de nuestra API al que acabamos de llamar

  • Programar la ejecución de tareas para ser ejecutadas de formas periódica, o bien en un instante concreto en el tiempo

 

Celery utiliza un broker de mensajes para procesar las tareas. Puede utilizar RabbitMQ, Redis, AmazonSQS. En nuestro caso vamos a configurarlo para trabajar con RabbitMQ.

 

Vamos a ver una configuración básica de Celery 4 con Django.

 

 

Instalación de Celery

 

Para empezar, deberemos instalar Celery en el entorno virtual en el que se ejecute nuestro proyecto.

En principio bastaría con ejecutar pip install celery desde el entorno virtual.

Otra opción será añadir celery a las dependencias de nuestro proyecto.

 

En requirements.txt añadimos:

celery

amqp

 

Y ejecutamos:

 

Configuración de Celery

 

La configuración de Celery la realizamos declarando constantes en nuestro settings.py

En nuestro caso, para Celery 4, la sintaxis estándar a seguir es la siguiente

 

 

Vemos que hemos configurado celery para utilizar amqp como broker, con un usuario dado y un password, que previamente hemos exportado como variable de nuestro entorno virtual en nuestro postactivate.

 

Tras fijar la configuración en settings, creamos un archivo celery.py en la aplicación raíz de nuestro proyecto Django. Basta con que este archivo contenga lo siguiente:

 

 

Como vemos, instanciamos la aplicación de Celery con un nombre dado, cargamos la configuración desde el módulo settings, y activamos el “descubrimiento automático” de nuevas tareas.

 

Instalación de RabbitMQ

 

logo rabbitmq 

 

En nuestro caso vamos a utilizar RabbitMQ como broker de mensajes. Nuestra aplicación de backend se ejecuta en un servidor Ubuntu, por tanto tendremos que instalar y ejecutar RabbitMQ.

Seguiremos las instrucciones que indica el manual de Rabbit.

 

 

Creamos un usuario para Celery:

 

Reiniciamos el servicio:

 

 

 

Creando nuestra primera tarea

 

Como ejemplo utilizaremos una tarea en la que enviamos un correo de bienvenida a todos los nuevos usuarios que se dan de alta en nuestra aplicación. Para ello, dentro de la app de django users, creamos un archivo llamado tasks.py

 

Y en él, introduciremos el código es el siguiente:

 

Además, dejaremos un rastro en el logger de la aplicación para poder comprobar si se ha realizado el envío correctamente, si fuera necesario.

 

4. Llamar a la tarea:

¿Y cómo enviamos el correo de bienvenida? Podríamos hacerlo de dos maneras: al final el proceso de sign-up, o bien con una signal sobre el modelo de usuario. Si bien esta opción ofusca de cierta manera la lógica de creación de usuario, pensamos que en este punto sí es adecuado recoger aquí esta funcionalidad, por presentar una solución más compacta:

 

 

En esta llamada vemos dos puntos muy relevantes:

  • La llamada a la tarea la realizamos con el método apply_async(). En este caso el paso de parámetros lo hemos hecho mediante una signature, pero podríamos haberlo hecho directamente sobre apply_async()

  • En la llamada no pasamos como parámetros la dirección de correo electrónico del usuario. Es más, si hubiera más datos que quisiéramos utilizar del modelo de usuario, tampoco los pasaremos como parámetros. Sólo pasaremos los ID de los modelos que deseamos utilizar en la tarea. Esto es así por una razón muy sencilla: las tareas de celery se procesan en una cola. Se consumen por uno o más workers, con uno o más threads cada uno. El instante de ejecución de la tarea no es el momento en que realizamos la llamada. Por tanto, si pasamos datos del modelo como parámetro, estos podrían cambiar hasta que se realice la ejecución, y encontraríamos una condición de carrera de manual.
    Siempre pasaremos los ID de los modelos que vamos a utilizar y, además, debemos ser conscientes de que quizá ese modelo ya no exista cuando se ejecute la tarea. Deberemos capturar correctamente esa excepción.

 

5. Llevar a producción: ‘daemonización’

Ya tenemos nuestro primer envío de correo a todos los usuarios que se registran, asíncrono, no bloqueante. Bien, ¿cómo lo llevamos a producción?

 

La ejecución de nuestra aplicación se lanza ejecutando un worker que consuma nuestra aplicación de celery, en nuestro entorno virtual. En producción deberemos ejecutar la instrucción de forma ‘daemonizada’. En nuestro caso utilizamos Supervisor para lanzar las aplicaciones de backend.

 

Así quedaría una plantilla del script de ejecución:

 

Bastaría sustituir las variables del template por las de nuestro proyecto. Como vemos, lanzamos un worker para consumir una cola sobre la aplicación celery de nuestro proyecto. Esta es la solución más sencilla.  Según refleja la documentación de celery se podrían añadir más workers, utilizar concurrencia para especificar el número de procesos por worker, etc.

 

Ahora, añadiremos a supervisor una configuración para lanzar este script.

En /etc/supervisor/conf.d/ añadiremos un archivo llamado project_celery.conf con el siguiente contenido

 

 

Para lanzar nuestra aplicación celery, bastará con ejecutar:

sudo supervisorctl start project_celery

 

Tareas de ejecución periódica: Celery Beat

 

Cuando iniciamos este artículo, uno de nuestros objetivos era poder ejecutar tareas de forma programada, periódica. Bien, aquí entra Celery Beat. Será otra aplicación de celery que únicamente se encargará de la programación o “scheduling” de tareas.

 

Vamos a crear una tarea que, por ejemplo, setea un flag del perfil de usuario a un cierto valor. Por ejemplo, puede que a los usuarios les sirvamos un mensaje diario que sólo deben ver una vez en el día. En este caso, tendríamos un atributo booleano en el perfil de usuario, que deberíamos marcar como False cada día.

 

Bien, ahora, ¿quién se encarga de programar esta tarea?

Una forma muy sencilla de configurar el ‘scheduling’ de tareas es desde nuestro archivo de settings del proyecto.

 

Un ejemplo sería el siguiente:

 

Hemos fijado las 4 am de la timezone de Celery para ejecutar la tarea. Con esto, todas las noches se realizará esta acción. Podríamos utilizar el programador con horario solar de Celery, es realmente potente.

Reflexionemos un momento sobre la ejecución de esta tarea: obtiene los usuarios a actualizar, y realiza el seteo de ese atributo en un bucle for. Si la operación a realizar dentro de ese bucle fuera muy costosa; o si el número de usuarios fuera muy elevado, esta tarea bloquearía al worker mediante todo su tiempo de ejecución. Si hubiera más tareas, éstas deberían esperar en la cola para ser consumidas. Podríamos optimizar la ejecución de esta tarea englobando el código a ejecutar para cada usuario en otra tarea de celery. Así, con la primera llamada seleccionaremos a los usuarios, para posteriormente crear N tareas, una por usuario, que se encolarían para ser procesadas por nuestro worker. Es posible que, si utilizamos varios procesos para el worker, la tarea se ejecute en menos tiempo que con el ejemplo que hemos utilizado inicialmente.

 

Daemonización de Celery beat

 

Bien, tenemos nuestra app de Celery, nuestra app de Celery beat, pero hay que llevar a producción también nuestro programador de tareas. Lo haremos de forma muy parecida: creamos un script que lanzaremos desde supervisor.

 

Nuestro script seguirá esta plantilla:

 

En supervisor, crearemos este archivo de configuración, esta vez siguiendo un template:

 

Vemos que la configuración es muy similar a la seguida para nuestra app inicial.

Debemos recordar que primero debemos lanzar nuestra aplicación de celery y, posteriormente, nuestro celery beat.

 

 

Monitorización: panel web con Flower

 

Existen multitud de herramientas para monitorizar la ejecución de nuestras tareas de Celery. Nosotros nos decantamos por Flower, un panel web que nos expone resultados de ejecución de nuestras tareas.

Dado que ya tenemos configurado nuestro proyecto, añadir Flower será muy sencillo.

Bastará con añadir Flower a los requisitos del proyecto:

flower

 

Volvemos a instalar dependencias ejecutando desde nuestro entorno virtual:

pip install -r requirements.txt

 

En este punto hemos instalado Flower, pero hemos de lanzar la ejecución de la herramienta. Aquí nos explican cómo hacerlo. Esa sintaxis nos recuerda a la ejecución de los workers de celery.

 

Para ejecutarlo y llevarlo a producción seguiremos los mismos pasos que hemos descrito anteriormente:

Lanzaremos Flower de forma ‘daemonizada’ a través de Supervisor. Para esto añadimos otro script a /etc/supervisor/conf.d/ con el siguiente contenido

 

 

Por otro lado, el contenido del script que lanzará supervisor será el siguiente:

 

Éste es un ejemplo con sintaxis de plantilla. Deberíamos sustituir las variables por las de nuestro proyecto.

 

Reparemos en dos detalles:

Utilizamos la opción --url_prefix=flower con el objetivo de poder visualizar el panel en la ruta /flower/ de nuestra api. Más adelante veremos cómo hacemos esto.

Además, no queremos exponer nuestro panel Flower de forma pública. Por esto debemos securizar el acceso con un usuario:password.

 

Finalmente, en nuestro archivo de configuración del sitio de Nginx deberemos añadir la ruta a Flower, para que aparezca bajo server_name/flower/ , siendo server_name la dirección de nuestra app Django.

 

La configuración en Nginx se hará añadiendo estas líneas al sitio de nuestra app web:

 

Y ya lo tenemos. Podemos monitorizar la ejecución de nuestras tareas, ver las que han fallado, tiempos de entrega y procesado, tiempo de espera en colas, etc.

 

Públicado el 20/06/2017

Comparte este post:

CATEGORÍAS: Aprendizaje Desarrollo Tendencias