Celery e Django: creazione di tasks asincroni

Al giorno d'oggi, il processamento e l'analisi dei dati viene sempre più richiesto all'interno delle applicazioni web. Purtroppo il tempo di esecuzione richiesto a volte può essere troppo grande per gestire le richieste in modo asincrono. In questo tutorial scopriamo come utilizzare Celery in un progetto Django per creare dei task asincroni per le nostre esigenz

Share

Condividi su facebook
Condividi su linkedin
Condividi su twitter
Condividi su email
Reading time: 8 minutes

Le applicazioni web richiedono al giorno d’oggi di essere sempre più veloci per rispondere alle esigenze degli utenti. Esistono degli studi che dimostrano che un tempo di caricamento di un sito superiore a 4 secondi può ridurre notevolmente il coinvolgimento degli utenti e di conseguenza gli introiti di un sito. Pensate ad esempio quando usate un e-commerce. Se la navigazione è complessa e il caricamento delle pagine è lento, non sarete invogliati a comprare su quella piattaforma a meno che non sia strettamente necessario!

Nonostante sia richiesta una velocità sempre maggiore, la mole di dati da processare aumenta man mano. I calcoli che si devono fare a volte richiedono molti secondi, se non addirittura minuti e ore. Non è, quindi, pensabile gestire alcuni calcoli in modo sincrono. Si deve utilizzare un modello distribuito che ottimizzi le prestazioni e che allo stesso tempo non sia bloccante per l’applicazione principale.

Quando per necessità si devono impostare dei tasks asincroni e distribuiti e programmate in Python, il framework predominante è Celery.

Celery è un pacchetto che implementa il modello di coda di messaggi per il calcolo distribuito su uno o più nodi sfruttando l’Advanced Message Queuing Protocol (AMQP), un protocollo open standard a livello applicativo per lo sviluppo di un middleware orientato ai messaggi.

Celery non è solo un framework per eseguire tasks asincroni e distribuiti, ma può essere anche utilizzato per diversi altri scopi. Ad esempio è possibile pianificare l’esecuzione di alcuni tasks in modo analogo a cron, oppure mediante celery chords eseguire task distribuiti in stile map-reduce su grandi moli di dati.

In questo articolo vedremo come configurare l’ultima versione di Celery 5.x sia in modalità standalone che all’interno delle applicazioni Django.

Prerequisiti

Per rendere il lavoro altamente trasportabile da una postazione ad un’altra useremo Docker e Docker Compose. Consigliamo quindi di leggere gli articoli Introduzione a DockerDocker compose – come orchestrare diversi container per avere un’idea della tecnologia che utilizzeremo.

All’interno del nostro progetto Docker andremo a creare diversi containers. La versione di Python richiesta da Celery deve essere uguale o superiore alla 3.6. Bisogna anche scegliere un broker. Questo rappresenta il middleware che facilita la comunicazione tra i vari servizi Python in modo continuo e distribuito. È pertanto importante scegliere il giusto broker fin dall’inizio in base alle sue caratteristiche e al contesto in cui si opera.

I broker supportati da Celery 5.x sono i seguenti:

  • RabbitMQ è completo, stabile, durevole e facile da installare. Di solito è la scelta consigliata per un ambiente di produzione.
  • Redis fornisce le stesse funzionalità di RabbitMQ, ma è più suscettibile alla perdita di dati in caso di interruzione improvvisa o di mancanza di corrente.
  • AWS SQS è il servizio per la gestione delle code di messaggi fornito da Amazon. Nonostante ci sia la possibilità di avere il servizio gratuitamente entro certi limiti di utilizzi, la sua configurazione risulta leggermente più complessa. Si presta quindi per applicazioni basate su microservizi che richiedono un’alta affidabilità e un carico di lavoro elevato.

Oltre a questi è anche supportato in modo sperimentale Zookeeper. Poichè non c’è un supporto diretto di Celery si sconsiglia il suo utilizzo per ambienti di produzione.

Per semplificare le configurazioni, in questo tutorial useremo Redis. Nel caso vogliate cimentarvi con gli altri broker non dovrete che cambiare solamente i parametri di connessione e installare alcuni pacchetti qualora fossero necessari. Rimandiamo all guida ufficiale di Celery per le varie configurazioni dei brokers. Nel caso di Redis è possibile anche usare una versione cloud del servizio: redislab. Tra le varie opzioni esiste un pacchetto gratuito ideale per fare i test. 

Struttura del progetto Django

Prima di sviluppare il Docker è necessario creare una struttura di base del nostro progetto Django. Per farlo usiamo pipenv per creare un ambiente minimale python con i pacchetti essenziali per impostare la struttura del progetto. Iniziamo con il comando seguente comando:

pipenv shell 

Verrà quindi installato un piccolo ambiente python che possiamo configurare senza modificare le configurazioni di sistema. Per creare il progetto ci servirà solamente Django. Infatti tutto gli altri pacchetti saranno installati all’interno del Docker che ospiterà la nostra applicazione. Installiamo il pacchetto mediante il comando:

pip install Django 

Una volta terminata l’installazione possiamo procedere con creare il progetto e le app che ci servono. In questo tutorial chiameremo il progetto celerytutorial. Creiamo poi un’app chiamata bitcoins al cui interno andremo a definire un task per il recupero del prezzo dei bitcoins. Potete trovare tutti i comandi di Django nella documentazione ufficiale. Vi riportiamo di seguito quelli utilizzati per il nostro progetto.

django-admin startproject celerytutorial
cd celerytutorial
python manage.py startapp bitcoins 

All’interno della cartella del progetto andiamo ad aggiungere le cartelle _config, _static e _templates. Queste ci serviranno rispettivamente per indicare i pacchetti da installare per python e javascript, i file statici del nostro sito (css, javascript, immagini), e infine i template delle varie applicazioni. Potete usare, ovviamente, la struttura che più preferite.

Impostazione del Docker

In questo tutorial useremo una configurazione di un docker compose per lo sviluppo. Useremo quindi il server di Django per eseguire la nostra applicazione. Qualora si dovesse fornire l’applicazione in un ambiente di produzione, si dovrebbe aggiungere un web server come Nginx o Apache.

Il file del docker-compose da inserire nella cartella principale del progetto è il seguente.
version: '3'

services:
  celerytutorial:
    container_name: celerytutorial
    build: ./celerytutorial/
    entrypoint: ['sh', '/data/web/celerytutorial_setup.sh']
    volumes:
      - ./celerytutorial:/data/web
      - celerytutorial_static:/assets
      - celerytutorialstatus:/celerytutorial_status
    working_dir: /data/web
    restart: always
    ports:
        - "8000:8000"
    depends_on:
      - celerytutorialmongodb1
      

  celerytutorialmongodb3:
    image: mongo:4
    restart: always
    container_name: celerytutorialmongodb3
    volumes:
      - celerytutorialmongodata3:/data/db
    expose:
      - "27017"
    entrypoint: [ "/usr/bin/mongod", "--replSet", "rscelerytutorial", "--bind_ip_all", "--wiredTigerCacheSizeGB", "1" ]
    
  celerytutorialmongodb2:
    image: mongo:4
    restart: always
    container_name: celerytutorialmongodb2
    volumes:
      - celerytutorialmongodata2:/data/db
    expose:
      - "27017"
    entrypoint: [ "/usr/bin/mongod", "--replSet", "rscelerytutorial", "--bind_ip_all", "--wiredTigerCacheSizeGB", "1"]

  celerytutorialmongodb1:
    image: mongo:4
    restart: always
    container_name: celerytutorialmongodb1
    volumes:
      - celerytutorialmongodata1:/data/db
    expose:
      - "27017"
    ports:
      - "27018:27017"
    entrypoint: [ "/usr/bin/mongod", "--replSet", "rscelerytutorial", "--bind_ip_all", "--wiredTigerCacheSizeGB", "1"]
    
  celerytutorialmongosetup:
    image: "mongo-setup"
    build: "./mongo-setup"
    container_name: "celerytutorialmongosetup"
    depends_on:
      - celerytutorialmongodb1
    volumes:
      - celerytutorialstatus:/data/

  celerytutorialredis:
    container_name: "celerytutorialredis"
    image: redis:alpine
    expose:
      - "6379"
    restart: always
    sysctls:
      net.core.somaxconn: '4096' 
        
  celerycelerytutorial:
    container_name: "celerycelerytutorial"
    build: ./celerytutorial/
    command: celery -A celerytutorial worker -l INFO --concurrency=3 
    working_dir: /code
    volumes:
      - ./celerytutorial:/code
    depends_on:
      - celerytutorialmongodb1
      - celerytutorialredis

  
  celerytutorialflower:
    container_name: celerytutorialflower
    image: mher/flower
    command: ["flower", "--broker=redis://celerytutorialredis:6379/0", "--port=5555"]  
    ports:  
      - "5555:5555"


volumes:
    celerytutorial_static:
    celerytutorialmongodata1:
    celerytutorialmongodata2:
    celerytutorialmongodata3:
    celerytutorialstatus: 

Analizziamo nel dettaglio i vari containers. 

Il container chiamato celerytutorial è relativo all’applicazione sviluppata in Django. Il servizio sarà reso disponibile sulla porta 8000 del host.

Il build dell’immagine relativa è definito in un file Docker incluso nella directory del progetto Django. Questo Docker file usa l’approccio multi-stage build che permette di utilizzare, durante le varie fasi di installazione, immagini diverse. In particolare, usiamo l’immagine node per installare le librerie javascript e l’immagine python per il progetto Django. L’output dell’installazione delle librerie javascript viene copiato nell’immagine basata su Python. In questo modo non è necessario installare node nell’immagine finale, rendendo il tutto più snello.

Le librerie javascript e i pacchetti python utili al progetto sono definiti in file appositi all’interno della cartella _config/packaging del progetto Django.

Il contenuto del Docker file è riportato di seguito.

# Install node modules
FROM node AS build

RUN mkdir /assets
WORKDIR /assets
COPY _config/packaging/package.json /assets
RUN npm install


FROM python:3.7
ENV PYTHONUNBUFFERED=1

# Copy installed modules into this new container
COPY --from=build /assets /assets

RUN mkdir /data
WORKDIR /data
COPY _config/packaging/requirements.txt /data/
RUN pip install --upgrade pip
RUN pip install -r requirements.txt 

L’entrypoint del container Django è uno script bash che lancia il server di test di Django. Questo script può essere modificato per inizializzare il progetto. Ad esempio è possibile effettuare le migrazioni dei modelli e/o creare l’utente superuser. In caso di utilizzo del progetto in un ambiente di produzione, si possono inserire ulteriori comandi per lanciare l’applicazione mediante un demone (ad esempio gunicorn). La struttura del file utilizzata in questo tutorial è la seguente.

#!/bin/bash

echo 'Launching the celerytutorial container...\n\n'


if [ ! -f /celerytutorial_status/celerytutorial-init.flag ]; then
    echo "Init celerytutorial"

    mkdir -p /data/celerytutorial_status/
    touch /celerytutorial_status/celerytutorial-init.flag

else
    echo "celerytutorial already initialized"
fi

echo 'Starting development server'
python manage.py runserver 0.0.0.0:8000 

I container celerytutorailmongodbX sono le diverse istanze del replica set di MongoDB. In questo tutorial non useremo direttamente MongoDB per salvare i dati, ma ci tornerà utile nei prossimi articoli. 

Anche il container celerytutorialmongosetup è adibito alla configurazione del replica set di MongoDB. 

Per maggiori dettagli su come implementare il replica set in ambiente Docker vi rimandiamo all’articolo MongoDB e Docker – Come creare e configurare un replica set.

Celery ha bisogno di appoggiarsi di un broker per gestire la coda dei messaggi. Utilizziamo l’immagine di redis presente nel Docker Hub per definire il container celerytutorialredis.

Per lanciare Celery usiamo un altro container, celerycelerytutorial, basato sulla build del progetto Django. In questo modo si è protetti da eventuali malfunzionamenti di uno dei due container. In un ambiente di produzione si può anche ipotizzare di distribuire i container su differenti macchine per bilanciare il carico di lavoro mediante Docker swarm. L’entrypoint del container è il comando che lancia 3 workers di Celery in parallelo. 

Infine, aggiungiamo il servizio Flower. Questo servizio ci permette, mediante un’applicazione web, di monitorare i task eseguiti da Celery. Rimandiamo alla documentazione ufficiale per maggiori dettagli.

Configurazione di Django

L’integrazione di Celery all’interno di un progetto Django avviene mediante alcune modifiche al codice.

Per prima cosa bisogna aggiornare i settings del progetto Django per definire le connessioni con redis e i parametri di configurazione di celery. Avendo redis a disposizione possiamo anche usarlo per la cache di Django. Il codice da inserire è il seguente.

REDIS_HOST = 'redis://celerytutorialredis:6379'
CELERY_BROKER_URL = REDIS_HOST 
CELERY_RESULT_BACKEND = REDIS_HOST
CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'


CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://celerytutorialredis:6379/1", # use database #1 (.e.g., docker-compose exec redis redis-cli -n 1)
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    }
} 

Per definire l’applicazione Celery è necessario creare un file celery.py nella stessa cartella dei settings. All’interno del file si definisce l’importazione dei settings e il comando per far scoprire automaticamente i tasks definiti nelle varie app del progetto. Di seguito è riportato il contenuto del file.

import os

from celery import Celery


# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'celerytutorial.settings')

from django.conf import settings

app = Celery('celerytutorial')

# Using a string here means the worker will not have to
# pickle the object when using Windows.
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks() 

Per lanciare Celery automaticamente quando viene lanciato Django, bisogna aggiornare il file __init__.py nella cartella di dove c’è il file settings.py come segue.

from __future__ import absolute_import, unicode_literals
from .celery import app as celery_app

__all__ = ['celery_app'] 

Creazione di un task

Per definire un task è necessario creare un file tasks.py all’interno delle app del progetto Django. Nel nostro caso, scriviamo un task per recuperare i valori dei bitcoins forniti dal servizio coindesk.

Il task si collegherà all’api pubblica e restituirà un messaggio con il valore attuale dei bitcoins nella valuta richiesta. Di default la valuta è l’euro. Di seguito riportiamo il codice. 

rom celery import shared_task, group, chain
from celerytutorial.mongodb import db, to_json
import requests
import json
from celerytutorial.celery import app

@app.task
def getBitcoins(currency_code = 'EUR'):
    try:
        bpi_url = 'https://api.coindesk.com/v1/bpi/currentprice.json'
        response = requests.get(bpi_url)
        if response.status_code != 200:
            raise Exception(f'GET {bpi_url} returned unexpected response code: {response.status_code}')

        data = json.loads(response.content.decode('utf-8'))

        price_data = 'Latest Bitcoin Price {} {}'.format(currency_code, data.get('bpi').get(currency_code).get('rate_float'))

        return price_data
    except Exception as e:
        return f'Something went wrong: {e}' 

In questo modo sarà possibile richiamare il task da qualsiasi vista della nostra applicazione in modo asincrono.

Nei prossimi tutorial vedremo come modificare questo task per automatizzarlo e salvare i risultati in MongoDB.

Letture consigliate

More To Explore

Python

Celery e Django: creazione di tasks asincroni

Al giorno d’oggi, il processamento e l’analisi dei dati viene sempre più richiesto all’interno delle applicazioni web. Purtroppo il tempo di esecuzione richiesto a volte può essere troppo grande per gestire le richieste in modo asincrono. In questo tutorial scopriamo come utilizzare Celery in un progetto Django per creare dei task asincroni per le nostre esigenz

Neo4j

Neo4j: guida all’uso di un database a grafo

Neo4j è il leader dei database a grafo. Mediante le Cypher query è possibile gestire in modo ottimale una rappresentazione a grafo dei nostri dati e scoprire interessanti correlazioni tra di essi. Scopriamo come usarlo con alcuni semplici esempi di query e qualche suggerimento di caso d’uso avanzato.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Design with MongoDB

Design with MongoDB!!!

Buy the new book that will help you to use MongoDB correctly for your applications. Available now on Amazon!