Celery - Supervisor: gevent vs prefork

By valbornoz June 19, 2025, 10:11 p.m.

Celery - Supervisor: gevent vs prefork

Cuando se realiza una configuración de supervisor podemos definir como celery va a manejar la concurrencia de nuestro worker, para esto podemos usar "-P". Por lo que podemos tener los siguientes dos casos:

-P prefork (por defecto): usa procesos hijos (multiprocessing) - Cada "worker" es un proceso completo separado (real paralelo en CPU) - Consume más memoria (cada proceso es independiente) - Bueno para tareas CPU-bound o que hacen mucho trabajo "pesado"

command=/documents/myproject/bin/celery worker -A myproject --loglevel=INFO -Ofair --concurrency=12 --queue=default -n="default@worker"

Cuando se usa celery -P prefork(Este es el valor por defecto)

  • Celery lanzara 12 procesos hijos
  • Cada proceso es independiente (por eso consume más memoria)
  • Real paralelismo en CPUs (bueno si haces muchas tareas que usan CPU)

Supervisor no lanza estos procesos por separado - Supervisor lanza el proceso padre de Celery, y Celery lanza los procesos hijos.

-P gevent: usa greenlets - Un solo proceso, que lanza corrutinas ligeras(greenlets) - Mucho más eficiente en tareas de I/O (red, espera de sockets, APIs) - Puede manejar muchas más tareas concurrentes sin usar tanta RAM - No mejora en tareas pesadas de CPU (porque sigue siendo 1 proceso)

command=/documents/myproject/bin/celery worker -A myproject --loglevel=INFO -Ofair -P gevent --concurrency=250 --queue=default -n="default@worker"
  • Celery lanza 1 proceso padre con 250 greenlets(corrutinas, no procesos)
  • Muy eficiente para tareas de red o I/O (llamadas a APIs, base de datos, escribir en disco..)
  • Consume poca memoria incluso con cientos de tareas concurrentes
  • No hace paralelo en CPU real. Si cada tarea usa mucha CPU, Gevent no es adecuado.
Pool (-P) Tipo de concurrencia Uso preferente
prefork Procesos (multiprocessing) Tareas pesadas en CPU (cálculos, PDF, imágenes)
gevent Greenlets (corrutinas dentro de 1 proceso) Tareas I/O-bound (red, APIs, espera de sockets, webhooks)

gevent vs prefork

Contenido Bloqueante

Cuento se define que tipo de pool se usara en la configuración de supervisor es necesario saber que tipo de tareas recibira el worker, por ejemplo, es necesario saber que tipo de codigo estara ejecutando, en especial si es contenido bloqueante(save del ORM de Django).

Suponiendo que se tiene un loop como un for que tiene un recorrido de 5000 vueltas, en cada vuelta se realiza un save a un modelo, esto es un contenido bloqueante. ¿Que pool es recomendado usar?

Gevent

Permite manejar muchas tareas "en paralelo" (porque no son procesos sino greenlets)

  • .save() es una operación bloqueante, porque toca el ORM + base de datos.
  • Gevent no es ideal si se esta haciendo mucho trabajo CPU + ORM.
  • Si se hace .save() 5000 veces dentro de un for, todo eso ocurre dentro de un único greenlet, por lo que no se paraleliza bien.

Prefork

  • Cada worker es un proceso separado.
  • Cada worker puede usar toda la CPU disponible.
  • Cada proceso se conecta a la DB por su cuenta.
  • No importa que .save() sea bloqueante.
  • Causa mas consumo de memoria

En este caso en concreto debemo usar prefork, ya que de usar Gevent, bloqueariamos el worker, causando la impresión de que no se estan realizando mas tareas hasta que la tarea con el loop de 5000 vueltas termine.

Prueba de contenido bloqueante

En la siguiente prueba ejecutaremos las siguientes tareas, para intentar bloquear el worker con una sola tarea y no permitir la ejecución de otras tareas.

@shared_task(queue="default", routing_key='default', on_failure=except_periodictask)
def bloqueante():
    print("Inicia tarea bloqueante")
    total = 0
    for i in range(1, 500):  # IMPORTANTE: Para causar el bloque se puede usar 500000000 sin embargo, si el equipo no cuenta con los recursos suficientes puede causar un bloqueo del equipo
        total += i
    print("Termina tarea bloqueante")

@shared_task(queue="default", routing_key='default', on_failure=except_periodictask)
def normal_1():
    print("Tarea normal 1 ejecutada")

@shared_task(queue="default", routing_key='default', on_failure=except_periodictask)
def normal_2():
    print("Tarea normal 2 ejecutada")

Primero realizaremos la prueba anterior con la configuración siguiente usando Prefork:

command=/documents/myproject/bin/celery worker -A myproject --loglevel=INFO -Ofair --concurrency=12 --queue=default -n="default@worker"

Como resultado tendremos lo siguiente, la tarea bloqueante se quedara ejecutando y las dos siguientes, se ejecutaran sin ningun tipo de retraso causado por la tarea bloqueante:

Uso de recursos del servidor

Estado de la ejecución de las tareas desde Flower

Por otro lado, si realizamos la prueba anterior con la configuración siguiente usando Gevent:

command=/documents/myproject/bin/celery worker -A myproject --loglevel=INFO -Ofair -P gevent --concurrency=250 --queue=default -n="default@worker"

como resultado tendremos un bloqueo completo del worker, y en el peor de los casos, del servidor.

====================================================================================================

ES IMPORTANTE TOMAR EN CUENTA QUE SI SE REALIZA LA EJECUCIÓN DE LAS PRUEBAS CON GEVENT, EXISTE LA POSIBILIDAD QUE SU EQUIPO SE CONGELE O BLOQUEE POR EL USO EXCESIVO DE CPU, TOMELO EN CUENTA CUANDO REALIZE LAS PRUEBAS.

====================================================================================================