Django 非同期View入門

2021/08/18

Ryuji Tsutsui

みんなのPython勉強会#84 資料

Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License .

じめに

自己紹介

  • Ryuji Tsutsui @ryu22e

  • 普段は 株式会社hokan でDjangoを使ってWebサービスを作っています

  • 関わっているコミュニティ: Python Boot Camp、Shonan.py、Python Charity Talks in Japanなど

  • PyCon APAC 2013、PyCon JP 2014はスタッフでした

  • 著書(共著):『 Python実践レシピ

宣伝】『Python Monthly Topics』公開中!

技術評論社さんのサイトにて、『Python実践レシピ』の共著者の一部メンバーで持ち回りで書いています。 月一回、Pythonの旬な話題ついて紹介しているので、よかったら読んでください!

https://gihyo.jp/list/group/Python-Monthly-Topics

今日話すこと

  1. Webアプリケーションの非同期処理とは何か

  2. Djangoの非同期Viewとはどんなものか

  3. Djangoの非同期Viewを書く上での注意点

  4. 本番環境で非同期アプリケーションを動かすには

1. Webアプリケーションの非同期処理とは何か

非同期処理の説明

非同期処理とは

複数のリクエストを受け取った際、シングルスレッドの中で各リクエスト用の処理を細かく切り替えながら、同時に動かしているように見せかけて実行すること「並行処理」とも呼ぶ)

非同期処理の説明

非同期処理の利点とは

Djangoの公式ドキュメントでは以下のように説明している。

The main benefits are the ability to service hundreds of connections without using Python threads. This allows you to use slow streaming, long-polling, and other exciting response types.

非同期処理の説明

日本語訳:

主な利点は、Pythonのスレッドを使用せずに数百の接続を処理できることです。これにより、スローストリーミング、ロングポーリング、その他のエキサイティングなレスポンスタイプを使用することができます。

非同期処理の説明

つまり、非同期処理により、少ないリソースで大量のリクエストを効率よく捌くことができる。

PythonのWebアプリケーションで非同期処理を書くには

Webアプリケーションを動かすには、WebサーバーとPythonアプリケーションの間でデータのやり取りを仲介する標準インターフェースが必要。

WSGIとは

  • Pythonの世界でよく使われる標準インターフェース

  • Web Server Gateway Interfaceの

  • Djangoでもサポートしている

  • これは同期処理を前提としている

ASGIとは

  • WSGIの「Spiritual successor(精神的続編)」として誕生した標準インターフェース

  • Asynchronous Server Gateway Interfaceの

  • 非同期に対応している

(ちなみに)「Spiritual successor(精神的続編)」とは

  • ビデオゲームでよく使われる用語で、前作と直接関係はないものの、インスピレーションを得て作られた作品のことを意味する

  • Wikipediaにも記事がある: https://en.wikipedia.org/wiki/Spiritual_successor

  • 例: 『Demon's Souls』→『DARK SOULS』

2. Djangoの非同期Viewとはどんなものか

DjangoでのASGI対応の歴史

DjangoでのASGI対応の歴史

Django 3.0(2019年12月2日リリース)

  • ASGIをサポートするようになった

  • ただし、非同期Viewがまだ書けないので「ASGIをサポートしているのに非同期アプリケーションは作れない」という状態

DjangoでのASGI対応の歴史

Django 3.1(2021年3月リリース)

非同期Viewをサポート(ただし関数ベースViewのみ)

import datetime
from django.http import HttpResponse

# 関数の先頭にasyncを入れる
async def current_datetime(request):
    now = datetime.datetime.now()
    html = 'It is now %s.' % now
    return HttpResponse(html)

DjangoでのASGI対応の歴史

Django 4.1(2022年8月3日リリース)

非同期クラスベースViewをサポート

import asyncio

from django.http import HttpResponse
from django.views import View


class AsyncView(View):
    # メソッドの先頭にasyncを付ける
    async def get(self, request, *args, **kwargs):
        await asyncio.sleep(1)
        return HttpResponse("非同期ビューのレスポンス")

DjangoでのASGI対応の歴史

(非同期クラスベースViewのデモ)

3. Djangoの非同期Viewを書く上での注意点

このコードは実は動かない

from django.http import HttpResponse
from django.views import View

from .models import Book


class AsyncView(View):
    async def get(self, request, *args, **kwargs):
        titles = []
        for book in Book.objects.all():
            titles.append(book.title)
        return HttpResponse(",".join(titles))

このコードは実は動かない

SynchronousOnlyOperationエラーが発生

SynchronousOnlyOperationエラーが発生

SynchronousOnlyOperationエラーとは何か

非同期処理の中で安全に呼べない処理が書かれていた場合に発生するエラー。

SynchronousOnlyOperationエラーとは何か

Djangoの内部では同期処理を前提としたグローバルな状態を持っている処理があって、これを非同期で実行させることができない。

SynchronousOnlyOperationエラーとは何か

前述の例ではModel経由でデータベースの操作を行っているが、これは同期処理専用に該当する。

では、どうすればよいか

モデルの操作に async付ける。

from django.http import HttpResponse
from django.views import View

from .models import Book


class AsyncView(View):
    async def get(self, request, *args, **kwargs):
        titles = []
        async for book in Book.objects.all():
            titles.append(book.title)
        return HttpResponse(",".join(titles))

では、どうすればよいか

create()get()delete() などのメソッドの非同期用は、先頭にaを付けたメソッド acreate()aget()adelete()いうメソッドがある。

class AsyncView(View):
    async def get(self, request, *args, **kwargs):
        # メソッドの先頭にaを付ける(awaitを使うのも忘れずに)
        book = await Book.objects.acreate(title="Example")
        book = await Book.objects.aget(pk=book.pk)
        await book.adelete()
        ...

では、どうすればよいか

モデル以外の同期専用処理は以下のようにして呼び出す。

from asgiref.sync import sync_to_async

def sync_only1():
   """同期処理専用の関数"""
   ...

# sync_to_asyncでラップして実行
await sync_to_async(sync_only1)()

# 関数デコレータとしても使える
@sync_to_async
def sync_only2():
   """同期処理専用の関数"""
   ...

とりあえず動かしたいならこんな手もある

環境変数 DJANGO_ALLOW_ASYNC_UNSAFE任意の値を設定すると、とりあえず動かせる。

$ # 開発用サーバー実行
$ DJANGO_ALLOW_ASYNC_UNSAFE=1 python manage.py runserver
$ # テスト実行
$ DJANGO_ALLOW_ASYNC_UNSAFE=1 python manage.py test

DJANGO_ALLOW_ASYNC_UNSAFEの使い方について注意

この機能は開発環境でとりあえず動かしてみる用途で使うだけにして、本番環境では使わないこと!

DJANGO_ALLOW_ASYNC_UNSAFEの使い方について注意

Djangoには非同期の実行を想定していない、グローバルな状態を保持する処理があり、SynchronousOnlyOperation エラーはそれらを非同期で実行させないための仕組み。

DJANGO_ALLOW_ASYNC_UNSAFEの使い方について注意

SynchronousOnlyOperation エラーを無視すると、予期せぬ動作(データの損失や破損)が発生する可能性がある。

Modelの非同期インターフェースについて残念なお知らせ

4.1リリースノート より:

Note that, at this stage, the underlying database operations remain synchronous, with contributions ongoing to push asynchronous support down into the SQL compiler, and integrate asynchronous database drivers.

Modelの非同期インターフェースについて残念なお知らせ

日本語訳:

なお、現段階では、基本的なデータベース操作は同期のままであり、非同期のサポートをSQLコンパイラに落とし込み、非同期データベースドライバを統合するための貢献が続いています。

Modelの非同期インターフェースについて残念なお知らせ

つまり、4.1時点では内部のコードは非同期で安全に呼べるようにしただけで、中身は同期処理のまま。

Modelの非同期インターフェースについて残念なお知らせ

おそらく、中身が非同期処理になったら非同期用のデータベースドライバー(Psycopg3, asyncpg, aiomysqlなど)をサポートするようになるはず!

本番環境で非同期アプリケーションを動かすには

WSGI用アプリケーションサーバーではダメ

GunicornuWSGI などはWSGI専用なので、非同期用アプリケーションでは使えない。

正確には、アプリケーションを動かせるが本来のパフォーマンスを発揮できない。

ASGI用アプリケーションサーバーを使う

以下のASGI対応アプリケーションサーバーを使う。

ASGI用アプリケーションサーバーを使う

(Uvicornを使ったデモ)

最後に

まとめ

  • PythonのWebアプリケーションで非同期処理を書くにはASGIが必要

  • Djangoでは3.0でASGIをサポート、3.1で非同期関数ベースView、4.1で非同期クラスベースViewをサポート

  • 非同期View内で同期処理専用の処理を呼ぶと SynchronousOnlyOperation エラーが発生

  • 本番環境で動かすにはASGI対応アプリケーションサーバーを使うこと

おしまい

清聴ありがとうございました。

質問あったらどうぞ!