Tareas asíncronas en Django con Celery
En este taller aprenderás la importancia de las tareas asíncronas en el desarrollo de una aplicación web, qué...
Vamos a profundizar en una interesante herramienta como es Celery, explicando qué es, sus muchas ventajas, cómo comenzar y algunos consejos para su uso.
Celery es un gestor de tareas distribuido y asíncrono desarrollado en Python. Es una herramienta magnífica para aplicaciones de alta disponibilidad y con alta carga, y también recomendable cuando consideramos que la carga va a ir aumentando progresivamente y vamos a ir incorporando nuevas máquinas poco a poco a nuestro cluster inicial.
Con Celery podrás utilizar los recursos desaprovechados de tus servidores, distribuir tareas a servidores especializados, añadir o quitar servidores para escalar tu sistema cómodamente y gestionar la prioridad de las tareas.
A continuación, hablaremos de:
Celery se asemeja a sistemas de colas como RabbitMQ y Apache Kafka, pero podrás montar tu sistema con mucho menos trabajo, ya que no tendrás que configurar las colas ni preocuparte por los detalles internos de cómo se distribuyen las tareas.
Además, se integra bien con otros framework Python como Django o Flask y es perfectamente compatible con herramientas de testing como las librerías unittest y pytest, como explicaremos más adelante en este artículo.
Celery se compone de dos tipos de elementos: los backend y los nodos o workers. Además, necesitarás un cliente que se comunique con los backend.
Usarás el cliente para colocar tareas en colas a modo de broker de mensajes y los nodos irán seleccionando las tareas correspondientes para ejecutarlas. Una vez ejecutadas, los resultados se volcarán en el backend de resultados, de donde podrás extraerlos.
El backend incluye dos funcionalidades:
Como transport existen dos implementaciones completas, en RabbitMQ y Redis, pero existen implementaciones parciales en otras tecnologías. La elección más habitual es RabbitMQ.
Como backend de resultados existen muchas implementaciones, pero destacaremos que podemos usar también RabbitMQ y Redis comobackend de resultados. Inicialmente, es habitual usar Redis como _backend de resultados.
Para empezar con Celery está bien utilizar RabbitMQ o Redis para ambos roles, pero en cuanto tengamos cierta carga debemos pasar a un backend de resultados más orientado a almacenaje - una base de datos. Es posible incluso usar la misma DB que Django usando su ORM, con la librería django-celery-results
.
Podéis consultar todas las implementaciones de backend de resultados en la Guía para Usuarios de Celery para seleccionar la que más se ajuste a vuestro caso de uso y stack preferido.
Los nodos o workers son programas Python que se pueden ejecutar en modo demonio en un servidor. Se pueden ejecutar uno o varios nodos por servidor. Cada nodo se puede configurar para que coja tareas de un determinado tipo y según una configuración particular.
Si tienes tareas especializadas que quieres ejecutar en un servidor concreto, entonces solo lanzarás nodos de un determinado tipo en dicho servidor.
En el caso más simple, tendrás uno o varios nodos que resolverán tareas de cualquier tipo.
Para encolar tareas necesitaremos un modo de enviar mensajes al backend. Para ello existen diferentes clientes para frameworks Python y para otros lenguajes como PHP o Javascript. Para Django y Flask no hace falta un cliente especial.
Con el cliente también podremos obtener el resultado de una tarea específica a partir de su identificador único, o de múltiples resultados de un tipo de tarea en general, filtradas por diferentes parámetros.
Para crear un cluster de servidores con Celery necesitarás abrir puertos de comunicación entre estos servidores en los protocolos que determine la tecnología utilizada y la configuración establecida.
Os dejamos la referencia de los primeros pasos en la documentación oficial.
Los nodos tienen dos modos de funcionamiento principales: mediante hilos de procesador o CPU y mediante hilos ligeros. Dependiendo del tipo de carga que queramos resolver, cada worker funcionará de un modo u otro. Por supuesto, podéis tener nodos de ambos tipos según las características de vuestras tareas
Como norma general, para tareas con alto uso de CPU, se recomienda usar hilos de procesador. El número de hilos de procesador que un servidor puede soportar se consigue a partir del número de núcleos de CPU disponible a partir de la fórmula 2 x N -1
, siendo N el número de núcleos. Si nuestra CPU dispone de Hyperthreading, podemos considerar que tenemos el doble de núcleos de los reales. Este número también se conoce como “núcleos virtuales”.
Como podrás intuir, se reserva un hilo para el sistema, Celery puede ser muy intenso y freír tu servidor si le dejas. Si en ese servidor existen otras aplicaciones, entonces limitaremos aún más el número de nodos.
Para tareas con alto uso de entrada y salida de datos (IO) bien sea escritura y lectura a disco o comunicación por red, se recomienda usar hilos ligeros. Hay diferentes tipos de hilos ligeros que puedes investigar por tu cuenta: gevent
y eventlet
. Ten en cuenta que necesitarás instalar alguna librería extra para usar ciertos tipos de hilo.
Para tareas mixtas, lo más simple es usar hilos de procesador, o hilos ligeros, pero limitando mucho su número.
Entender el tipo de tareas que estamos ejecutando te servirá para optimizar al máximo el uso de recursos, en el caso de poder utilizar hilos ligeros pasarás de paralelizar tareas en unos pocos hilos de procesador, a hacerlo en decenas o cientos de hilos ligeros.
En entornos productivos, Celery es un sistema intensivo capaz de gestionar un número muy alto de tareas concurrentes que es muy difícil de seguir “a ojo”. Por tanto, es muy importante monitorizarlo apropiadamente para evitar problemas mayores.
El primer paso es establecer un buen logging como harías en cualquier proyecto. El módulo logging de la librería estándar de Python viene preparado de manera nativa para incluir identificadores de hilos (threads), que en nuestro caso serán tareas.
En el caso de Celery, usaremos un logger particular de la siguiente manera:
from celery.utils.log import get_task_logger
from worker import app
logger = get_task_logger(__name__)
@app.task()
def my_task(x, y):
result = x + y
logger.info("Computing")
return result
Así, todas nuestras entradas de logging tendrán el nombre de la tarea (my_task) y su identificador único, típicamente un UUID.
A partir de aquí, podemos aplicar soluciones típicas de logging centralizado como el stack ELK (Elastic Search - Logstash - Kibana) y conectarlo con sistemas típicos de monitorización y alertas.
Además de las técnicas de monitorización clásicas, existe un proyecto llamado Flower creado en el seno de la comunidad diseñado específicamente para monitorizar Celery.
Flower se conectará a los backend como un worker más y extraerá información de la ejecución de las tareas periódicamente. Flower te proporcionará una interfaz gráfica donde podrás ver información sobre una tarea o un grupo de tareas como:
Al utilizar Flower, tendrás que abrir puertos adicionales en tu cluster y utilizar un servidor web como Apache o Nginx para servirlo - para poder acceder a la interfaz.
Dada la simplicidad de incluir Flower en tu sistema, es altamente recomendable añadirlo desde el principio.
Celery se integra perfectamente en las herramientas de testing de Python como unittest y pytest. Para ello configuraremos Celery en modo eager (algo así como “inmediato”) y resolveremos las tareas con delay nulo. Esto nos permitirá testear nuestro código resuelve-tareas sin ninguna adaptación extra.
Además, podremos substituir nuestro backend de resultados de producción por algo más manejable, como por ejemplo, una instancia local de Redis.
Si vuestro framework de referencia es pytest, deberéis activar su integración, por ejemplo, instalando pytest-celery
.
Para más información acerca de tests con Celery, de nuevo os recomendamos la documentación oficial.
Más allá de recibir y resolver tareas, Celery tiene la capacidad de reaccionar ante cualquier eventualidad y de configurar la ejecución de acuerdo a nuestras necesidades. Así que, en vez de diseñar lógicas complejas nosotros mismos, acudiremos a la configuración avanzada.
Entre otras muchas características, Celery te permite establecer diferentes límites para la ejecución de tareas, por ejemplo, crear tareas perecederas, esto es, tareas que no se ejecutarán pasado un determinado momento.
Con cada tarea, podremos reaccionar a diferentes situaciones fácilmente, como por ejemplo, cancelar una tarea que lleve mucho tiempo en cola o que tarde mucho en resolverse.
En el caso de que quisiéramos lanzar una tarea repetidas veces siguiendo cierta lógica, existe un módulo especializado en tareas periódicas, llamado Celery Beat.
Con Celery Beat podrás ejecutar tareas de manera recurrente durante un periodo determinado o permanente. Celery Beat es muy versátil y podrás o bien repetir con una frecuencia - por ejemplo cada 5 minutos- o bien a una determinada hora del día o del mes - como por ejemplo, todos los domingos a las 7 o pasados 10 minutos de cada hora entre las 8 y las 19 horas.
Si hay un error en la ejecución de una tarea y consideramos que vale la pena lanzarla otra vez, podemos definir un número de reintentos determinado, con un tiempo de espera determinado o mediante un algoritmo - aleatorio, backoff exponencial, etc.
Dependiendo de la naturaleza de nuestra tarea es posible que nos sea indiferente que esta se ejecute correctamente o que, por lo contrario, necesitemos confirmación. Para el segundo caso, activaremos la confirmación de tarea terminada (acknowledgement o ACK), así, si una tarea tarda mucho en resolverse o un worker se ha parado en medio de una ejecución, esta se volverá a encolar y ejecutar.
Esta configuración nos permite, entre otras cosas, parar workers en cualquier momento - para tareas de mantenimiento, por ejemplo - sin miedo a que una tarea no se vaya a ejecutar jamás.
Una vez familiarizado con Celery, encontrarás situaciones comunes como la caída de un elemento o una carga demasiado alta, para las que debes estar preparado. A continuación, te doy una serie de recomendaciones para prevenir o reaccionar ante ellas.
Es habitual que nos equivoquemos al predecir la carga o que en momentos puntuales del día se acumulen tareas de un tipo particular. Si es necesario responder rápido, es bueno tener un worker listo para conectarse y añadir capacidad a tu sistema, o contar con recetas o scripts que provisionen nuevas máquinas rápidamente. Sé previsor y no dejes de comprobar que dichas recetas funcionan correctamente.
Añadir un servidor nuevo a un cierto sistema implican riesgos de seguridad, configuración de redes, permisos de IP, gestión de certificados, etc. Cuanto más preparado estés, menos tiempo tardarás en reaccionar. En la línea del párrafo anterior, comprueba tu sistema con antelación y garantiza que todo funciona correctamente y de manera segura.
Otra eventualidad frecuente es que el gestor de tareas se caiga. Por ello es muy recomendable que este tenga configurada persistencia en disco, ya que tanto Redis como RabbitMQ trabajan por defecto solo en memoria RAM. Así, cuando reactivemos nuestro broker, las tareas pendientes no habrán desaparecido.
En el caso de los workers, es suficiente con activar la confirmación de tareas como vimos más arriba, para que, en caso de parada, no perdamos ninguna ejecución importante.
Y por último, no sobrecargues los parámetros de las tareas ni el resultado. Estos se guardarán temporalmente en el broker y podrían saturarlo. Piensa que los workers pueden acceder a otros componentes como una aplicación normal, por ejemplo, leer y escribir en Bases de Datos.
Por lo tanto, no envíes toda la información a través de Celery, sino simplemente los medios necesarios para que tus programas pueda conseguirlos por sí mismos, nos referimos a rutas de ficheros, identificadores de objetos, credenciales, etc.
Hasta este punto hemos visto cómo y cuándo usar Celery, pero no debemos caer en el típico error de utilizar la misma herramienta para todo. Celery es una gran herramienta, pero hay casuísticas donde puede parecer una solución válida y realmente no lo es.
Un caso típico es cuando queremos hacer una serie de tareas de manera secuencial. Puedes conseguir hacerlo con Celery, haciendo que una tarea genere una petición para otra, pero hay herramientas mucho mejor adaptadas como Apache Airflow o herramientas específicas para ETLs.
No podremos usar Celery cuando queramos resolver tareas en otros lenguajes diferentes a Python. Podemos caer en la tentación de llamar a otro lenguaje desde Python, pero en general es una mala idea. En tales casos, una posibilidad es usar una cola de mensajes genérica directamente, como por ejemplo RabbitMQ.
El tercer caso más común donde Celery puede parecer una buena opción, pero no lo es tanto, es cuando necesitamos resolver una acción de manera inmediata. Celery puede funcionar en modo eager, como hemos visto, pero no es para lo que está pensado. En todo caso, pensad que las funciones que resuelven nuestras tareas se pueden reutilizar en otros servicios, así evitaréis caer en este antipatrón.
Celery es una herramienta muy capaz que nos facilitará la vida al escalar nuestro sistema para resolver una alta carga sin grandes complicaciones e inversiones. Es un paso natural desde una aplicación básica como puede ser una API, y antes de introducirnos en sistemas más complejos como sistemas orientados a eventos.
También cabe destacar su gran documentación oficial, a la que debemos acudir a menudo para conseguir el máximo partido de nuestro sistema.
Esperamos que este artículo os haya ayudado a conocer mejor este gran proyecto y os ayude a hacer vuestro día a día más fácil.
También te puede interesar
En este taller aprenderás la importancia de las tareas asíncronas en el desarrollo de una aplicación web, qué...
Aprende a crear un sitio web con Flask, el mini framework web de Python, perfecto para aquellos que...