Python

Асинхронные задачи в Django с Redis и Celery

Spread the love

Введение

В этой статье я расскажу о очередях сообщений celery и о том как использовать celery в сочетании с Redis в приложении Django. Чтобы продемонстрировать особенности реализации, создадим небольшое приложение для обработки изображений, которое генерирует эскизы изображений из изображений представленных пользователями.

В статье будут рассмотрены следующие темы:

  • Справочная информация об очередях сообщений (Message Queues) с Celery и Redis
  • Локальная настройка Django, Celery и Redis
  • Создание миниатюр изображений в задаче Celery
  • Развертывание приложение на сервере Ubuntu

Код для этого примера можно найти на GitHub вместе с инструкциями по установке и настройке, если вы просто хотите сразу перейти к функционально завершенному приложению, в противном случае в оставшейся части статьи я расскажу, как собрать все с нуля.

Справочная информация об очередях сообщений (Message Queues) с Celery и Redis

Celery — это программный пакет для организации очередей задач на основе Python, который позволяет выполнять асинхронные задачи, основанные на информации, содержащейся в сообщениях, которые генерируются в коде приложения (в нашем примере Django), предназначенном для очереди задач Celery. Celery также может быть использован для выполнения повторяемых, периодических (то есть запланированных) задач, но это мы не будем рассматривать в этой статье.

Celery лучше всего использовать в сочетании с решением для хранения сообщений, которое часто называют брокером сообщений. Распространенным посредником сообщений, который используется с celery, является Redis, который является быстродействующим хранилищем данных по типу ключ-значение. Redis также служит хранилищем результатов, поступающих из очередей celery, которые затем извлекаются потребителями очереди.

Локальная настройка с Django, Celery и Redis

Сначала начнем с самой сложной части — установки Redis.

Установка Redis в Windows

  1. Скачайте Redis zip файл и распакуйте в какой-нибудь каталог
  2. Найдите файл с именем redis-server.exe и дважды щелкните, чтобы запустить сервер в командном окне.
  3. Аналогичным образом найдите другой файл с именем redis-cli.exe и дважды щелкните его, чтобы открыть программу в отдельном командном окне.
  4. В командном окне, в котором запущен клиент Cli, проверьте, чтобы клиент мог общаться с сервером, введя команду ping, и, если все пойдет хорошо, ответ PONG должен быть возвращен.

Установка Redis на Mac OSX / Linux

  1. Загрузите файл архива Redis и распакуйте его в какой-нибудь каталог
  2. Запустите команду make install, чтобы собрать программу
  3. Откройте окно терминала и выполните команду redis-server.
  4. В другом окне терминала запустите redis-cli
  5. В окне терминала, в котором работает клиент Cli, проверьте, чтобы клиент мог общаться с сервером, выполнив команду ping, и, если все пойдет хорошо, ответ PONG должен быть возвращен.

Установите Python Virtual Env и зависимости

Теперь перейдем к созданию виртуальной среды Python3 и установке пакетов зависимостей, необходимых для этого проекта.

Для начала создадим каталог проекта с именем image_parroter, а затем внутри него создадим свою виртуальную среду. Все дальнейшие команды будут только в Unix-стиле, но большинство, если не все, будут одинаковыми для среды Windows.

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

Теперь, когда виртуальная среда активирована, я можно установить пакеты Python.

(venv) $ pip install Django Celery redis Pillow django-widget-tweaks
(venv) $ pip freeze > requirements.txt
  • Pillow — это не связанный с celery пакет Python для обработки изображений, который будет использовать позже в этом уроке для демонстрации использования задач celery.
  • Django Widget Tweaks — это плагин Django, обеспечивающий гибкость при отображении входных данных формы.

Настройка проекта Django

Двигаясь дальше, создадим проект Django с именем image_parroter, а затем приложение Django с именем thumbnailer.

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

На данный момент структура каталогов должна выглядит следующим образом:

$ 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

Чтобы интегрировать Celery в проект Django, добавим новый файл imageparroter/imageparrroter/celery.py в соответствии с соглашениями, описанными в документации Celery. Внесите в него следующий код:

# 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()  

В этом новом файле Python импортируется пакет os и класс Celery из пакета celery. Модуль os используется для связывания переменной среды Celery под названием DJANGO_SETTINGS_MODULE с модулем настроек проекта Django. После этого создается экземпляр класса Celery для создания переменной celery_app. Затем обновляется конфигурацию приложения Celery настройками, которые вскоре добавим в файл настроек проекта Django, идентифицируемые префиксом CELERY_. Наконец, недавно созданный экземпляр celery_app запускается для автоматического обнаружения задач в проекте.

Теперь в модуле settings.py проекта, в самом низу, определим раздел для настроек celery и добавим настройки, которые вы видите ниже. Эти настройки говорят Celery использовать Redis в качестве посредника сообщений, а также как к нему подключиться. Они также говорят Celery ожидать, что сообщения будут передаваться между очередями задач Celery и брокером сообщений Redis в mime-типе 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'  

Далее нужно убедиться, что ранее созданное и настроенное приложение celery внедряется в приложение Django при его запуске. Это можно сделать, импортировав приложение Celery в основной скрипт __init__.py проекта Django и явно зарегистрировав его как символ пространства имен в пакете Django «image_parroter».

# image_parroter/image_parroter/__init__.py

from .celery import celery_app

__all__ = ('celery_app',)  

Продолжим следовать предложенным соглашениям, добавляя новый модуль tasks.py в приложение thumbnailer. Внутри модуля tasks.py импортируется функция-декоратор shared_tasks и используется для определения функции задачи celery с именем add_task, как показано ниже.

# image_parroter/thumbnailer/tasks.py

from celery import shared_task

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

Наконец, нужно добавить приложение thumbnailer в список INSTALLED_APPS в модуле settings.py проекта image_parroter. Также добавим приложение widget_tweaks, которое будет использоваться для управления рендерингом ввода формы, чтобы позволить пользователям загружать файлы.

# 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',
]

Теперь можно проверить как все работает с помощью нескольких простых команд на трех терминалах.

В одном терминале нужно запустить Redis-сервер, вот так:

$ 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  

Во втором терминале, с активным экземпляром виртуальной среды Python, установленным ранее, в корневом каталоге пакета проекта (тот же, что содержит модуль manage.py), запустить программу celery.

(venv) $ celery worker -A image_parroter --loglevel=info

 -------------- celery@Adams-MacBook-Pro-191.local 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

В третьем и последнем терминале, снова с активной виртуальной средой Python, нужно запустить оболочку Django Python и протестировать add_task, вот так:

(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  

Обратите внимание на использование метода .delay (…) в объекте add_task. Это распространенный способ передачи любых необходимых параметров объекту задачи, с которым он работает, а также инициирования отправки его посреднику сообщений и очереди задач. Результатом вызова метода .delay (…) является возвращаемое значение promise типа celery.result.AsyncResult. Это возвращаемое значение содержит такую информацию, как идентификатор задачи, ее состояние выполнения и состояние задачи, а также возможность доступа к любым результатам, полученным задачей, с помощью метода .get(), как показано в примере.

Создание миниатюр изображений в задаче Celery

Теперь перейдем к созданию некоторых более полезных функций в приложением thumbnailer.

Вернувшись в модуль tasks.py, импортируем класс Image из пакета PIL, затем добавим новую задачу под названием make_thumbnails, которая принимает путь к файлу изображения и список из двух измерений ширины и высоты для создания миниатюр.

# 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

Вышеуказанная задача make_thumbnails просто загружает файл входного изображения в экземпляр Pillow Image, затем перебирает список измерений, переданный в задачу, создавая миниатюру для каждого, добавляя каждый эскиз в zip-архив, а также очищает промежуточные файлы. Возвращается простой словарь с указанием URL-адреса, с которого можно скачать zip-архив миниатюр.

Далее перейдем к созданию представлений (views) Django для предоставления шаблона с формой загрузки файла.

Для начала укажем в проекте Django местоположение MEDIA_ROOT, в котором будут находиться файлы изображений и zip-архивы, а также укажим MEDIA_URL, откуда может быть скачен контент. В модуле image_parroter/settings.py добавим местоположения настроек MEDIA_ROOT, MEDIA_URL, IMAGES_DIR и затем определим команды для создания этих каталогов, если они не существуют.

# 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)

Внутри модуля thumbnailer/views.py импортируем класс django.views.View который используется для создания класса HomeView, содержащего методы get и post, как показано ниже.

Метод get просто возвращает шаблон home.html, который вскоре будет создан, и передает ему FileUploadForm, состоящий из поля ImageField.

Метод post создает объект FileUploadForm, используя данные, отправленные в запросе, проверяет его достоверность, затем, если он действителен (то есть верный), сохраняет загруженный файл в IMAGES_DIR и запускает задачу make_thumbnails, одновременно захватывая идентификатор и статус задачи для передачи в шаблон, или возвращает форму с ошибками в шаблон 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)

Ниже класса HomeView размещен класс TaskView, который будет использоваться через запрос AJAX для проверки состояния задачи make_thumbnails. Здесь вы можете заметите, что я импортировал объект current_app из пакета celery и использовал его для получения объекта AsyncResult задачи, связанного с идентификатором task_id, из запроса. Далее создается словарь response_data со статусом и идентификатором задачи, а затем, если статус указывает на то, что задача выполнена успешно, получаем результаты, вызывая метод get() объекта AsynchResult, присваивая его ключу results в response_data, который возвращается как JSON в HTTP-запросе.

Прежде чем создавать пользовательский интерфейс шаблона, нужно сопоставить вышеупомянутые классы представлений Django с некоторыми URL-адресами. Начнем с добавления модуля urls.py в приложение thumbnailer и определяю следующих 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'),
]

Затем в конфигурации основного URL-адреса проекта включим URL-адреса уровня приложения thumbnailer.urls, а также сообщить ему URL-адрес мультимедиа, например:

# 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)

Затем создадим простой шаблон для пользователя, чтобы он мог отправить файл изображения, а также проверить состояние отправленных задач make_thumbnails и начать загрузку полученных миниатюр. Для начала нужно создать каталог для размещения этого единственного шаблона в каталоге thumbnailer следующим образом:

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

Затем в этот каталог templates/thumbnailer добавим шаблон с именем home.html. Внутри home.html загрузим теги шаблона «widget_tweaks«, потом создадим HTML код, импортируем CSS bulma CSS, а также библиотеку JavaScript Axios.js. В теле HTML-страницы опишем заголовок, заполнитель для отображения сообщения о результатах и форму загрузки файла.

<!-- 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>  

В нижней части элемента body я добавил JavaScript, чтобы обеспечить дополнительное поведение. В нем сначала создается ссылка на поле ввода файла и регистрируется прослушиватель (listener) изменений, который просто добавляет имя выбранного файла в UI после его выбора.

Далее идет более актуальная часть. Я использовал шаблонный оператор if для проверки наличия task_id, передаваемого из представления класса HomeView. Он указывает на ответ после того, как задача make_thumbnails была отправлена. Затем я использовал тег шаблона url, чтобы создать соответствующий URL-адрес для проверки состояния задачи и начало интервального AJAX-запроса к этому URL-адресу с помощью библиотеки Axios, о которой я упоминал ранее.

Если состояние задачи возвращается как «SUCCESS», вставляется ссылка на загрузку в DOM и затем она запускается, вызывая загрузку и сбрасывая интервальный таймер. Если статус «FAILURE», просто очищается интервал, а если статус не «SUCCESS» или «FAILURE», то ничего не делается, пока не будет вызван следующий интервал.

На этом этапе откройте еще один терминал, еще раз с активной виртуальной средой Python, и запустите сервер разработки Django, как показано ниже:

(venv) $ python manage.py runserver
  • Терминалы задач redis-server и celery, описанные ранее, также должны быть запущены, и если вы не перезапустили worker Celery с момента добавления задачи make_thumbnails, вам нужно нажать Ctrl + C, чтобы остановить worker, а затем выполнить celery worker -A image_parroter —loglevel=info снова, чтобы перезапустить его. Worker celery должны быть перезапущены каждый раз, когда производится изменение кода, связанного с задачей celery.

Теперь можно загрузить представление (views) home.html в своем браузере по адресу http://localhost:8000, и отправить файл изображения, а приложение должно ответить архивом results.zip, содержащим исходное изображение и уменьшенное изображение размером 128×128 пикселей.

Развертывание на сервере Ubuntu

В завершение этой статьи рассмотрим, как установить и настроить это наше приложение на сервере Ubuntu v18 LTS.

После того, как подключимся через SSH к серверу, обновим его и затем устанавим все необходимые пакеты.

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

Также создадим пользователя с именем webapp, у которого будет свой домашний каталог для установки проекта Django.

# adduser webapp

Затем добавим пользователя webapp в группы sudo и www-data . После переключимся на пользователя webapp и зайдем в его домашний каталог.

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

Внутри каталога клонируем репозиторий GitHub image_parroter, затем создадим виртуальную среду Python, активируем ее, и затем установим зависимости из файла requirements.txt.

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

В дополнение к только что установленным requirements добавим еще одну библиотеку, uwsgi (контейнер веб-приложений ), который будет обслуживать приложение Django.

(venv) $ pip install uWSGI

Прежде чем двигаться дальше, было бы неплохо обновить файл settings.py, чтобы переключить значение DEBUG на False и добавить IP-адрес в список ALLOWED_HOSTS.

После этого перейдем в каталог проекта Django image_parroter (тот, который содержит модуль wsgi.py) и добавьте новый файл для хранения параметров конфигурации uwsgi с именем uwsgi.ini и поместим в него следующее:

# 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  

Далее создадим каталог для логов и дадим ему соответствующие разрешения.

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

Затем создадим файл службы systemd для управления сервером приложений uwsgi, который находится по пути /etc/systemd/system/uwsgi.service и содержит следующее:

# 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  

Теперь можно запустить службу uwsgi, проверить, что ее состояние в порядке, и включить ее, чтобы она автоматически запускалась при загрузке.

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

На этом этапе приложение Django и служба uwsgi настроены, и можно перейти к настройке сервера redis.

Я лично предпочитаю использовать службы systemd, поэтому отредактируем файл /etc/redis/redis.conf, установим параметр supervised, равный systemd. После этого перезагрузим redis-сервер, проверяя его состояние и включим его в автозагрузку.

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

Далее следует настроить celery. Начнем этот процесс с создания каталога логов для Celery и предоставим ему соответствующие разрешения, например:

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

После этого добавим файл конфигурации Celery с именем celery.conf в тот же каталог, что и файл uwsgi.ini, описанный ранее, со следующим содержимым:

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

Чтобы завершить настройку celery, добавим собственный файл службы systemd по пути /etc/systemd/system/celery.service со следующим содержимым:

# 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  

Последнее, что нужно сделать, это настроить nginx для работы в качестве обратного прокси-сервера для приложения uwsgi/django, а также для обработки содержимого в каталоге media. Добавим конфигурацию nginx в /etc/nginx/sites-available/image_parroter :

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;
  }
}

Затем удалим конфигурацию nginx по умолчанию, что бы использовать server_name _; для того чтобы перехватить весь трафик http через порт 80, далее создадим символическую ссылку между конфигурацией, которую только что добавили в каталог sites-available, в каталог sites-enabled .

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

После этого можно перезапустить nginx, проверить его состояние и включить его в автозагрузку.

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

На этом этапе можно ввести в своем браузере IP-адрес вашего сервера Ubuntu и протестировать приложение thumbnailer.

Заключение

В этой статье описывается, зачем и как использовать Celery для запуска асинхронной задачи. Это делается для значительного улучшения взаимодействия с пользователем, и уменьшая влияние блокирующего кода веб-приложения во время обработки дальнейших запросов.

Я приложил все усилия, чтобы предоставить подробное объяснение процесса начала до конца, начиная с настройки среды разработки, реализации задач celery, создания задач в коде приложения Django, а также получения результатов с помощью Django и некоторого простого JavaScript.

Оригинальная статья: Adam McQuistan Asynchronous Tasks in Django with Redis and Celery

Была ли вам полезна эта статья?
[20 / 5]

Spread the love
Editorial Team

View Comments

  • Кто объяснит какую роль здесь играет Redis?

    • Если упрощенно Redis это простая и быстрая база данных для хранения временных данных.

  • выполняю все в точности по инструкции, но когда дохожу до delay в shell, этот метод не отрабатывает. т.е. буквально программа замирает. без него работает. В чем может быть причина?

    • Стоит обратить внимание на логи celery. По идее, когда все в порядке, в консоли вываливается сообщение, что "такая-то задача была обработана за столько-то секунд". Еще, как вариант, стоит перепроверить точно ли поднялся redis и на правильном ли порту, т.к. "замирание" программы может говорить о бесконечном ожидании ответа от чего-то(например от redisa). Но это все догадки. По идее все должно быть в консоли celery, так что вам туда.

    • Тоже была такая проблема. Решилась добавлением опции --pool=solo в строчку запуска celery под windows. Полная строка запуска у меня выглядела так - celery -A image_parroter worker --loglevel=info --pool=solo.

  • где набрать команду make install, когда скачиваешь redis?

Recent Posts

Vue 3.4 Новая механика v-model компонента

Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование​ v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…

12 месяцев ago

Анонс Vue 3.4

Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…

12 месяцев ago

Как принудительно пере-отобразить (re-render) компонент Vue

Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…

2 года ago

Проблемы с установкой сертификата на nginix

Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…

2 года ago

Введение в JavaScript Temporal API

Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…

2 года ago

Когда и как выбирать между медиа запросами и контейнерными запросами

Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…

2 года ago