angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Flask, Celery, Redis y Docker

Docker-compose hace muy fácil utilizar el mismo Docker image para su aplicación Flask y el/los trabajador(es) Celery .

29 octubre 2020
post main image
https://unsplash.com/@dillonjshook

Este es un post sobre cómo uso Docker y Docker-composer para desarrollar y ejecutar mi sitio web Flask con Celery y Redis. Hay muchos artículos en internet sobre esto y si los busca no olvide buscar en Github.com. Sólo tomé los pedazos y creé mi propia configuración. Antes de entrar en esto quiero mencionar otras dos cosas que surgieron al agregar Celery a Flask.

El patrón de aplicación de Flask

De nuevo debo referirme al agradable post de Miguel sobre esto. El problema es que debemos crear e inicializar el objeto Celery en el momento en que llamemos a create_app() que configura nuestra aplicación Flask . Hay varias soluciones a esto y yo utilicé una que fue descrita en el artículo "Flask + Celery = how to", ver enlaces abajo. El truco está en usar __init__.py para otro propósito. En la mayoría de los casos estamos usando __init__.py como el archivo con la función create_app() . Lo que hacemos es mover todo este código a un nuevo archivo factory.py y usar __init__.py para instanciar el objeto Celery :

# __init__.py

from celery import  Celery

def make_celery(app_name=__name__):
    celery =  Celery(__name__)
    return celery

celery = make_celery()

Lo que sucede ahora es que instanciamos el objeto Celery en el momento de la creación de la aplicación y luego lo inicializamos en create_app() con los parámetros que especificamos en app.config. Para ejecutar nuestra aplicación cambiamos el archivo run.py . Ahora pasamos el objeto Celery a la función create_app() , algo así como:

#  run.py

import app
from app import factory

my_app = factory.create_app(celery=app.celery, ...)

Para más detalles, consulte el artículo mencionado anteriormente.

Celery y zonas horarias

Pensé que mencioné esto también porque me costó mucho trabajo. Intenté ajustar la zona horaria tanto para Flask como para Celery a algo distinto de UTC. Para la zona horaria 'Europa/Amsterdam' seguí recibiendo el mensaje (de Flower):

Substantial drift from celery@75895a6a62ab may mean clocks are out of sync. Current drift is 7200 seconds.

No he resuelto esto, pero afortunadamente no es realmente un problema. Es una buena práctica ejecutar una aplicación web con la zona horaria UTC y sólo convertir a una zona horaria local cuando se solicite, por ejemplo cuando se muestre una fecha y hora a un visitante. Para evitar problemas, usa UTC en todas partes!

Usando Docker

Estoy usando Docker para el desarrollo, las pruebas, staging y la producción. Debido a que mi servidor de producción está corriendo ISPConfig con una base de datos MariaDB y también tengo instalada una base de datos MariaDB en mi sistema de desarrollo, no agregué una base de datos a mi configuración de Docker sino que me conecto a la base de datos MariaDB usando un socket unix.

Tengo un archivo compartido docker-compose , docker-compose_shared.yml, y docker-compose para las opciones de despliegue, docker-compose_desarrollo.yml, docker-compose_producción.yml, ... Y cada opción de despliegue tiene su propio archivo de entorno.

Para iniciar el desarrollo, yo corro:

docker-compose  -f  docker-compose_shared.yml  -f  docker-compose_development.yml up

Y para iniciar la producción corro:

docker-compose  -f  docker-compose_shared.yml  -f  docker-compose_production.yml up -d

Usando el mismo Docker image para el web-service y el celery-worker

Antes de añadir Celery sólo tenía un servicio: la web. Con Celery tengo al menos tres más:

  • Redis
  • Uno o más trabajadores
  • Flower

Redis y Flower son triviales de agregar, pero ¿cómo agregamos un trabajador? Después de leer en internet decidí que el trabajador debería tener la misma imagen que la imagen de la web. Por supuesto que hay un montón de overhead (código muerto) aquí pero también hacemos nuestra vida más fácil ya que podemos usar código de trabajo que escribimos y probamos antes.

Cuando se construye una imagen con docker-compose, se construye la imagen para web-service . Cuando se ejecuta con docker-compose, tanto el web-service como el celery-worker-service deben utilizar esta imagen.

Abajo está el archivo docker-compose_shared.yml . Sólo muestro las líneas importantes. Algunas variables del archivo .env para la producción:

PROJECT_NAME=peterspython
PROJECT_CONFIG=production
DOCKER_IMAGE_VERSION=1.456
#  docker-compose_shared.yml

version: "3.7"

services:

  redis:
    image: "redis:5.0.9-alpine"
    ...

  web:
    image: ${PROJECT_NAME}_${PROJECT_CONFIG}_web_image:${DOCKER_IMAGE_VERSION}
    container_name: ${PROJECT_NAME}_${PROJECT_CONFIG}_web_container
    env_file:
      - ./.env
    build:
      context: ./project
      dockerfile:  Dockerfile
      args:
        ...
    ports:
      - "${SERVER_PORT_HOST}:${SERVER_PORT_CONTAINER}"
    environment:
      ...
    volumes:
      # connect to mysql via unix socket 
      - /var/run/mysqld:/var/run/mysqld
      # files outside the container:
      ...
    depends_on:
      - redis

  celery_worker1:
    image: ${PROJECT_NAME}_${PROJECT_CONFIG}_web_image:${DOCKER_IMAGE_VERSION}
    env_file:
      - ./.env
    restart: always
    environment:
      ...
    volumes:
      # connect to mysql via unix socket 
      - /var/run/mysqld:/var/run/mysqld
      # files outside the container:
      ...
    # for development we start the celery worker by hand after entering the container, this means we can stop and start after editing files
    # see  docker-compose_development.yml
    command: celery -A celery_worker.celery worker -Q ${CELERY_WORKER1_QUEUE} -n ${CELERY_WORKER1_NAME} ${CELERY_WORKER1_OPTIONS} --logfile=...
    depends_on:
      - web
      - redis

  flower:
    image: "mher/flower:0.9.5"
    ...

El archivo docker-compose_development.yml:

#  docker-compose_development.yml

version: "3.7"

services:
  web:
    ports:
      - "${SERVER_PORT_HOST}:${SERVER_PORT_CONTAINER}"
    volumes:
      # development: use files outside the container
      - ./project:/home/flask/project/
    command: python3 run_all.py

  celery_worker1:
    restart: "no"
    volumes:
      # development: use files outside the container
      - ./project:/home/flask/project/
    command: echo "do not run"

Y el archivo docker-compose_producción.yml:

#  docker-compose_production.yml

version: "3.7"

services:
  web:
    ports:
      - "${SERVER_PORT_HOST}:${SERVER_PORT_CONTAINER}"
    volumes:
    - /var/www/clients/${GROUP}/${OWNER}/web/static:/home/flask/project/sites/peterspython/static
    command: /usr/local/bin/gunicorn ${GUNICORN_PARAMETERS} -b :${SERVER_PORT_CONTAINER} wsgi_all:application

Iniciar y detener manualmente al trabajador durante el desarrollo

Cuando desarrollamos una aplicación Flask utilizamos la opción DEBUG . Entonces la aplicación se reinicia automáticamente cuando hacemos cambios como guardar un archivo.

El trabajador es un programa independiente, cuando se está ejecutando no sabe que ha cambiado el código. Esto significa que debemos detener e iniciar al trabajador después de hacer cambios en una tarea. En el archivo docker-compose_shared.yml hay un comando que inicia el trabajador. Pero en el archivo de desarrollo anulo este comando usando:

    command: echo "do not run"

Durante el desarrollo abro una ventana terminal y comienzo todo usando 'docker-compose up'. Ahora puedo ver todos los mensajes (de depuración) y problemas de la aplicación.

El trabajador no se inició. En otra ventana de terminal introduzco el contenedor del trabajador Docker usando 'docker-compose run sh'. Note que también podría usar 'docker compose exec sh' aquí.

Ya he creado un script de shell start_workers.sh en mi directorio de proyectos:

#  start_workers.sh

celery -A celery_worker.celery worker -Q celery_worker1_queue -n celery_worker1 --loglevel=DEBUG  --logfile=...

Ahora todo lo que tengo que hacer para iniciar el trabajador es escribir en el shell del contenedor:

./start_workers.sh

y Celery comienza.

Para detener Celery sólo tengo que pulsar CTRL-C. Respuesta:

worker: Hitting Ctrl+C again will terminate all running tasks!

worker:  Warm shutdown  (MainProcess)

Aquí tenemos la opción de terminar cualquier tarea en curso.

También es posible hacer un apagado gracioso del trabajador. Vaya a la ventana de la terminal del trabajador y escriba CTRL-Z para detener al trabajador. Respuesta:

[1]+   Stopped                 ./start_workers.sh

Luego escriba:

kill %1

Ahora Celery responde con (seguido del mensaje del proceso de terminación):

worker:  Warm shutdown  (MainProcess)

[1]+   Terminated              ./start_workers.sh

Celery uso de la memoria del trabajador

La solución presentada no es la solución más amigable para la memoria, pero tampoco es tan mala. Para obtener una indicación utilizamos el comando Docker :

docker stats

Respuesta:

CONTAINER ID     NAME                                        CPU  %     MEM USAGE /  LIMIT      MEM %               NET I/O             BLOCK I/O           PIDS
d531bfb686d0      peterspython_production_flower_1           0.17%     32.03MiB / 7.791GiB   0.40%               6.18MB / 2.26MB     0B / 0B             6
a63af28ce411      peterspython_production_celery_worker1_1   0.30%     121.6MiB / 7.791GiB   1.52%               9.95MB / 10.4MB     0B / 0B             3
b8b9f080dc26      peterspython_production_web_container      0.02%     467.3MiB / 7.791GiB   5.86%               1.35MB / 55.7MB     0B / 0B             6
de4fb0ef253a      peterspython_production_redis_1            0.16%     9.059MiB / 7.791GiB   0.11%               12.6MB / 16.1MB     0B / 0B             4

Inicié el web-service con 5 trabajadores Gunicorn . Hay un trabajador Celery con dos hilos (--concurrency=2). El trabajador Celery toma aquí unos 120 MB. Por supuesto que el uso de la memoria aumentará en algunos casos, pero mientras la mayoría de las tareas no sean muy intensivas en memoria, no creo que valga la pena quitarle el código al trabajador Celery .

Resumen

Usar Docker con Docker-compose no sólo es genial porque tenemos un sistema (casi) idéntico para el desarrollo y la producción. También es muy fácil añadir servicios como Redis y Flower. Y usar el mismo Docker image para nuestra aplicación y el trabajador Celery es también muy fácil con Docker-compose. Como siempre hay un inconveniente: lleva tiempo configurar esto. Pero el resultado compensa mucho.

Enlaces / créditos

Celery and the Flask Application Factory Pattern
https://blog.miguelgrinberg.com/post/celery-and-the-flask-application-factory-pattern/page/0

Celery in a Flask Application Factory
https://github.com/zenyui/celery-flask-factory

Dockerize a Flask, Celery, and Redis Application with Docker Compose
https://nickjanetakis.com/blog/dockerize-a-flask-celery-and-redis-application-with-docker-compose

Flask + Celery = how to.
https://medium.com/@frassetto.stefano/flask-celery-howto-d106958a15fe

start-celery-for-dev.py
https://gist.github.com/chenjianjx/53d8c2317f6023dc2fa0

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios (1)

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.

avatar

Hi there thanks!
Is it possible for you to share/post a minimal github project with all these files?