Django と Celery で非同期処理を実装する

Django と Celery で非同期処理を実装します。

リアルタイムで重たい処理を実行したい場合、様々な問題が出てきます。パッと思いつくのは、Apache や Nginx など Web サーバーのタイムアウト問題でしょうか。あと、ユーザを待たせてしまうUXの問題などもあります。

この問題を解決する方法として、重たい処理は非同期としてバックグラウンドで実行し、HTTP のレスポンスは、すぐに返してしまう方法があります。

完成イメージ

環境とバージョン

  • CentOS 7.4.1708
  • Python 3.6.4
  • Django 2.0.3
  • Celery 4.2.0
  • django-celery-results 1.0.1
  • mysqlclient 1.3.12
  • epel-release 7.11
  • Redis 3.2.10
  • MySQL 5.7

インストール

Python, Django

Python と Django のインストールは、Python Web フレームワーク Django の環境を構築するの記事を参考にしてください。

Celery, django-celery-results, redis

1
$ pip install celery django-celery-results redis

Celery と Celery で使用する Python パッケージをインストールします。

MySQL のインストール

1
2
$ sudo yum-config-manager --disable ius
$ sudo yum install mysql mysql-devel mysql-server mysql-utilities

Celery の結果を DB に保存するために MySQL をインストールします。

MySQL のインストールについては、こちらを参考にしてください。

Python を ius リポジトリでインストールした場合、MySQL リポジトリと衝突します。

なので、先に ius リポジトリを disable にしてください。

mysqlclient

1
$ sudo pip install mysqlclient

MySQL に接続するための Python パッケージをインストールします。

Redis

1
2
3
$ sudo yum install epel-release
$ sudo yum install redis
$ sudo systemctl start redis

Broker は RabbitMQ ではなく、Redis を使いたいと思います。

実装

パッケージのインストールが一通り終わりましたので実装して行きます。

tree

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ tree
.
├── app
│   ├── __init__.py
│   ├── apps.py
│   ├── tasks.py
│   ├── templates
│   │   └── app
│   │       └── index.html
│   └── views.py
├── manage.py
└── proj
    ├── __init__.py
    ├── celery.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py
 
4 directories, 11 files

まず最初に、ファイル構成です。このように実装していきます。

settings.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ALLOWD_HOSTS = ['*']
INSTALLED_APPS = [
    'app.apps.AppConfig',
    'django_celery_results',
    ...
]
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'proj',
        'USER': 'root',
        'PASSWORD': 'Eba7|B33veK+',
        'OPTIONS': {
            'charset': 'utf8mb4',
        }
    }
}
CELERY_RESULT_BACKEND = 'django-db'

settings.py です。

Celery の実行結果を MySQL に保存するために、django-celery-results を使います。そのため、INSTALLED_APPS と CELERY_RESULT_BACKEND の設定をします。

celery.py

1
2
3
4
5
6
7
8
import os
from celery import Celery
 
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proj.settings')
app = Celery('proj')
app.conf.broker_url = 'redis://localhost:6379/0'
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()

Celery の設定ファイルを実装します。この celery.py は settings.py と同じ階層に保存します。

__init__.py

1
2
3
from .celery import app as celery_app
 
__all__ = ('celery_app',)

この __init__.py は celery.py と同じ階層の __init__.py です。

tasks.py

1
2
3
4
5
from celery import shared_task
 
@shared_task
def add(x, y):
    return x + y

tasks.py は、非同期処理のメインとなる関数を実装するファイルです。

サンプルとして、足し算結果を返す関数を実装しましたが、本来であれば、ここに重たい処理がくると思います。

urls.py

1
2
3
4
5
6
from django.urls import path
from app.views import index
 
urlpatterns = [
    path('', index, name='index'),
]

ブラウザから動作を確認するために、urls.py を用意します。

views.py

1
2
3
4
5
6
7
8
9
from django.shortcuts import render
from django_celery_results.models import TaskResult
from app.tasks import add
 
def index(request):
    add.delay(1, 1)
    object_list = TaskResult.objects.all().order_by('pk')
    context = {'object_list': object_list}
    return render(request, 'app/index.html', context)

アクセスがあったら、バックグランドに足し算処理を投げ、ブラウザにすぐにレスポンスを返します。

ページに何か表示した方がいいと思ったので、Celery の結果を表示しています。

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<html>
  <head>
    <title>Django と Celery で非同期処理を実装する</title>
  </head>
  <body>
    <h1>Django と Celery で非同期処理を実装する</h1>
    <table>
      <thead>
        <tr>
          <th>#</th>
          <th>Task Id</th>
          <th>Task Name</th>
          <th>Status</th>
          <th>Result</th>
          <th>Date Done</th>
        </tr>
      </thead>
      <tbody>
        {% for object in object_list %}
        <tr>
          <td>{{ forloop.counter }}</td>
          <td>{{ object.task_id }}</td>
          <td>{{ object.task_name }}</td>
          <td>{{ object.status }}</td>
          <td>{{ object.result }}</td>
          <td>{{ object.date_done }}</td>
        </tr>
        {% endfor %}
      </tbody>
    </table>
  </body>
</html>

DB マイグレーション

Create database

1
$ mysql -u root -p -e 'create database proj default charset utf8mb4'

非同期結果を DB に保存しますので、まずデータベースを用意します。

migrate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ python3.6 manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, django_celery_results, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying django_celery_results.0001_initial... OK
Applying sessions.0001_initial... OK

データベースを用意したら、テーブルを作成します。

動作確認

Django の起動

1
2
3
4
5
6
7
8
$ python3.6 manage.py runserver 0.0.0.0:8000
Performing system checks...
 
System check identified no issues (0 silenced).
July 04, 2018 - 04:29:40
Django version 2.0.3, using settings 'proj.settings'
Starting development server at http://0.0.0.0:8000/
Quit the server with CONTROL-C.

ブラウザでアクセスするために、Django のデバッグ用サーバー runserver を起動します。

Celery の起動

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ celery -A proj worker -l info
-------------- celery@localhost.localdomain v4.2.0 (windowlicker)
---- **** -----
--- * *** * -- Linux-3.10.0-693.21.1.el7.x86_64-x86_64-with-centos-7.4.1708-Core 2018-07-04 04:35:30
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> app: proj:0x7f42c6e217f0
- ** ---------- .> transport: redis://localhost:6379/0
- ** ---------- .> results:
- *** --- * --- .> concurrency: 1 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.&gt; celery exchange=celery(direct) key=celery
 
[tasks]
. app.tasks.add
 
[2018-07-04 04:35:30,719: INFO/MainProcess] Connected to redis://localhost:6379/0
[2018-07-04 04:35:30,743: INFO/MainProcess] mingle: searching for neighbors
[2018-07-04 04:35:31,794: INFO/MainProcess] mingle: all alone
[2018-07-04 04:35:31,813: WARNING/MainProcess] /usr/lib/python3.6/site-packages/celery/fixups/django.py:200: UserWarning: Using settings.DEBUG leads to a memory leak, never use this setting in production environments!
warnings.warn('Using settings.DEBUG leads to a memory leak, never '
[2018-07-04 04:35:31,813: INFO/MainProcess] celery@localhost.localdomain ready.

非同期処理をするために、Celery を起動します。

ブラウザで http://192.168.33.10:8000 にアクセス

ブラウザでアクセスする毎に非同期処理 Celery がバックグラウンドで実行し、結果が表示されるのが確認できると思います。

まとめ

Django と Celery で非同期処理を実装しました。

今回は、足し算するだけの軽い処理なので、本当は非同期にする必要がないですね。

しかし、最初から重たい処理になることがわかっている場合や、運用後にデータ量が多くなり、処理が重たくなってきた場合は、Celery で非同期処理を検討してみるといいかもしれません。