RedisとCeleryを使ったDjangoの非同期タスク

このチュートリアルでは、Django アプリケーションで Redis と組み合わせて celery を利用する方法とともに、なぜ celery メッセージキューが有用なのかについて、一般的な理解を提供したいと思います。実装の詳細を示すために、ユーザが投稿した画像のサムネイルを生成する、最小限の画像処理アプリケーションを構築します。

以下のトピックがカバーされる予定です。

  • CeleryとRedisを使ったメッセージキューの背景
  • Django、Celery、Redis を使ったローカルデバイスのセットアップ
  • Celeryタスクでの画像サムネイル作成
  • Ubuntu サーバへのデプロイ

このサンプルのコードは GitHub にありますし、インストールやセットアップの説明もありますので、機能的に完全なアプリケーションをすぐに使いたい場合は、この記事の残りの部分で、ゼロからすべてを構築する方法を説明します。

CeleryとRedisによるメッセージキューの背景

CeleryはPythonベースのタスクキューイング・ソフトウェアで、アプリケーションコード(この例ではDjango)から生成されるCeleryタスクキュー宛のメッセージに含まれる情報によって、非同期計算ワークロードの実行を可能にします。Celery はまた、繰り返し可能な定期的な(つまりスケジュールされた)タスクを実行するために使うこともできますが、この記事の焦点はそれではないでしょう。

Celeryは、メッセージブローカーと呼ばれるストレージソリューションと組み合わせて使用するのが最適です。メッセージブローカーとしてよく使われるのはRedisで、これは高性能なインメモリーのKey-Valueデータストアです。具体的には、RedisはCeleryのタスクキューで実行される作業を記述するアプリケーションコードによって生成されるメッセージを格納するために使用されます。また、RedisはCeleryのキューから出力される結果を保存し、キューのコンシューマーがそれを取得する役割も果たす。

Django、Celery、Redisによるローカルデバイスのセットアップ

まずは一番大変な、Redisのインストールから。

WindowsにRedisをインストールする

  1. Redisのzipファイルをダウンロードし、どこかのディレクトリに解凍します。
    1. redis-server.exeというファイルを見つけてダブルクリックし、コマンドウィンドウでサーバを起動します。
    1. redis-cli.exeというファイルをダブルクリックし、コマンドウィンドウでプログラムを起動します。
    1. cliクライアントを実行しているコマンドウィンドウで、pingコマンドを発行してクライアントがサーバーと通信できることを確認します。

Mac OSX / LinuxにRedisをインストールする

  1. Redisのtarballファイルをダウンロードし、どこかのディレクトリに解凍します。
    1. make installでmakeファイルを実行し、プログラムをビルドする。
    1. ターミナルウィンドウを開き、redis-serverコマンドを実行します。
    1. 別のターミナルウィンドウで redis-cli を実行します。
    1. 端末ウィンドウで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 パッケージで、このチュートリアルの後半で celery タスクの実際の使用例を示すために使用する予定です。
  • Django Widget Tweaks は、フォーム入力のレンダリング方法を柔軟にするための Django プラグインです。

Django プロジェクトをセットアップする

次に、 image_parroter という名前の Django プロジェクトを作成し、 thumbnailer という名前の Django アプリ を作成します。

(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


このDjangoプロジェクトにCeleryを統合するために、Celeryのドキュメントに記載されている規則に従って、新しいモジュール image_parroter/image_parrroter/celery.py を追加します。この新しい Python モジュールの中で、 os パッケージと Celery クラスを celery パッケージからインポートしています。

osモジュールは、DJANGO_SETTINGS_MODULEという Celery 環境変数と Django プロジェクトの settings モジュールを関連付けるために使用されます。続いて、Celeryクラスのインスタンスを生成して、celery_appインスタンス変数を生成しています。そして、 Celery アプリケーションの設定を、 Django プロジェクトの settings ファイルにある '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 settings のセクションを定義して、以下のような設定を追加します。これらの設定は、CeleryがメッセージブローカーとしてRedisを使用することと、Redisに接続する場所を指定します。また、CeleryのタスクキューとRedisのメッセージブローカーとの間でやり取りされるメッセージは、application/jsonというMIMEタイプであることを期待するよう、Celeryに伝えています。

# 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 アプリケーションを実行する際にインジェクトされるようにする必要があります。これは、Django プロジェクトのメインスクリプトである _init_

で Celery アプリケーションをインポートし、Django パッケージの “image_parroter” で名前空間付きのシンボルとして明示的に登録することで実現します。

# image_parroter/image_parroter/__init__.py


from .celery import celery_app


__all__ = ('celery_app',)


引き続き、”thumbnailer” アプリケーションの中に tasks.py という新しいモジュールを追加して、提案された規約に従っています。tasks.py モジュールの中で、 shared_tasks 関数デコレーターをインポートし、それを使って、以下のように adding_task という celery タスク関数を定義しています。

# image_parroter/thumbnailer/tasks.py


from celery import shared_task


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


最後に、image_parroter プロジェクトの settings.py モジュールの INSTALLED_APPS のリストに thumbnailer アプリを追加する必要があります。また、”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',
]


これで、3つのターミナルでいくつかの簡単なコマンドを使ってテストができるようになりました。

1つのターミナルでは、redis-serverを次のように実行する必要があります。

$ 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


2番目のターミナルでは、以前にインストールした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


最後の 3 番目の端末では、再び 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


adding_taskオブジェクトで.delay(…)` メソッドを使用していることに注意してください。これは、作業中のタスクオブジェクトに必要なパラメータを渡すための一般的な方法です。

Celeryタスクで画像のサムネイルを作成する

Redis でバックアップされた Celery インスタンスを Django アプリケーションに統合するための基本的なセットアップが終わったので、前に述べた thumbnailer アプリケーションでより便利な機能のデモンストレーションに移ることができます。

tasks.py モジュールに戻って、 PIL パッケージから Image クラスをインポートして、 make_thumbnails という新しいタスクを追加します。これは、画像ファイルのパスと、サムネイルを作成する幅と高さの2タプルのリストを受け取ります。

# 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


上記のサムネイルタスクは、入力画像ファイルを Pillow Image インスタンスにロードし、タスクに渡された寸法リストをループしてそれぞれのサムネイルを作成し、中間ファイルをクリーンアップしながら各サムネイルを zip アーカイブに追加するだけです。サムネイルのZIPアーカイブをダウンロードできるURLを指定したシンプルな辞書が返されます。

celery タスクが定義されたので、ファイルアップロードフォームを持つテンプレートを提供する Django ビューを構築することに移ります。

まず始めに、Django プロジェクトに画像ファイルや zip アーカイブを置くことができる MEDIA_ROOT という場所を与え (上の例のタスクではこれを使いました)、コンテンツを提供する MEDIA_URL を指定します。image_parroter/settings.py モジュールで、MEDIA_ROOTMEDIA_URLIMAGES_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 クラスをインポートし、それを使って、以下のように getpost メソッドを含む HomeView クラスを作成します。

getメソッドは、まもなく作成される home.html テンプレートを返し、HomeViewクラスの上にあるように、ImageFieldフィールドで構成されるFileUploadForm` を渡します。

postメソッドはリクエストで送られたデータを使用してFileUploadFormオブジェクトを構築し、その有効性をチェックします。有効な場合はアップロードされたファイルをIMAGES_DIRに保存して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タスクのステータスを確認するために使用する。ここでは、celeryパッケージからcurrent_appオブジェクトをインポートして、リクエストからtask_idに関連付けられたタスクのAsyncResultオブジェクトを取得するために使用していることに気づかれるでしょう。タスクのステータスと ID を含むresponse_data辞書を作成し、ステータスがタスクの実行に成功したことを示す場合、AsynchResultオブジェクトのget()メソッドを呼び出して結果を取得し、response_dataresults` キーに割り当てて JSON として HTTP リクエスト元に返却しています。

テンプレート UI を作る前に、上記の Django のビュークラスをいくつかの適切な URL にマップする必要があります。まず、 thumbnailer アプリケーションの中に urls.py モジュールを追加して、以下の 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 タスクのステータスをチェックしたり、結果のサムネイルをダウンロードするためのシンプルなテンプレートビューを構築し始めます。まず始めに、thumbnailerディレクトリ内にこのテンプレートを格納するディレクトリを以下のように作成する必要があります。

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


そして、このtemplates/thumbnailerディレクトリにhome.htmlというテンプレートを追加します。home.htmlの中では、まず「widget_tweaks」というテンプレート・タグを読み込み、次にbulma CSSというCSSフレームワークとAxios.jsというJavaScriptライブラリをインポートして、HTMLを定義しています。HTMLページの本文には、タイトル、results in progressメッセージを表示するプレースホルダー、ファイルアップロードフォームを用意しています。

<!-- templates/thumbnailer/home.html --
{% load widget_tweaks %}
<!DOCTYPE html

<html lang="en"
<head
<meta charset="utf-8"/
<meta content="width=device-width, initial-scale=1.0" name="viewport"/
<meta content="ie=edge" http-equiv="X-UA-Compatible"/
<titleThumbnailer</title
<link href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css" rel="stylesheet"/
<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 aria-label="main navigation" class="navbar" role="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' %}" enctype="multipart/form-data" method="POST"
              {% 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 class="file-name" id="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 を追加しています。まず、ファイル入力フィールドへの参照を作成し、変更リスナーを登録します。これは、いったん選択されたファイルの名前をUIに追加するだけです。

次に、より関連性の高い部分です。Django のテンプレート論理演算子 if を使って、 HomeView クラスのビューから受け渡される task_id があるかどうかをチェックします。これは make_thumbnails タスクが送信された後のレスポンスを示しています。次に、Django の url テンプレートタグを使って、適切なタスクの状態をチェックする URL を作成し、先ほど説明した Axios ライブラリを使用して、その URL に時間をおいて AJAX リクエストを開始します。

タスクのステータスが “SUCCESS” と報告された場合、DOM にダウンロードリンクを注入し、それを発火させてダウンロードをトリガーし、インターバルタイマーをクリアします。もしステータスが「FAILURE」であれば、単にインターバルをクリアします。もしステータスが「SUCCESS」でも「FAILURE」でもなければ、次のインターバルが呼び出されるまで何もしないことにします。

この時点で、私はまた別のターミナルを開くことができます。

Ubuntuサーバーへのデプロイメント

この記事を完成させるために、Ubuntu v18 LTS サーバに Redis と Celery を非同期バックグラウンドタスクに利用する Django アプリケーションをインストールし、設定する方法を紹介したいと思います。

サーバにSSH接続した後、サーバをアップデートし、必要なパッケージをインストールします。

(venv) $ python manage.py runserver


また、「webapp」というユーザーを作って、Djangoプロジェクトをインストールするためのホームディレクトリを作ります。

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


ユーザデータを入力した後、webapp ユーザを sudo と www-data グループに追加し、webapp ユーザに切り替えて、ホームディレクトリに cd しています。

# adduser webapp


Webアプリのディレクトリで、image_parroter GitHub リポジトリを clone して、 cd して、Python 仮想環境を作成し、有効化して、requirements.txt ファイルから依存関係をインストールします。

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


先ほどインストールした要件に加えて、Django アプリケーションを提供する uwsgi Web アプリケーションコンテナ用の新しい要件を追加したいと思います。

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


先に進む前に settings.py ファイルを更新して DEBUG の値を False にし、IP アドレスを ALLOWED_HOSTS のリストに追加しておくとよいでしょう。

その後、Django image_parroter プロジェクトのディレクトリ (wsgi.py モジュールを含むディレクトリ) に移動し、uwsgi の設定を保持するための新しいファイル uwsgi.ini を追加して、以下のように記述します。

(venv) $ pip install uWSGI


忘れないうちに、logging ディレクトリを追加して、適切なパーミッションと所有者を与えておきます。

# 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


次に、uwsgi アプリケーションサーバを管理するための systemd サービスファイルを作成します。このファイルは /etc/systemd/system/uwsgi.service にあり、以下の内容になっています。

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


これで uwsgi サービスを起動し、ステータスが問題ないことを確認し、ブート時に自動的に起動するように有効にすることができます。

# 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


これで Django アプリケーションと uwsgi サービスがセットアップされ、redis-server の設定に移ることができます。

個人的には systemd サービスを使うのが好きなので、 /etc/redis/redis.conf 設定ファイルを編集して supervised パラメータを systemd と同じに設定します。その後、redis-serverを再起動し、状態を確認して、ブート時に起動できるようにします。

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


次は、celeryの設定です。まず、Celeryのログを記録する場所を作成し、適切なパーミッションと所有者を設定します。

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


次に、先ほどの uwsgi.ini と同じディレクトリに celery.conf という名前の Celery の設定ファイルを追加し、以下のように記述します。

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


celery の設定を終えるために、systemd サービスファイルを /etc/systemd/system/celery.service に追加し、以下のように記述します。

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


最後に、uwsgi/djangoアプリケーションのリバースプロキシとして動作するようにnginxを設定し、mediaディレクトリのコンテンツを提供します。これは、 /etc/nginx/sites-available/image_parroter にあるnginxの設定に以下の内容を追加することで実現します。

# 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


次に、server_name _; を使って 80 番ポートですべての http トラフィックをキャッチできるようにするデフォルトの nginx 設定を削除し、先ほど “sites-available” ディレクトリに追加した設定と、その隣の “sites-enabled” ディレクトリの間にシンボリックリンクを作成します。

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を再起動し、状態を確認し、ブート時に起動できるようにすることができました。

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


この時点で、ブラウザをこのUbuntuサーバーのIPアドレスに向け、thumbnailerアプリケーションをテストすることができます。

結論

この記事では、非同期タスクをキックオフし、それが完了するまで連続的に実行されるという共通の目的のために、Celeryを使用する理由と使用方法について説明しました。これにより、ユーザーエクスペリエンスが大幅に向上し、Webアプリケーションサーバーがそれ以上のリクエストを処理できないようにする長時間実行のコードパスの影響を軽減することができます。

開発環境の設定から、celery タスクの実装、Django アプリケーションコードでのタスクの生成、そして Django といくつかの簡単な JavaScript による結果の消費まで、最初から最後まで詳細に説明することに全力を尽くしたつもりです。

読んでくれてありがとう、そしていつも通り、コメントや批評を恥ずかしがらずに下記へどうぞ。

</str:task_id

タイトルとURLをコピーしました