Tareas asíncronas en Django con Redis y Celery

T

Introducción

En este tutorial, proporcionaré una comprensión general de por qué las colas de mensajes de apio son valiosas junto con cómo utilizar el apio junto con Redis en una aplicación Django. Para demostrar los detalles de la implementación, construiré una aplicación de procesamiento de imágenes minimalista que genera miniaturas de las imágenes enviadas por los usuarios.

Se cubrirán los siguientes temas:

  • Antecedentes de las colas de mensajes con apio y redis
  • Configuración del desarrollador local con Django, Celery y Redis
  • Creación de miniaturas de imágenes dentro de una tarea de apio
  • Implementar en un servidor Ubuntu

El código para este ejemplo se puede encontrar en GitHub junto con las instrucciones de instalación y configuración si solo desea saltar directamente a una aplicación funcionalmente completa; de lo contrario, durante el resto del artículo, lo guiaré a través de cómo construir todo desde cero.

Antecedentes de las colas de mensajes con apio y redis

Celery es un paquete de software de cola de tareas basado en Python que permite la ejecución de cargas de trabajo computacionales asíncronas impulsadas por información contenida en mensajes que se producen en el código de la aplicación (Django en este ejemplo) destinado a una cola de tareas de Celery. El apio también se puede utilizar para ejecutar tareas repetibles, periódicas (es decir, programadas), pero ese no será el enfoque de este artículo.

El apio se utiliza mejor junto con una solución de almacenamiento que a menudo se denomina agente de mensajes. Un corredor de mensajes común que se utiliza con el apio es Redis, que es un almacén de datos de valor clave en memoria de alto rendimiento. Específicamente, Redis se usa para almacenar mensajes producidos por el código de la aplicación que describen el trabajo a realizar en la cola de tareas de Celery. Redis también sirve como almacenamiento de los resultados de las colas de apio que luego son recuperados por los consumidores de la cola.

Configuración del desarrollador local con Django, Celery y Redis

Comenzaré con la parte más difícil primero, que es la instalación de Redis.

Instalación de Redis en Windows

  • Descargue el archivo zip de Redis y descomprímalo en algún directorio
  • Busque el archivo llamado redis-server.exe y haga doble clic para iniciar el servidor en una ventana de comandos
  • Del mismo modo, busque otro archivo llamado redis-cli.exe y haga doble clic en él para abrir el programa en una ventana de comando separada
  • Dentro de la ventana de comandos que ejecuta el cliente cli, pruebe para asegurarse de que el cliente puede hablar con el servidor emitiendo el comando pingy, si todo va bien, se PONGdebe devolver una respuesta de

Instalación de Redis en Mac OSX / Linux

  • Descargue el archivo tarball de Redis y extráigalo en algún directorio
  • Ejecute el archivo make con make installpara construir el programa
  • Abra una ventana de terminal y ejecute el redis-servercomando
  • En otra ventana de terminal, ejecuta redis-cli
  • Dentro de la ventana de la terminal que ejecuta el cliente cli, pruebe para asegurarse de que el cliente pueda hablar con el servidor emitiendo el comando pingy, si todo va bien, se PONGdebe devolver una respuesta de

Instale Python Virtual Env y las dependencias

Ahora puedo pasar a crear un entorno virtual Python3 e instalar los paquetes de dependencia necesarios para este proyecto.

Para comenzar, crearé un directorio para albergar cosas llamado image_parroter y luego dentro de él crearé mi entorno virtual. Todos los comandos de aquí en adelante serán solo del tipo Unix, pero la mayoría, si no todos, serán iguales para un entorno Windows.

$ mkdir image_parroter
$ cd image_parroter
$ python3 -m venv venv
$ source venv/bin/activate

Con el entorno virtual ahora activado, puedo instalar los paquetes de Python.

(venv) $ pip install Django Celery redis Pillow django-widget-tweaks
(venv) $ pip freeze > requirements.txt
  • Pillow es un paquete de Python no relacionado con el apio para el procesamiento de imágenes que usaré más adelante en este tutorial para demostrar un caso de uso del mundo real para las tareas del apio.
  • Django Widget Tweaks es un complemento de Django para proporcionar flexibilidad en cómo se procesan las entradas de formulario.

Configuración del proyecto Django

Continuando, creo un proyecto Django llamado image_parroter y luego una aplicación Django llamada thumbnailer.

(venv) $ django-admin startproject image_parroter
(venv) $ cd image_parroter
(venv) $ python manage.py startapp thumbnailer

En este punto, la estructura del directorio tiene el siguiente aspecto:

$ tree -I venv
.
└── image_parroter
    ├── image_parroter
    │   ├── __init__.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    ├── manage.py
    └── thumbnailer
        ├── __init__.py
        ├── admin.py
        ├── apps.py
        ├── migrations
        │   └── __init__.py
        ├── models.py
        ├── tests.py
        └── views.py

Para integrar Celery dentro de este proyecto de Django, agrego un nuevo módulo image_parroter / image_parrroter / celery.py siguiendo las convenciones descritas en los documentos de Celery . Dentro de este nuevo módulo de Python, importo el ospaquete y la Celeryclase del paquete de apio.

El osmódulo se usa para asociar una variable de entorno Celery llamada DJANGO_SETTINGS_MODULEcon el módulo de configuración del proyecto Django. Después de eso, creo una instancia de la Celeryclase para crear la celery_appvariable de instancia. Luego actualizo la configuración de la aplicación Celery con configuraciones que pronto colocaré en el archivo de configuración del proyecto Django identificable con un prefijo ‘CELERY_’. Finalmente, le digo a la celery_appinstancia recién creada que descubra automáticamente las tareas dentro del proyecto.

El módulo celery.py completo se muestra a continuación:

# image_parroter/image_parroter/celery.py

import os
from celery import Celery

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'image_parroter.settings')

celery_app = Celery('image_parroter')
celery_app.config_from_object('django.conf:settings', namespace="CELERY")
celery_app.autodiscover_tasks()

Ahora, en el módulo settings.py del proyecto, en la parte inferior, defino una sección para la configuración de apio y agrego la configuración que ve a continuación. Estas configuraciones le dicen a Celery que use Redis como el intermediario de mensajes, así como dónde conectarse a él. También le dicen a Celery que espere que los mensajes se pasen de un lado a otro entre las colas de tareas de Celery y el agente de mensajes de Redis para que estén en el tipo mime de application / json.

# image_parroter/image_parroter/settings.py

... skipping to the bottom

# celery
CELERY_BROKER_URL = 'redis://localhost:6379'
CELERY_RESULT_BACKEND = 'redis://localhost:6379'
CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TASK_SERIALIZER = 'json'

A continuación, necesito asegurarme de que la aplicación de apio creada y configurada previamente se inyecte en la aplicación Django cuando se ejecute. Esto se hace importando la aplicación Celery dentro del script principal __init__.py del proyecto Django y registrándolo explícitamente como un símbolo de espacio de nombres dentro del paquete “image_parroter” de Django.

# image_parroter/image_parroter/__init__.py

from .celery import celery_app

__all__ = ('celery_app',)

Continúo siguiendo las convenciones sugeridas agregando un nuevo módulo llamado tasks.py dentro de la aplicación “thumbnailer”. Dentro del módulo tasks.py, importo el shared_tasksdecorador de funciones y lo uso para definir una función de tarea de apio llamada adding_task, como se muestra a continuación.

# image_parroter/thumbnailer/tasks.py

from celery import shared_task

@shared_task
def adding_task(x, y):
    return x + y

Por último, necesito agregar la aplicación de miniaturas a la lista del INSTALLED_APPSmódulo settings.py del proyecto image_parroter. Mientras estoy allí, también debería agregar la aplicación “widget_tweaks” que se usará para controlar la representación de la entrada del formulario que usaré más adelante para permitir que los usuarios carguen archivos.

# image_parroter/image_parroter/settings.py

... skipping to the INSTALLED_APPS

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'thumbnailer.apps.ThumbnailerConfig',
    'widget_tweaks',
]

Ahora puedo probar cosas usando algunos comandos simples en tres terminales.

En una terminal necesito tener el servidor redis ejecutándose, así:

$ redis-server
48621:C 21 May 21:55:23.706 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
48621:C 21 May 21:55:23.707 # Redis version=4.0.8, bits=64, commit=00000000, modified=0, pid=48621, just started
48621:C 21 May 21:55:23.707 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
48621:M 21 May 21:55:23.708 * Increased maximum number of open files to 10032 (it was originally set to 2560).
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 4.0.8 (00000000/0) 64 bit
  .-`` .-```.  ```/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 48621
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

48621:M 21 May 21:55:23.712 # Server initialized
48621:M 21 May 21:55:23.712 * Ready to accept connections

En una segunda terminal, con una instancia activa del entorno virtual Python instalado previamente, en el directorio del paquete raíz del proyecto (el mismo que contiene el módulo manage.py) lanzo el programa celery.

(venv) $ celery worker -A image_parroter --loglevel=info
 
 -------------- [email protected] v4.3.0 (rhubarb)
---- **** ----- 
--- * ***  * -- Darwin-18.5.0-x86_64-i386-64bit 2019-05-22 03:01:38
-- * - **** --- 
- ** ---------- [config]
- ** ---------- .> app:         image_parroter:0x110b18eb8
- ** ---------- .> transport:   redis://localhost:6379//
- ** ---------- .> results:     redis://localhost:6379/
- *** --- * --- .> concurrency: 8 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** ----- 
 -------------- [queues]
                .> celery           exchange=celery(direct) key=celery
                

[tasks]
  . thumbnailer.tasks.adding_task

En la tercera y última terminal, nuevamente con el entorno virtual Python activo, puedo iniciar el shell Django Python y probar mi adding_task, así:

(venv) $ python manage.py shell
Python 3.6.6 |Anaconda, Inc.| (default, Jun 28 2018, 11:07:29) 
>>> from thumbnailer.tasks import adding_task
>>> task = adding_task.delay(2, 5)
>>> print(f"id={task.id}, state={task.state}, status={task.status}") 
id=86167f65-1256-497e-b5d9-0819f24e95bc, state=SUCCESS, status=SUCCESS
>>> task.get()
7

Tenga en cuenta el uso del .delay(...)método en el adding_taskobjeto. Esta es la forma común de pasar los parámetros necesarios al objeto de tarea con el que se está trabajando, así como de iniciar su envío al intermediario de mensajes y la cola de tareas. El resultado de llamar al .delay(...)método es un valor de retorno similar a una promesa del tipo celery.result.AsyncResult. Este valor de retorno contiene información como la identificación de la tarea, su estado de ejecución y el estado de la tarea junto con la capacidad de acceder a cualquier resultado producido por la tarea a través del .get()método como se muestra en el ejemplo.

Creación de miniaturas de imágenes dentro de una tarea de apio

Ahora que la configuración de la placa de la caldera para integrar una instancia de Celery respaldada por Redis en la aplicación Django está fuera del camino, puedo pasar a demostrar algunas funciones más útiles con la aplicación de miniaturas mencionada anteriormente.

De vuelta en el módulo tasks.py, importo la Imageclase del PILpaquete, luego agrego una nueva tarea llamada make_thumbnails, que acepta una ruta de archivo de imagen y una lista de dimensiones de ancho y alto de 2 tuplas para crear miniaturas.

# image_parroter/thumbnailer/tasks.py
import os
from zipfile import ZipFile

from celery import shared_task
from PIL import Image

from django.conf import settings

@shared_task
def make_thumbnails(file_path, thumbnails=[]):
    os.chdir(settings.IMAGES_DIR)
    path, file = os.path.split(file_path)
    file_name, ext = os.path.splitext(file)

    zip_file = f"{file_name}.zip"
    results = {'archive_path': f"{settings.MEDIA_URL}images/{zip_file}"}
    try:
        img = Image.open(file_path)
        zipper = ZipFile(zip_file, 'w')
        zipper.write(file)
        os.remove(file_path)
        for w, h in thumbnails:
            img_copy = img.copy()
            img_copy.thumbnail((w, h))
            thumbnail_file = f'{file_name}_{w}x{h}.{ext}'
            img_copy.save(thumbnail_file)
            zipper.write(thumbnail_file)
            os.remove(thumbnail_file)

        img.close()
        zipper.close()
    except IOError as e:
        print(e)

    return results

La tarea de miniaturas anterior simplemente carga el archivo de imagen de entrada en una instancia de Pillow Image, luego recorre la lista de dimensiones que se pasó a la tarea creando una miniatura para cada una, agregando cada miniatura a un archivo zip mientras también limpia los archivos intermedios. Se devuelve un diccionario simple que especifica la URL desde la que se puede descargar el archivo zip de miniaturas.

Con la tarea de apio definida, paso a construir las vistas de Django para ofrecer una plantilla con un formulario de carga de archivos.

Para comenzar, le doy al proyecto Django una MEDIA_ROOTubicación donde pueden residir los archivos de imagen y los archivos zip (usé esto en la tarea de ejemplo anterior) y especifico desde MEDIA_URLdónde se puede servir el contenido. En el módulo image_parroter / settings.py agrego el MEDIA_ROOT, MEDIA_URL, IMAGES_DIRlugares ajustes a continuación proporcionan la lógica para crear estas ubicaciones si no existen.

# image_parroter/settings.py

... skipping down to the static files section

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/

STATIC_URL = '/static/'
MEDIA_URL = '/media/'

MEDIA_ROOT = os.path.abspath(os.path.join(BASE_DIR, 'media'))
IMAGES_DIR = os.path.join(MEDIA_ROOT, 'images')

if not os.path.exists(MEDIA_ROOT) or not os.path.exists(IMAGES_DIR):
    os.makedirs(IMAGES_DIR)

Dentro del módulo thumbnailer / views.py, importo la django.views.Viewclase y la uso para crear una HomeViewclase que contiene métodos gety post, como se muestra a continuación.

El getmétodo simplemente devuelve una plantilla home.html, que se creará en breve, y le entrega FileUploadFormun ImageFieldcampo compuesto por un campo como se ve arriba de la HomeViewclase.

El postmétodo construye el FileUploadFormobjeto usando los datos enviados en la solicitud, verifica su validez, luego, si es válido, guarda el archivo cargado en el IMAGES_DIRe inicia una make_thumbnailstarea mientras toma la tarea idy el estado para pasar a la plantilla, o devuelve el formulario con su errores en la plantilla home.html.

# thumbnailer/views.py

import os

from celery import current_app

from django import forms
from django.conf import settings
from django.http import JsonResponse
from django.shortcuts import render
from django.views import View

from .tasks import make_thumbnails

class FileUploadForm(forms.Form):
    image_file = forms.ImageField(required=True)

class HomeView(View):
    def get(self, request):
        form = FileUploadForm()
        return render(request, 'thumbnailer/home.html', { 'form': form })
    
    def post(self, request):
        form = FileUploadForm(request.POST, request.FILES)
        context = {}

        if form.is_valid():
            file_path = os.path.join(settings.IMAGES_DIR, request.FILES['image_file'].name)

            with open(file_path, 'wb+') as fp:
                for chunk in request.FILES['image_file']:
                    fp.write(chunk)

            task = make_thumbnails.delay(file_path, thumbnails=[(128, 128)])

            context['task_id'] = task.id
            context['task_status'] = task.status

            return render(request, 'thumbnailer/home.html', context)

        context['form'] = form

        return render(request, 'thumbnailer/home.html', context)


class TaskView(View):
    def get(self, request, task_id):
        task = current_app.AsyncResult(task_id)
        response_data = {'task_status': task.status, 'task_id': task.id}

        if task.status == 'SUCCESS':
            response_data['results'] = task.get()

        return JsonResponse(response_data)

Debajo de la HomeViewclase, he colocado una TaskViewclase que se utilizará a través de una solicitud AJAX para verificar el estado de la make_thumbnailstarea. Aquí notará que importé el current_appobjeto del paquete de apio y lo usé para recuperar el AsyncResultobjeto de la tarea asociado con el task_idde la solicitud. Creo un response_datadiccionario del estado y la identificación de la tarea, luego, si el estado indica que la tarea se ha ejecutado correctamente, obtengo los resultados llamando al get()método del AsynchResultobjeto y asignándolo a la resultsclave del response_dataque se devolverá como JSON al solicitante HTTP.

Antes de que pueda crear la interfaz de usuario de la plantilla, necesito asignar las clases de vistas de Django anteriores a algunas URL sensibles. Comienzo agregando un módulo urls.py dentro de la aplicación de miniaturas y defino las siguientes URL:

# thumbnailer/urls.py

from django.urls import path

from . import views

urlpatterns = [
  path('', views.HomeView.as_view(), name="home"),
  path('task/<str:task_id>/', views.TaskView.as_view(), name="task"),
]

Luego, en la configuración de la URL principal del proyecto, necesito incluir las URL de nivel de la aplicación, así como también informar sobre la URL de los medios, así:

# image_parroter/urls.py

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('thumbnailer.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

A continuación, empiezo a crear una vista de plantilla simple para que un usuario envíe un archivo de imagen, así como para verificar el estado de las make_thumbnailstareas enviadas e iniciar una descarga de las miniaturas resultantes. Para empezar, necesito crear un directorio para albergar esta plantilla única dentro del directorio de miniaturas, de la siguiente manera:

(venv) $ mkdir -p thumbnailer/templates/thumbnailer

Luego, dentro de este directorio templates / thumbnailer agrego una plantilla llamada home.html. Dentro de home.html empiezo cargando las etiquetas de plantilla “widget_tweaks”, luego paso a definir el HTML importando un marco CSS llamado bulma CSS , así como una biblioteca JavaScript llamada Axios.js . En el cuerpo de la página HTML proporciono un título, un marcador de posición para mostrar un mensaje de resultados en progreso y el formulario de carga del archivo.

<!-- templates/thumbnailer/home.html -->
{% load widget_tweaks %}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Thumbnailer</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
  <script src="https://cdn.jsdelivr.net/npm/vue"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
  <script defer src="https://use.fontawesome.com/releases/v5.0.7/js/all.js"></script>
</head>
<body>
  <nav class="navbar" role="navigation" aria-label="main navigation">
    <div class="navbar-brand">
      <a class="navbar-item" href="/">
        Thumbnailer
      </a>
    </div>
  </nav>
  <section class="hero is-primary is-fullheight-with-navbar">
    <div class="hero-body">
      <div class="container">
        <h1 class="title is-size-1 has-text-centered">Thumbnail Generator</h1>
        <p class="subtitle has-text-centered" id="progress-title"></p>
        <div class="columns is-centered">
          <div class="column is-8">
            <form action="{% url 'home' %}" method="POST" enctype="multipart/form-data">
              {% csrf_token %}
              <div class="file is-large has-name">
                <label class="file-label">
                  {{ form.image_file|add_class:"file-input" }}
                  <span class="file-cta">
                    <span class="file-icon"><i class="fas fa-upload"></i></span>
                    <span class="file-label">Browse image</span>
                  </span>
                  <span id="file-name" class="file-name" 
                    style="background-color: white; color: black; min-width: 450px;">
                  </span>
                </label>
                <input class="button is-link is-large" type="submit" value="Submit">
              </div>
              
            </form>
          </div>
        </div>
      </div>
    </div>
  </section>
  <script>
  var file = document.getElementById('{{form.image_file.id_for_label}}');
  file.onchange = function() {
    if(file.files.length > 0) {
      document.getElementById('file-name').innerHTML = file.files[0].name;
    }
  };
  </script>

  {% if task_id %}
  <script>
  var taskUrl = "{% url 'task' task_id=task_id %}";
  var dots = 1;
  var progressTitle = document.getElementById('progress-title');
  updateProgressTitle();
  var timer = setInterval(function() {
    updateProgressTitle();
    axios.get(taskUrl)
      .then(function(response){
        var taskStatus = response.data.task_status
        if (taskStatus === 'SUCCESS') {
          clearTimer('Check downloads for results');
          var url = window.location.protocol + '//' + window.location.host + response.data.results.archive_path;
          var a = document.createElement("a");
          a.target="_BLANK";
          document.body.appendChild(a);
          a.style = "display: none";
          a.href = url;
          a.download = 'results.zip';
          a.click();
          document.body.removeChild(a);
        } else if (taskStatus === 'FAILURE') {
          clearTimer('An error occurred');
        }
      })
      .catch(function(err){
        console.log('err', err);
        clearTimer('An error occurred');
      });
  }, 800);

  function updateProgressTitle() {
    dots++;
    if (dots > 3) {
      dots = 1;
    }
    progressTitle.innerHTML = 'processing images ';
    for (var i = 0; i < dots; i++) {
      progressTitle.innerHTML += '.';
    }
  }
  function clearTimer(message) {
    clearInterval(timer);
    progressTitle.innerHTML = message;
  }
  </script> 
  {% endif %}
</body>
</html>

En la parte inferior del bodyelemento, agregué JavaScript para proporcionar un comportamiento adicional. Primero creo una referencia al campo de entrada del archivo y registro un oyente de cambios, que simplemente agrega el nombre del archivo seleccionado a la interfaz de usuario, una vez seleccionado.

Luego viene la parte más relevante. Utilizo el ifoperador lógico de plantilla de Django para verificar la presencia de un task_idser transmitido desde la HomeViewvista de clase. Esto indica una respuesta después de que make_thumbnailsse ha enviado una tarea. Luego uso la urletiqueta de plantilla de Django para construir una URL de verificación de estado de tarea apropiada y comenzar una solicitud AJAX temporizada por intervalo a esa URL usando la biblioteca Axios que mencioné anteriormente.

Si el estado de una tarea se informa como “ÉXITO”, inyecto un enlace de descarga en el DOM y hago que se active, lo que activa la descarga y borro el temporizador de intervalo. Si el estado es “FALLO”, simplemente borro el intervalo, y si el estado no es “ÉXITO” ni “FALLO”, no hago nada hasta que se invoca el siguiente intervalo.

En este punto, puedo abrir otra terminal, una vez más con el entorno virtual Python activo, e iniciar el servidor de desarrollo Django, como se muestra a continuación:

(venv) $ python manage.py runserver
  • Los terminales de tareas de redis-server y apio descritos anteriormente también deben estar ejecutándose, y si no ha reiniciado el trabajador de Apio desde que agregó la make_thumbnailstarea, querrá Ctrl+Cdetener el trabajador y luego celery worker -A image_parroter --loglevel=infovolver a ejecutarlo para reiniciarlo. Los trabajadores de apio deben reiniciarse cada vez que se realiza un cambio de código relacionado con la tarea de apio.

Ahora puedo cargar la vista home.html en mi navegador en http: // localhost: 8000 , enviar un archivo de imagen y la aplicación debería responder con un archivo results.zip que contiene la imagen original y una miniatura de 128×128 píxeles.

Implementar en un servidor Ubuntu

Para completar este artículo, mostraré cómo instalar y configurar esta aplicación Django que utiliza Redis y Celery para tareas en segundo plano asincrónicas en un servidor Ubuntu v18 LTS.

Una vez SSH en el servidor, lo actualizo y luego instalo los paquetes necesarios.

# apt-get update
# apt-get install python3-pip python3-dev python3-venv nginx redis-server -y

También hago un usuario llamado “webapp”, que me da un directorio de inicio para instalar el proyecto Django.

# adduser webapp

Después de ingresar los datos del usuario, agrego el usuario de la aplicación web a los grupos sudo y www-data, cambio al usuario de la aplicación web y luego cda su directorio de inicio.

# usermod -aG sudo webapp
# usermod -aG www-data webapp
$ su webapp
$ cd

Dentro del directorio de la aplicación web, puedo clonar el repositorio image_parroter GitHub, cden el repositorio, crear un entorno virtual de Python, activarlo y luego instalar las dependencias del archivo requirements.txt.

$ git clone https://github.com/amcquistan/image_parroter.git
$ python3 -m venv venv
$ . venv/bin/activate
(venv) $ pip install -r requirements.txt

Además de los requisitos que acabo de instalar, quiero agregar uno nuevo para el contenedor de la aplicación web uwsgi que servirá a la aplicación Django.

(venv) $ pip install uWSGI

Antes de continuar, sería un buen momento para actualizar el archivo settings.py para cambiar el valor DEBUG a False y agregar la dirección IP a la lista de ALLOWED_HOSTS.

Después de eso, muévase al directorio del proyecto Django image_parroter (el que contiene el módulo wsgi.py) y agregue un nuevo archivo para guardar los ajustes de configuración de uwsgi, llamado uwsgi.ini, y coloque lo siguiente en él:

# uwsgi.ini
[uwsgi]
chdir=/home/webapp/image_parroter/image_parroter
module=image_parroter.wsgi:application
master=True
processes=4
harakiri=20

socket=/home/webapp/image_parroter/image_parroter/image_parroter/webapp.sock  
chmod-socket=660  
vacuum=True
logto=/var/log/uwsgi/uwsgi.log
die-on-term=True 

Antes de que me olvide, debo continuar y agregar el directorio de registro y otorgarle los permisos y la propiedad adecuados.

(venv) $ sudo mkdir /var/log/uwsgi
(venv) $ sudo chown webapp:www-data /var/log/uwsgi 

A continuación, hago un archivo de servicio systemd para administrar el servidor de aplicaciones uwsgi, que se encuentra en /etc/systemd/system/uwsgi.servicey contiene lo siguiente:

# uwsgi.service
[Unit]
Description=uWSGI Python container server  
After=network.target

[Service]
User=webapp
Group=www-data
WorkingDirectory=/home/webapp/image_parroter/image_parroter
Environment="/home/webapp/image_parroter/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin"
ExecStart=/home/webapp/image_parroter/venv/bin/uwsgi --ini image_parroter/uwsgi.ini

[Install]
WantedBy=multi-user.target

Ahora puedo iniciar el servicio uwsgi, verificar que su estado sea correcto y habilitarlo para que se inicie automáticamente al arrancar.

(venv) $ sudo systemctl start uwsgi.service
(venv) $ sudo systemctl status uwsgi.service
(venv) $ sudo systemctl enable uwsgi.service

En este punto, la aplicación Django y el servicio uwsgi están configurados y puedo continuar con la configuración del servidor redis.

Personalmente prefiero usar los servicios systemd, por lo que editaré el /etc/redis/redis.confarchivo de configuración estableciendo el supervisedparámetro igual a systemd. Después de eso, reinicio el servidor redis, verifico su estado y lo habilito para que se inicie en el arranque.

(venv) $ sudo systemctl restart redis-server
(venv) $ sudo systemctl status redis-server
(venv) $ sudo systemctl enable redis-server

El siguiente paso es configurar el apio. Comienzo este proceso creando una ubicación de registro para Apio y le doy a esta ubicación los permisos y la propiedad adecuados, así:

(venv) $ sudo mkdir /var/log/celery
(venv) $ sudo chown webapp:www-data /var/log/celery

A continuación, agrego un archivo de configuración de Apio, llamado celery.conf, en el mismo directorio que el archivo uwsgi.ini descrito anteriormente, colocando lo siguiente en él:

# celery.conf

CELERYD_NODES="worker1 worker2"
CELERY_BIN="/home/webapp/image_parroter/venv/bin/celery"
CELERY_APP="image_parroter"
CELERYD_MULTI="multi"
CELERYD_PID_FILE="/home/webapp/image_parroter/image_parroter/image_parroter/%n.pid"
CELERYD_LOG_FILE="/var/log/celery/%n%I.log"
CELERYD_LOG_LEVEL="INFO"

Para terminar de configurar apio, agrego su propio archivo de servicio systemd en /etc/systemd/system/celery.servicey coloco lo siguiente en él:

# celery.service
[Unit]
Description=Celery Service
After=network.target

[Service]
Type=forking
User=webapp
Group=webapp
EnvironmentFile=/home/webapp/image_parroter/image_parroter/image_parroter/celery.conf
WorkingDirectory=/home/webapp/image_parroter/image_parroter
ExecStart=/bin/sh -c '${CELERY_BIN} multi start ${CELERYD_NODES} 
  -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} 
  --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'
ExecStop=/bin/sh -c '${CELERY_BIN} multi stopwait ${CELERYD_NODES} 
  --pidfile=${CELERYD_PID_FILE}'
ExecReload=/bin/sh -c '${CELERY_BIN} multi restart ${CELERYD_NODES} 
  -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} 
  --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL}'

[Install]
WantedBy=multi-user.target

Lo último que debe hacer es configurar nginx para que funcione como un proxy inverso para la aplicación uwsgi / django, así como para servir el contenido en el directorio de medios. Hago esto agregando una configuración nginx en /etc/nginx/sites-available/image_parroter, que contiene lo siguiente:

server {
  listen 80;
  server_name _;

  location /favicon.ico { access_log off; log_not_found off; }
  location /media/ {
    root /home/webapp/image_parroter/image_parroter;
  }

  location / {
    include uwsgi_params;
    uwsgi_pass unix:/home/webapp/image_parroter/image_parroter/image_parroter/webapp.sock;
  }
}

A continuación, elimino la configuración nginx predeterminada que me permite usar server_name _;para capturar todo el tráfico http en el puerto 80, luego creo un enlace simbólico entre la configuración que acabo de agregar en el directorio “sitios disponibles” al directorio “sitios habilitados” adyacente lo.

$ sudo rm /etc/nginx/sites-enabled/default
$ sudo ln -s /etc/nginx/sites-available/image_parroter /etc/nginx/sites-enabled/image_parroter

Una vez hecho esto, puedo reiniciar nginx, verificar su estado y habilitarlo para que se inicie en el arranque.

$ sudo systemctl restart nginx
$ sudo systemctl status nginx
$ sudo systemctl enable nginx

En este punto, puedo apuntar mi navegador a la dirección IP de este servidor Ubuntu y probar la aplicación de miniaturas.

Conclusión

Este artículo describió por qué usar, así como cómo usar, Celery con el propósito común de iniciar una tarea asincrónica, que se activa y se ejecuta en serie hasta su finalización. Esto conducirá a una mejora significativa en la experiencia del usuario, reduciendo el impacto de las rutas de código de larga ejecución que impiden que el servidor de aplicaciones web maneje más solicitudes.

He hecho todo lo posible para proporcionar una explicación detallada del proceso de principio a fin desde la configuración de un entorno de desarrollo, la implementación de tareas de apio, la producción de tareas en el código de la aplicación Django y el consumo de resultados a través de Django y algo de JavaScript simple.

Gracias por leer y, como siempre, no dude en comentar o criticar a continuación.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con tus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad