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

Spread the love
  • 3
    Поделились

Введение

В этой статье я расскажу о очередях сообщений 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
  • 3
    Поделились
Подписаться
Уведомление о
guest
6 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Алексей
Алексей
1 год назад

Толково! Спасибо!!!!

Андрей
Андрей
10 месяцев назад

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

Editorial Team
Администратор
Editorial Team
10 месяцев назад
Reply to  Андрей

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

Алексей
Алексей
7 месяцев назад

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

Максим Лейман
Максим Лейман
6 месяцев назад
Reply to  Алексей

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

Василий
Василий
5 месяцев назад
Reply to  Алексей

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

Last edited 5 месяцев назад by Василий
6
0
Будем рады вашим мыслям, пожалуйста, прокомментируйте.x
()
x