Flask, Celery, Redis et Docker
Docker-compose permet d'utiliser très facilement le même Docker image pour votre application Flask et le(s) travailleur(s) Celery .
Ce billet explique comment j'utilise Docker et Docker-composer pour développer et faire fonctionner mon site web Flask avec Celery et Redis. Il existe de nombreux articles sur Internet à ce sujet et si vous les recherchez, n'oubliez pas de faire une recherche sur Github.com. J'ai juste pris les morceaux et j'ai créé ma propre installation. Avant d'entrer dans le vif du sujet, je voudrais mentionner deux autres choses qui sont apparues lorsque j'ai ajouté Celery à Flask.
Le modèle d'application Flask
Encore une fois, je dois me référer à l'article de Miguel Grinberg, qui est très intéressant à ce sujet. Le problème est que nous devons créer et initialiser l'objet Celery au moment où nous appelons create_app() qui met en place notre application Flask . Il existe plusieurs solutions à ce problème et j'en ai utilisé une qui est décrite dans l'article "Flask + Celery = comment faire", voir les liens ci-dessous. L'astuce consiste à utiliser __init__.py dans un autre but. Dans la plupart des cas, nous utilisons __init__.py comme fichier avec la fonction create_app() . Ce que nous faisons, c'est déplacer tout ce code dans un nouveau fichier factory.py et utiliser __init__.py pour instancier l'objet Celery :
# __init__.py
from celery import Celery
def make_celery(app_name=__name__):
celery = Celery(__name__)
return celery
celery = make_celery()
Ce qui se passe maintenant, c'est que nous instancions l'objet Celery au moment de la création de l'application et que nous l'initialisons ensuite dans create_app() avec les paramètres que nous avons spécifiés dans app.config. Pour exécuter notre application, nous modifions le fichier run.py . Nous passons maintenant l'objet Celery à la fonction create_app() , un peu comme :
# run.py
import app
from app import factory
my_app = factory.create_app(celery=app.celery, ...)
Pour plus de détails, reportez-vous à l'article mentionné ci-dessus.
Celery et fuseaux horaires
Je pensais le mentionner aussi parce que j'ai eu du mal à le faire. J'ai essayé de régler le fuseau horaire pour Flask et Celery sur autre chose que UTC. Pour le fuseau horaire "Europe/Amsterdam", je continuais à recevoir le message (de Flower) :
Substantial drift from celery@75895a6a62ab may mean clocks are out of sync. Current drift is 7200 seconds.
Je n'ai pas résolu ce problème, mais heureusement, ce n'est pas vraiment un problème. Il est bon d'utiliser une application web avec le fuseau horaire UTC et de ne convertir vers un fuseau horaire local que lorsque cela est demandé, par exemple pour montrer une date et une heure à un visiteur. Pour éviter les problèmes, utilisez UTC partout !
Utilisation de Docker
J'utilise Docker pour le développement, les essais, staging et la production. Comme mon serveur de production exécute ISPConfig avec une base de données MariaDB et que j'ai également une base de données MariaDB installée sur mon système de développement, je n'ai pas ajouté de base de données à ma configuration Docker mais je me connecte à la base de données MariaDB en utilisant une socket unix.
Je partage les fichiers docker-compose , docker-compose_shared.yml, et docker-compose pour les options de déploiement, docker-compose_development.yml, docker-compose_production.yml, ... Et chaque option de déploiement a son propre fichier d'environnement.
Pour démarrer le développement, je lance :
docker-compose -f docker-compose_shared.yml -f docker-compose_development.yml up
Et pour démarrer la production, je lance :
docker-compose -f docker-compose_shared.yml -f docker-compose_production.yml up -d
En utilisant le même Docker image pour le web-service et le celery-worker
Avant d'ajouter Celery , je n'avais qu'un seul service : le web. Avec Celery , j'en ai au moins trois de plus :
- Redis
- Un ou plusieurs travailleurs
- Flower
Les questions Redis et Flower sont triviales à ajouter, mais comment ajouter un travailleur ? Après avoir lu sur Internet, j'ai décidé que le travailleur devait avoir la même image que celle du site. Bien sûr, il y a beaucoup de surcharge (code mort) ici, mais nous rendons aussi notre vie plus facile car nous pouvons utiliser un code de travail que nous avons écrit et testé auparavant.
Lorsque l'on construit une image avec docker-compose, l'image pour web-service est construite. Lors de l'exécution avec docker-compose, le service web-service et le service celery-worker doivent tous deux utiliser cette image.
Le fichier docker-compose_shared.yml se trouve ci-dessous. Je ne montre que les lignes importantes. Quelques variables du fichier .env pour la production :
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"
...
Le fichier 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"
Et le fichier docker-compose_production.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
Démarrer et arrêter manuellement le travailleur pendant le développement
Lors du développement d'une application Flask , nous utilisons l'option DEBUG . Ensuite, l'application redémarre automatiquement lorsque nous apportons des modifications, par exemple en enregistrant un fichier.
Le travailleur est un programme séparé, lorsqu'il est en cours d'exécution, il ne sait pas que vous avez changé le code. Cela signifie que nous devons arrêter et redémarrer le travailleur après avoir apporté des modifications à une tâche. Dans le fichier docker-compose_shared.yml , il y a une commande qui permet de démarrer le travailleur. Mais dans le fichier de développement, j'annule cette commande en utilisant :
command: echo "do not run"
Pendant le développement, j'ouvre une fenêtre de terminal et je démarre tout en utilisant "docker-compose up". Maintenant, je peux voir tous les messages (de débogage) et les problèmes dans l'application.
Le travailleur n'a pas démarré. Dans une autre fenêtre de terminal, je saisis le conteneur Docker du travailleur en utilisant "docker-compose run sh". Notez que je pourrais également utiliser "docker compose exec sh" ici.
J'ai déjà créé un script shell start_workers.sh dans le répertoire de mon projet :
# start_workers.sh
celery -A celery_worker.celery worker -Q celery_worker1_queue -n celery_worker1 --loglevel=DEBUG --logfile=...
Maintenant, tout ce que j'ai à faire pour démarrer le travailleur est de taper dans le shell du conteneur :
./start_workers.sh
et le script Celery démarre.
Pour arrêter Celery , il suffit d'appuyer sur CTRL-C. Réponse :
worker: Hitting Ctrl+C again will terminate all running tasks!
worker: Warm shutdown (MainProcess)
Ici, nous avons la possibilité de mettre fin à toute tâche en cours.
Il est également possible d'effectuer un arrêt gracieux du travailleur. Allez dans la fenêtre du terminal du travailleur et tapez CTRL-Z pour arrêter le travailleur. Réponse :
[1]+ Stopped ./start_workers.sh
Puis tapez :
kill %1
Maintenant, Celery répond avec (suivi du message du processus de fin) :
worker: Warm shutdown (MainProcess)
[1]+ Terminated ./start_workers.sh
Celery utilisation de la mémoire du travailleur
La solution présentée n'est pas la plus favorable à la mémoire, mais elle n'est pas si mauvaise non plus. Pour obtenir une indication, nous utilisons la commande Docker :
docker stats
Réponse :
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
J'ai lancé la commande web-service avec 5 travailleurs Gunicorn . Il y a un travailleur Celery avec deux fils (--concurrency=2). Le travailleur Celery prend ici quelque 120 Mo. Bien sûr, l'utilisation de la mémoire augmentera dans certains cas, mais tant que la plupart des tâches ne requièrent pas beaucoup de mémoire, je ne pense pas qu'il vaille la peine de retirer le code de l'employé Celery .
Résumé
Utiliser Docker avec Docker-compose n'est pas seulement formidable parce que nous avons un système (presque) identique pour le développement et la production. Il est également très facile d'ajouter des services comme Redis et Flower. Et utiliser le même Docker image pour notre application et le travailleur Celery est également très facile avec Docker-compose. Comme toujours, il y a un inconvénient : il faut du temps pour le mettre en place. Mais le résultat compense beaucoup.
Liens / crédits
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
En savoir plus...
Celery Docker Docker-compose Flask Redis
Laissez un commentaire
Commentez anonymement ou connectez-vous pour commenter.
Commentaires (1)
Laissez une réponse
Répondez de manière anonyme ou connectez-vous pour répondre.
Hi there thanks!
Is it possible for you to share/post a minimal github project with all these files?
Récent
- Masquer les clés primaires de la base de données UUID de votre application web
- Don't Repeat Yourself (DRY) avec Jinja2
- SQLAlchemy, PostgreSQL, nombre maximal de lignes par user
- Afficher les valeurs des filtres dynamiques SQLAlchemy
- Transfert de données sécurisé grâce au cryptage à Public Key et à pyNaCl
- rqlite : une alternative à haute disponibilité et dist distribuée SQLite
Les plus consultés
- Utilisation des Python's pyOpenSSL pour vérifier les certificats SSL téléchargés d'un hôte
- Utiliser UUIDs au lieu de Integer Autoincrement Primary Keys avec SQLAlchemy et MariaDb
- Connexion à un service sur un hôte Docker à partir d'un conteneur Docker
- Utiliser PyInstaller et Cython pour créer un exécutable Python
- SQLAlchemy : Utilisation de Cascade Deletes pour supprimer des objets connexes
- Flask RESTful API validation des paramètres de la requête avec les schémas Marshmallow