Utilizzo di Celery con Django per l'elaborazione delle attività in background

Le applicazioni Web di solito iniziano in modo semplice ma possono diventare piuttosto complesse e la maggior parte di esse supera rapidamente la responsabilità di rispondere solo alle richieste HTTP.

Quando ciò accade, bisogna fare una distinzione tra ciò che deve accadere all'istante (di solito nel ciclo di vita della richiesta HTTP) e cosa può accadere alla fine. Perché? Bene, perché quando la tua applicazione si sovraccarica di traffico, cose semplici come questa fanno la differenza. 

Le operazioni in un'applicazione Web possono essere classificate come operazioni critiche o richieste in tempo reale e attività in background, quelle che si verificano al di fuori del tempo richiesto. Queste mappe sono quelle sopra descritte: 

  • deve avvenire immediatamente: operazioni su richiesta
  • deve succedere alla fine: attività in background

Le operazioni in fase di richiesta possono essere eseguite su un singolo ciclo di richiesta / risposta senza preoccuparsi del fatto che l'operazione scada o che l'utente possa avere un'esperienza negativa. Esempi comuni includono operazioni di database CRUD (creazione, lettura, aggiornamento, eliminazione) e gestione utenti (routine di login / disconnessione).

Le attività in background sono diverse in quanto di solito richiedono molto tempo e sono soggette a errori, principalmente a causa di dipendenze esterne. Alcuni scenari comuni tra le applicazioni Web complesse includono:

  • invio di e-mail di conferma o attività
  • giornalmente strisciare e raschiare alcune informazioni da varie fonti e memorizzarle
  • eseguire analisi dei dati
  • eliminazione di risorse non necessarie
  • esportazione di documenti / foto in vari formati

Le attività in background sono l'obiettivo principale di questo tutorial. Il modello di programmazione più comune utilizzato per questo scenario è Producer Consumer Architecture. 

In termini semplici, questa architettura può essere descritta in questo modo: 

  • I produttori creano dati o attività.
  • Le attività vengono inserite in una coda definita come la coda delle attività. 
  • I consumatori sono responsabili per il consumo dei dati o l'esecuzione delle attività. 

Di solito, i consumatori recuperano le attività dalla coda in modalità FIFO (First-in-first-out) o in base alle loro priorità. Anche i consumatori sono indicati come lavoratori, e questo è il termine che useremo per tutto, poiché è coerente con la terminologia utilizzata dalle tecnologie discusse.

Che tipo di attività possono essere elaborate in background? Compiti che:

  • non sono essenziali per la funzionalità di base dell'applicazione web
  • non può essere eseguito nel ciclo richiesta / risposta poiché sono lenti (I / O intensivo, ecc.)
  • dipende da risorse esterne che potrebbero non essere disponibili o non comportarsi come previsto
  • potrebbe essere necessario riprovare almeno una volta
  • devono essere eseguiti su un programma

Celery è la scelta di fatto per l'elaborazione di attività in background nell'ecosistema Python / Django. Ha un'API semplice e chiara e si integra magnificamente con Django. Supporta varie tecnologie per la coda di attività e vari paradigmi per i lavoratori.

In questo tutorial creeremo un'applicazione web di giocattoli Django (che si occupa di scenari del mondo reale) che utilizza l'elaborazione di attività in background.

Impostare le cose

Supponendo che tu abbia già familiarità con la gestione dei pacchetti Python e gli ambienti virtuali, installiamo Django:

$ pip installa Django

Ho deciso di creare un'altra applicazione di blogging. Il focus dell'applicazione sarà sulla semplicità. Un utente può semplicemente creare un account e senza troppa confusione può creare un post e pubblicarlo sulla piattaforma. 

Configura il quick_publisher Progetto Django:

$ django-admin startproject quick_publisher

Iniziamo l'app:

$ cd quick_publisher $ ./manage.py startapp main

Quando si avvia un nuovo progetto Django, mi piace creare un principale applicazione che contiene, tra le altre cose, un modello utente personalizzato. Più spesso, ho riscontrato limitazioni del Django predefinito Utente modello. Avere una consuetudine Utente il modello ci dà il vantaggio della flessibilità.

# main / models.py dai modelli di importazione django.db da django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, class BaseUserManager UserAccountManager (BaseUserManager): use_in_migrations = True def _create_user (self, email, password, ** extra_fields): if non email: raise ValueError ('Indirizzo email deve essere fornito') se non password: genera ValueError ('Password deve essere fornita') email = self.normalize_email (email) user = self.model (email = email, ** extra_fields) user.set_password (password) user.save (using = self._db) return user def create_user (self, email = None, password = None, ** extra_fields): return self._create_user (email, password, ** extra_fields) def create_superuser (self, email, password, ** extra_fields): extra_fields ['is_staff'] = True extra_fields ['is_superuser'] = True return self._create_user (email, password, ** extra_fields) class Utente (AbstractBaseUser, PermissionsMixin): REQUIRED_FIELDS = [] USERNAME_FIELD = oggetti 'email' = UserAccountManager () email = models.EmailField ('email', unique = True, blank = False, null = False) full_name = models.CharField ('nome completo', blank = True, null = True, max_length = 400) is_staff = models.BooleanField ('stato personale') , default = False) is_active = models.BooleanField ('active', default = True) def get_short_name (self): return self.email def get_full_name (self): return self.email def __unicode __ (self): return self.email

Assicurati di controllare la documentazione di Django se non hai familiarità con il funzionamento dei modelli utente personalizzati.

Ora dobbiamo dire a Django di usare questo modello utente invece di quello predefinito. Aggiungi questa linea al quick_publisher / settings.py file:

AUTH_USER_MODEL = 'main.User' 

Abbiamo anche bisogno di aggiungere il principale applicazione al INSTALLED_APPS lista in quick_publisher / settings.py file. Ora possiamo creare le migrazioni, applicarle e creare un superutente per poter accedere al pannello di amministrazione di Django:

$ ./manage.py makemigrations main $ ./manage.py migrare $ ./manage.py creauperuser

Creiamo ora un'applicazione Django separata che è responsabile per i post:

$ ./manage.py startapp publish

Definiamo un semplice modello Post in Editore / models.py:

da django.db importare modelli dal fuso orario di importazione di django.utils da django.contrib.auth import classe get_user_model Post (models.Model): author = models.ForeignKey (get_user_model ()) created = models.DateTimeField ('Data di creazione', default = timezone.now) title = models.CharField ('Title', max_length = 200) content = models.TextField ('Content') slug = models.SlugField ('Slug') def __str __ (self): return '"% s "di% s '% (self.title, self.author)

Agganciare il Inviare il modello con l'admin di Django è fatto nel Editore / admin.py file come questo:

da django.contrib import admin da .models import Post @ admin.register (Post) classe PostAdmin (admin.ModelAdmin): pass

Finalmente, agganciamo il editore applicazione con il nostro progetto aggiungendolo al INSTALLED_APPS elenco.

Ora possiamo eseguire il server e dirigerci verso http: // localhost: 8000 / admin / e crea i nostri primi post in modo da avere qualcosa con cui giocare:

$ ./manage.py runserver

Credo che tu abbia fatto i compiti e tu abbia creato i post. 

Andiamo avanti. Il prossimo passo ovvio è creare un modo per visualizzare i post pubblicati. 

# publisher / views.py da django.http importa Http404 da django.shortcuts import rendering da .models import Post def view_post (request, slug): prova: post = Post.objects.get (slug = slug) tranne Post.DoesNotExist: solleva Http404 ("Sondaggio inesistente") return render (request, 'post.html', context = 'post': post)

Associamo la nostra nuova vista con un URL in: quick_publisher / urls.py

# quick_publisher / urls.py dall'URL di importazione django.conf.urls da django.contrib import admin da publish.views import view_post urlpatterns = [url (r '^ admin /', admin.site.urls), url (r '^ (? P[a-zA-Z0-9 \ -] +) ', view_post, name = "view_post")]

Infine, creiamo il modello che esegue il rendering del post in: Editore / templates / post.html

       

titolo del post

post.content

Pubblicato da post.author.full_name su post.created

Ora possiamo andare a http: // localhost: 8000 / the-slug-of-the-post-creato / nel browser. Non è esattamente un miracolo del web design, ma creare post di bell'aspetto va oltre lo scopo di questo tutorial.

Invio di e-mail di conferma

Ecco lo scenario classico:

  • Crei un account su una piattaforma.
  • Fornisci un indirizzo email per essere identificato in modo univoco sulla piattaforma.
  • La piattaforma controlla che tu sia effettivamente il proprietario dell'indirizzo email inviando un'email con un link di conferma.
  • Fino a quando non si esegue la verifica, non è possibile utilizzare (completamente) la piattaforma.

Aggiungiamo un is_verified bandiera e il verification_uuid sul Utente modello:

# main / models.py import Uuid class Utente (AbstractBaseUser, PermissionsMixin): REQUIRED_FIELDS = [] USERNAME_FIELD = 'email' objects = UserAccountManager () email = models.EmailField ('email', unique = True, blank = False, null = False) full_name = models.CharField ('nome completo', blank = True, null = True, max_length = 400) is_staff = models.BooleanField ('staff status', default = False) is_active = models.BooleanField ('active', default = True) is_verified = models.BooleanField ('verified', default = False) # Aggiungi il flag 'is_verified' verify_uuid = models.UUIDField ('UUID verifica unica', default = uuid.uuid4) def get_short_name (self): return self.email def get_full_name (self): return self.email def __unicode __ (self): return self.email

Usiamo questa occasione per aggiungere il modello User all'admin:

da django.contrib import admin da .models import User @ admin.register (User) classe UserAdmin (admin.ModelAdmin): pass

Facciamo riflettere le modifiche nel database:

$ ./manage.py makemigrations $ ./manage.py migrare

Ora dobbiamo scrivere una parte di codice che invia un'email quando viene creata un'istanza utente. Questo è ciò che i segnali di Django servono, e questa è un'occasione perfetta per toccare questo argomento. 

I segnali vengono attivati ​​prima / dopo determinati eventi si verificano nell'applicazione. Possiamo definire le funzioni di callback che vengono attivate automaticamente quando vengono emessi i segnali. Per attivare un callback, dobbiamo prima collegarlo a un segnale.

Creeremo un callback che verrà attivato dopo la creazione di un modello utente. Aggiungeremo questo codice dopo il Utente definizione del modello in: principale / models.py

da django.db.models importare segnali da django.core.mail import send_mail def user_post_save (mittente, istanza, segnale, * args, ** kwargs): se non instance.is_verified: # Invia email di verifica send_mail ('Verifica il tuo account QuickPublisher ',' Segui questo link per verificare il tuo account: "http: // localhost: 8000% s '% reverse (' verify ', kwargs = ' uuid ': str (instance.verification_uuid)),' da @ quickpublisher. dev ', [instance.email], fail_silently = False,) signals.post_save.connect (user_post_save, sender = Utente)

Quello che abbiamo fatto qui è che abbiamo definito a user_post_save funzione e collegato al post_save segnale (uno che viene attivato dopo che un modello è stato salvato) inviato dal Utente modello.

Django non invia solo e-mail da solo; ha bisogno di essere legato a un servizio di posta elettronica. Per semplicità, puoi aggiungere le tue credenziali di Gmail in quick_publisher / settings.py, oppure puoi aggiungere il tuo fornitore di posta elettronica preferito. 

Ecco come si presenta la configurazione di Gmail:

EMAIL_USE_TLS = True EMAIL_HOST = 'smtp.gmail.com' EMAIL_HOST_USER = '@ gmail.com 'EMAIL_HOST_PASSWORD =''EMAIL_PORT = 587

Per testare le cose, vai nel pannello di amministrazione e crea un nuovo utente con un indirizzo email valido che puoi controllare rapidamente. Se tutto è andato bene, riceverai un'email con un link di verifica. La routine di verifica non è ancora pronta. 

Ecco come verificare l'account:

# main / views.py da django.http importa Http404 da django.shortcuts importazione render, reindirizzamento da .models import User def home (richiesta): return render (request, 'home.html') def verify (request, uuid): provare: utente = User.objects.get (verify_uuid = uuid, is_verified = False) tranne User.DoesNotExist: genera Http404 ("L'utente non esiste o è già verificato") user.is_verified = True user.save () return redirect ( 'casa')

Agganciare le viste in: quick_publisher / urls.py

# quick_publisher / urls.py dall'URL di importazione django.conf.urls da django.contrib import admin da editore.viste import view_post da main.views import home, verifica urlpatterns = [url (r '^ $', home, name = "" home "), url (r '^ admin /', admin.site.urls), url (r '^ verify / (? P[a-z0-9 \ -] +) / ', verify, name = "verify"), url (r' ^ (? P[a-zA-Z0-9 \ -] +) ', view_post, name = "view_post")]

Inoltre, ricorda di creare un home.html file sotto principale / templates / home.html. Sarà reso dal casa vista.

Prova a eseguire di nuovo l'intero scenario. Se tutto va bene, riceverai un'email con un URL di verifica valido. Se segui l'URL e poi accedi all'amministratore, puoi vedere come è stato verificato l'account.

Invio di email in modo asincrono

Ecco il problema con ciò che abbiamo fatto finora. Avrai notato che creare un utente è un po 'lento. Questo perché Django invia l'email di verifica entro il tempo richiesto. 

Ecco come funziona: inviamo i dati dell'utente all'applicazione Django. L'applicazione crea a Utente modella e quindi crea una connessione a Gmail (o un altro servizio selezionato). Django attende la risposta e solo allora restituisce una risposta al nostro browser. 

Qui è dove entra in gioco Celery. Innanzitutto, assicurati che sia installato:

$ pip installa Celery

Ora dobbiamo creare un'applicazione Celery nella nostra applicazione Django:

# quick_publisher / celery.py import os da Celery import Celery os.environ.setdefault ('DJANGO_SETTINGS_MODULE', 'quick_publisher.settings') app = Celery ('quick_publisher') app.config_from_object ('django.conf: settings') # Load moduli di attività da tutte le configurazioni di app Django registrate. app.autodiscover_tasks ()

Celery è una coda di attività. Riceve attività dalla nostra applicazione Django e le eseguirà in background. Il sedano deve essere associato ad altri servizi che fungono da intermediari. 

I broker intermediano l'invio di messaggi tra l'applicazione Web e Celery. In questo tutorial, useremo Redis. Redis è facile da installare e possiamo facilmente iniziare senza troppa confusione.

È possibile installare Redis seguendo le istruzioni sulla pagina Avvio rapido di Redis. Avrai bisogno di installare la libreria Redis Python, pip install redis, e il pacchetto necessario per l'utilizzo di Redis e Celery: pip installa sedano [redis].

Avvia il server Redis in una console separata come questa: $ redis-server

Aggiungiamo le configurazioni relative a Celery / Redis in quick_publisher / settings.py:

# REDIS related settings REDIS_HOST = 'localhost' REDIS_PORT = '6379' BROKER_URL = 'redis: //' + REDIS_HOST + ':' + REDIS_PORT + '/ 0' BROKER_TRANSPORT_OPTIONS = 'visibility_timeout': 3600 CELERY_RESULT_BACKEND = 'redis: / / '+ REDIS_HOST +': '+ REDIS_PORT +' / 0 '

Prima che tutto possa essere eseguito in Celery, deve essere dichiarato come attività. 

Ecco come fare:

# main / tasks.py import logging da django.urls import reverse da django.core.mail import send_mail from django.contrib.auth import get_user_model dall'app di importazione quick_publisher.celery @ app.task def send_verification_email (user_id): UserModel = get_user_model ( ) try: user = UserModel.objects.get (pk = user_id) send_mail ('Verifica il tuo account QuickPublisher', 'Segui questo link per verificare il tuo account: "http: // localhost: 8000% s'% reverse ('verify') , kwargs = 'uuid': str (user.verification_uuid)), '[email protected]', [user.email], fail_silently = False,) tranne UserModel.DoesNotExist: logging.warning ("Tentativo di inviare la verifica email a utente non esistente '% s' "% user_id)

Quello che abbiamo fatto qui è questo: abbiamo spostato la funzionalità di email di verifica dell'invio in un altro file chiamato tasks.py

Alcune note:

  • Il nome del file è importante. Celery passa attraverso tutte le app in INSTALLED_APPS e registra i compiti in tasks.py File.
  • Notate come abbiamo decorato il Invia email di verifica funzione con @ app.task. Questo dice a Celery che questa è un'attività che verrà eseguita nella coda dei task.
  • Nota come ci aspettiamo come argomento ID utente piuttosto che a Utente oggetto. Questo perché potremmo avere problemi nel serializzare oggetti complessi quando si inviano le attività a Celery. È meglio mantenerli semplici.

Tornando a principale / models.py, il codice del segnale si trasforma in:

da django.db.models import segnali da main.tasks import send_verification_email def user_post_save (mittente, istanza, segnale, * args, ** kwargs): se non instance.is_verified: # Invia email di verifica send_verification_email.delay (instance.pk) segnali .post_save.connect (user_post_save, mittente = utente)

Nota come chiamiamo il .ritardo metodo sull'oggetto compito. Ciò significa che stiamo inviando l'incarico a Celery e non aspettiamo il risultato. Se abbiamo usato send_verification_email (instance.pk) invece, lo manderemmo comunque a Celery, ma aspetteremmo che il compito finisse, che non è quello che vogliamo.

Prima di iniziare a creare un nuovo utente, c'è un problema. Il sedano è un servizio e dobbiamo avviarlo. Apri una nuova console, assicurati di attivare la virtualenv appropriata e accedi alla cartella del progetto.

$ sedano lavoratore -A quick_publisher --loglevel = debug --concurrency = 4

Questo avvia quattro processisti di Celery. Sì, ora puoi finalmente andare e creare un altro utente. Nota come non c'è ritardo e assicurati di guardare i log nella console di Celery e vedere se le attività sono state eseguite correttamente. Questo dovrebbe assomigliare a questo:

[2017-04-28 15: 00: 09,190: DEBUG / MainProcess] Attività accettata: main.tasks.send_verification_email [f1f41e1f-ca39-43d2-a37d-9de085dc99de] pid: 62065 [2017-04-28 15: 00: 11,740: INFO / PoolWorker-2] Task main.tasks.send_verification_email [f1f41e1f-ca39-43d2-a37d-9de085dc99de] riuscito in 2.5500912349671125s: Nessuno

Attività periodiche con il sedano

Ecco un altro scenario comune. Le applicazioni web più mature inviano le loro email al ciclo di vita degli utenti per mantenerle coinvolte. Alcuni esempi comuni di e-mail del ciclo di vita:

  • rapporti mensili
  • notifiche di attività (Mi piace, richieste di amicizia, ecc.)
  • promemoria per eseguire determinate azioni ("Non dimenticare di attivare il tuo account")

Ecco cosa faremo nella nostra app. Conteremo quante volte ogni post è stato visualizzato e inviamo un rapporto giornaliero all'autore. Una volta ogni giorno, passeremo attraverso tutti gli utenti, recupereremo i loro post e inviamo un'email con una tabella contenente i post e i conteggi delle visualizzazioni.

Cambiamo il Inviare modello in modo da poter accogliere lo scenario dei conteggi di vista.

class Post (models.Model): author = models.ForeignKey (User) created = models.DateTimeField ('Created Date', default = timezone.now) title = models.CharField ('Title', max_length = 200) content = models .TextField ('Content') slug = models.SlugField ('Slug') view_count = models.IntegerField ("Visualizza numero", default = 0) def __str __ (self): restituisce '"% s" di% s'% ( self.title, self.author)

Come sempre, quando cambiamo un modello, dobbiamo migrare il database:

$ ./manage.py makemigrations $ ./manage.py migrare

Modifichiamo anche il view_post Vista Django per contare le visualizzazioni:

def view_post (request, slug): prova: post = Post.objects.get (slug = slug) tranne Post.DoesNotExist: genera Http404 ("Sondaggio inesistente") post.view_count + = 1 post.save () restituisce il rendering (richiesta, 'post.html', context = 'post': post)

Sarebbe utile visualizzare il view_count nel modello. Aggiungi questo 

Visto post.view_count volte

 da qualche parte dentro il Editore / templates / post.html file. Esegui ora alcune visualizzazioni su un post e osserva come aumenta il contatore.

Creiamo un compito di Celery. Dato che si tratta di post, ho intenzione di inserirlo Editore / tasks.py:

da django.template import Template, Context from django.core.mail import send_mail from django.contrib.auth import get_user_model dall'app di importazione quick_publisher.celery da publisher.models import Post REPORT_TEMPLATE = "" "Ecco come hai fatto fino ad ora: % per post nei post% "post.title": visualizzato post.view_count volte | % endfor% "" "@ app.task def send_view_count_report (): per utente in get_user_model (). objects.all (): posts = Post.objects.filter (autore = utente) se non post: continua template = Template (REPORT_TEMPLATE) send_mail ('Your QuickPublisher Activity', template.render (context = Context ('posts': post)), '[email protected]', [user.email], fail_silently = False,)

Ogni volta che apporti modifiche alle attività di Celery, ricorda di riavviare il processo di Celery. Il sedano ha bisogno di scoprire e ricaricare i compiti. Prima di creare un'attività periodica, dovremmo verificarlo nella shell di Django per assicurarci che tutto funzioni come previsto:

$ ./manage.py shell In [1]: da editore.tasks import send_view_count_report In [2]: send_view_count_report.delay ()

Spero che tu abbia ricevuto una piccola relazione nella tua email. 

Creiamo ora un'attività periodica. Aprire quick_publisher / celery.py e registrare i compiti periodici:

# quick_publisher / celery.py import os da Celery import Celery da celery.schedules import crontab os.environ.setdefault ('DJANGO_SETTINGS_MODULE', 'quick_publisher.settings') app = Celery ('quick_publisher') app.config_from_object ('django.conf : settings ') # Carica i moduli di attività da tutte le configurazioni di app Django registrate. app.autodiscover_tasks () app.conf.beat_schedule = 'send-report-every-single-minute': 'task': 'publisher.tasks.send_view_count_report', 'schedule': crontab (), # cambia in 'crontab (minuto = 0, ora = 0) 'se vuoi che funzioni giornalmente a mezzanotte,

Finora, abbiamo creato un programma che avrebbe eseguito il compito publisher.tasks.send_view_count_report ogni minuto come indicato dal crontab () notazione. Puoi anche specificare vari programmi di Celery Crontab. 

Apri un'altra console, attiva l'ambiente appropriato e avvia il servizio Celery Beat. 

$ sedano-Un battitore quick_publisher

Il lavoro del servizio Beat è quello di spingere le attività in Celery secondo il programma. Tieni conto che il programma rende il send_view_count_report l'attività viene eseguita ogni minuto in base all'impostazione. È utile per i test, ma non è consigliato per un'applicazione web reale.

Rendere le attività più affidabili

Le attività vengono spesso utilizzate per eseguire operazioni inaffidabili, operazioni che dipendono da risorse esterne o che possono facilmente fallire a causa di vari motivi. Ecco una linea guida per renderli più affidabili:

  • Rendi le attività idempotenti. Un task idempotente è un'attività che, se interrotta a metà strada, non modifica in alcun modo lo stato del sistema. L'attività apporta modifiche complete al sistema o nessuna.
  • Riprovare le attività. Se l'attività fallisce, è una buona idea provarla ancora e ancora fino a quando non viene eseguita correttamente. Puoi farlo in Celery con Celery Retry. Un'altra cosa interessante da osservare è l'algoritmo di Backoff Esponenziale. Questo potrebbe rivelarsi utile quando si pensa di limitare il carico non necessario sul server dalle attività ripetute.

conclusioni

Spero che questo sia stato un tutorial interessante per te e una buona introduzione all'uso di Celery con Django. 

Ecco alcune conclusioni che possiamo trarre:

  • È buona norma mantenere attività inaffidabili e dispendiose in termini di tempo al di fuori del tempo richiesto.
  • Le attività a esecuzione prolungata devono essere eseguite in background dai processi di lavoro (o da altri paradigmi).
  • Le attività in background possono essere utilizzate per varie attività che non sono fondamentali per il funzionamento di base dell'applicazione.
  • Celery può anche gestire attività periodiche usando il battito di sedano servizio.
  • Le attività possono essere più affidabili se rese idempotenti e riprovate (magari usando il backoff esponenziale).