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

Введение В этом руководстве я дам общее представление о том, почему очереди сообщений сельдерея ценны, а также как использовать сельдерей в сочетании с Redis в приложении Django. Чтобы продемонстрировать особенности реализации, я создам минималистичное приложение для обработки изображений, которое генерирует эскизы изображений, отправленных пользователями. Будут рассмотрены следующие темы: * Общие сведения об очередях сообщений с Celery и Redis * Локальная настройка Dev с Django, Celery и Redis *

Вступление

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

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

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

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

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

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

Настройка локального разработчика с Django, Celery и Redis

Я начну с самой сложной части - установки Redis.

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

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

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

  1. Загрузите tarball-файл Redis и распакуйте его в какой-нибудь каталог.
  2. Запустите файл make с помощью 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 - это не связанный с сельдереем пакет Python для обработки изображений, который я буду использовать позже в этом руководстве для демонстрации реального варианта использования сельдерея.
  • 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, я добавляю новый модуль image_parroter / image_parrroter / celery.py в соответствии с соглашениями, описанными в документации Celery. В этом новом модуле Python я импортирую пакет os Celery из пакета celery.

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

Завершенный модуль celery.py показан ниже:

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

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

Затем мне нужно убедиться, что ранее созданное и настроенное приложение для сельдерея внедряется в приложение 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 и использую его для определения функции задачи сельдерея с именем adding_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 
 
 -------------- [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 

В третьем и последнем терминале, снова с активной виртуальной средой Python, я могу запустить оболочку Django Python и протестировать мою adding_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(...) для объекта adding_task Это обычный способ передать все необходимые параметры объекту задачи, с которым работаете, а также инициировать отправку его брокеру сообщений и очереди задач. Результатом вызова .delay(...) является возвращаемое значение типа celery.result.AsyncResult типа celery.result.AsyncResult. Это возвращаемое значение содержит такую информацию, как идентификатор задачи, ее состояние выполнения и статус задачи, а также возможность доступа к любым результатам, полученным задачей через метод .get() как показано в примере.

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

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

Вернувшись в модуль 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 

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

Определив задачу с сельдереем, я перехожу к созданию представлений 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 как показано выше класса HomeView

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

 (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, чтобы обеспечить дополнительное поведение. Сначала я создаю ссылку на поле ввода файла и регистрирую прослушиватель изменений, который просто добавляет имя выбранного файла в пользовательский интерфейс после его выбора.

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

Если статус задачи сообщается как «УСПЕШНО», я вставляю ссылку для загрузки в DOM и вызываю ее запуск, инициируя загрузку и очищая интервальный таймер. Если статус - «ОТКАЗ», я просто очищаю интервал, а если статус не «УСПЕШНО» или «ОТКАЗ», я ничего не делаю, пока не будет активирован следующий интервал.

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

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

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

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

В завершение этой статьи я продемонстрирую, как установить и настроить это приложение Django, которое использует Redis и Celery для асинхронных фоновых задач на сервере LTS Ubuntu v18.

После подключения по 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, затем cd в его домашний каталог.

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

Внутри каталога веб-приложения я могу клонировать репозиторий image_parroter GitHub, cd в репо, создать виртуальную среду 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 

В дополнение к требованиям, которые я только что установил, я хочу добавить новый для контейнера веб-приложения 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-server.

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

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

Далее нужно настроить сельдерей. Я начинаю этот процесс с создания места ведения журнала для 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" 

Чтобы завершить настройку сельдерея, я добавляю его собственный служебный файл 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, а также для обслуживания содержимого в каталоге мультимедиа. Я делаю это, добавляя конфигурацию 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 и протестировать приложение для миниатюр.

{.ezlazyload .img-responsive}

Заключение

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

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

Спасибо за чтение и, как всегда, не стесняйтесь комментировать или критиковать ниже.

comments powered by Disqus

Содержание