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

Flask, Celery, Redis en Docker

Docker-compose maakt het zeer eenvoudig om dezelfde Docker image te gebruiken voor uw Flask applicatie en de Celery medewerker(s).

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

Dit is een bericht over hoe ik Docker en Docker-composer gebruik om mijn Flask website met Celery en Redis te ontwikkelen en te beheren. Er zijn veel artikelen op het internet over dit onderwerp en vergeet niet te zoeken op Github.com als u ze zoekt. Ik heb gewoon de stukjes en beetjes gepakt en mijn eigen setup gemaakt. Voordat ik hier op inga wil ik nog twee andere dingen noemen die naar voren kwamen bij het toevoegen van Celery aan Flask.

Het toepassingspatroon van Flask

Ook hier moet ik weer verwijzen naar Miguel 's leuke post over dit onderwerp. Het probleem is dat we het Celery object moeten maken en initialiseren op het moment dat we create_app() aanroepen die onze Flask app opzet. Er zijn verschillende oplossingen hiervoor en ik heb er een gebruikt die beschreven is in het artikel 'Flask + Celery = hoe werkt het', zie onderstaande links. De truc is om __init__.py voor een ander doel te gebruiken. In de meeste gevallen gebruiken we __init__.py als bestand met de functie create_app() . Wat we doen is al deze code verplaatsen naar een nieuw bestand factory.py en __init__.py gebruiken om het Celery object te instantiëren:

# __init__.py

from celery import  Celery

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

celery = make_celery()

Wat er nu gebeurt is dat we het Celery object instantiëren bij het aanmaken van de app en het later initialiseren in create_app() met de parameters die we in app.config hebben opgegeven. Om onze app te draaien wijzigen we het run.py bestand. We geven nu het object Celery door aan de functie create_app() , zoiets als:

#  run.py

import app
from app import factory

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

Zie voor details het hierboven genoemde artikel.

Celery en tijdzones

Ik dacht dat ik dit ook vermeldde omdat ik er moeite mee had. Ik heb geprobeerd de tijdzone voor zowel Flask als Celery in te stellen op iets anders dan UTC. Voor tijdzone 'Europa/Amsterdam' bleef ik het bericht krijgen (van Flower):

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

Ik heb dit niet opgelost, maar gelukkig is dit niet echt een probleem. Het is een goede gewoonte om een webapplicatie met tijdzone UTC te draaien en alleen naar een lokale tijdzone te converteren wanneer daarom wordt gevraagd, bijvoorbeeld bij het tonen van een datum en tijd aan een bezoeker. Om problemen te voorkomen, gebruikt u UTC overal!

Gebruik Docker

Ik gebruik Docker voor ontwikkeling, testen, staging en productie. Omdat mijn productieserver ISPConfig met een MariaDB database draait en ik ook een MariaDB database op mijn ontwikkelingssysteem heb geïnstalleerd, heb ik geen database toegevoegd aan mijn Docker configuratie maar in plaats daarvan verbinding gemaakt met de MariaDB database via een unix socket.

Ik heb een gedeeld docker-compose bestand, docker-compose_shared.yml, en docker-compose bestanden voor deployment opties, docker-compose_development.yml, docker-compose_production.yml, ... En elke inzetoptie heeft een eigen omgevingsbestand.

Om de ontwikkeling te starten draai ik:

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

En om de productie te starten draai ik:

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

Met dezelfde Docker image voor de web-service en de celery-worker

Voordat ik Celery toevoegde, had ik maar één dienst: web. Met Celery heb ik er minstens drie meer:

  • Redis
  • Een of meer werknemers
  • Flower

Redis en Flower zijn triviaal om toe te voegen, maar hoe voegen we een arbeider toe? Na het lezen op het internet heb ik besloten dat de werker hetzelfde beeld moet hebben als het web-beeld. Natuurlijk is er veel overhead (dode code) hier, maar we maken ons leven ook makkelijker omdat we werkende code kunnen gebruiken die we eerder hebben geschreven en getest.

Bij het bouwen van een afbeelding met docker-compose wordt de afbeelding voor web-service gebouwd. Bij het draaien met docker-compose moeten zowel de web-service als de celery-worker-service deze afbeelding gebruiken.

Hieronder staat het bestand docker-compose_shared.yml . Ik laat alleen de belangrijke regels zien. Enkele variabelen in het bestand .env voor productie:

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"
    ...

Het bestand 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"

En het bestand 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

Handmatig starten en stoppen van de werknemer tijdens de ontwikkeling

Bij de ontwikkeling van een Flask applicatie gebruiken we de optie DEBUG . Vervolgens start de applicatie automatisch opnieuw op wanneer we wijzigingen aanbrengen zoals het opslaan van een bestand.

De werker is een apart programma, als het draait weet het niet dat je de code hebt veranderd. Dit betekent dat we moeten stoppen en de werker moeten starten na het maken van wijzigingen aan een taak. In het docker-compose_shared.yml bestand staat een commando dat de werker start. Maar in het ontwikkelingsbestand overrulen we dit commando met behulp van:

    command: echo "do not run"

Tijdens de ontwikkeling open ik een terminalvenster en start ik alles op met 'docker-compose up'. Nu kan ik alle (debug)berichten en problemen in de applicatie zien.

De werker is niet gestart. In een ander terminalvenster voer ik de werknemer Docker container in met 'docker-compose run sh'. Merk op dat ik hier ook 'docker compose exec sh' kan gebruiken.

Ik heb al een shell script start_workers.sh gemaakt in mijn project directory:

#  start_workers.sh

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

Nu hoef ik alleen nog maar in de container-shell te typen om de werker te starten:

./start_workers.sh

en Celery start.

Om Celery te stoppen druk ik gewoon op CTRL-C. Reactie:

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

worker:  Warm shutdown  (MainProcess)

Hier hebben we de optie om alle lopende taken te beëindigen.

Het is ook mogelijk om een sierlijke afsluiting van de werknemer te doen. Ga naar het terminalvenster van de werker en typ CTRL-Z om de werker te stoppen. Reactie:

[1]+   Stopped                 ./start_workers.sh

Typ dan:

kill %1

Nu antwoordt Celery met (gevolgd door het bericht van het afsluitproces):

worker:  Warm shutdown  (MainProcess)

[1]+   Terminated              ./start_workers.sh

Celery werknemer geheugengebruik

De gepresenteerde oplossing is niet de meest geheugenvriendelijke oplossing, maar het is ook niet zo slecht. Om een indicatie te krijgen gebruiken we het commando Docker :

docker stats

Reactie:

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

Ik heb de web-service gestart met 5 Gunicorn medewerkers. Er is één Celery werker met twee draden (--concurrency=2). De Celery werker neemt hier ongeveer 120 MB in beslag. Natuurlijk zal het geheugengebruik in sommige gevallen toenemen, maar zolang de meeste taken niet erg geheugenintensief zijn denk ik niet dat het de moeite waard is om de code van de Celery werker te strippen.

Samenvatting

Het gebruik van Docker met Docker-compose is niet alleen geweldig omdat we een (bijna) identiek systeem hebben voor ontwikkeling en productie. Het is ook zeer eenvoudig om diensten als Redis en Flower toe te voegen. En het gebruik van dezelfde Docker image voor onze applicatie en de Celery medewerker is ook zeer eenvoudig met Docker-compose. Zoals altijd is er een keerzijde: het kost tijd om dit in te stellen. Maar het resultaat maakt veel goed.

Links / credits

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

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen (1)

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.

avatar

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