Upgrade to Pro — share decks privately, control downloads, hide ads and more …

お前はまだ本当のDjango ASGIを知らない(PyConJP2022 )

Denzow
October 15, 2022
3k

お前はまだ本当のDjango ASGIを知らない(PyConJP2022 )

Django は3.0よりASGIをサポートしました。WSGIの後継として登場したASGIですが、実際にこうすれば使える、async/awaitで書きます。といった情報は多く見かけるようになりました。しかし、結局運用的に何が美味しいのか、パフォーマンス的に何が美味しいのか、そもそも実際に美味しいのかといった観点の情報はまだあまり世の中に多くありません。

今回はDjangoが正式にASGI対応を始める前のDjango 2.x時代からDjango Channelsを利用したASGIウェブアプリケーションを運用してきたLAPRASでの経験を踏まえ、Django標準のASGI対応をお伝えしていきます。

Denzow

October 15, 2022
Tweet

Transcript

  1. お前はまだ本当の

    Django ASGIを知らない

    @denzowill / Denzow Yamada

    2022/10/15

    #PyConJP2022


    View Slide

  2. 自己紹介

    2


    View Slide

  3. LAPRAS

    3


    View Slide

  4. LAPRAS

    4

    https://lapras.com/public/denzow

    View Slide

  5. 本セッションの概要

    5


    View Slide

  6. 本セッションの概要

    6

    ASGI?


    View Slide

  7. 本セッションの概要

    7

    ASGI?

    WebSocket?


    View Slide

  8. 本セッションの概要

    8

    ASGI?

    WebSocket?

    Django Channels?


    View Slide

  9. 本セッションの概要

    9

    ASGI?

    WebSocket?

    Django Channels?

    Async View?


    View Slide

  10. Django の ASGI 対応

    10


    View Slide

  11. そもそもASGI?

    11

    ASGI?

    View Slide

  12. そもそもASGI?

    12

    Asynchronous
    Server
    Gateway
    Interface

    View Slide

  13. そもそもASGI?

    13

    Asynchronous
    Server
    Gateway
    Interface

    View Slide

  14. そもそもASGI?

    14

    Asynchronous
    Server
    Gateway
    Interface
    非同期

    サーバー

    ゲートウェイ

    インターフェース


    View Slide

  15. そもそもASGI?

    15

    https://asgi.readthedocs.io/en/latest/introduction.html
    ASGI は、 Web サーバー、フレームワーク、およびアプリケーション間の
    互換性のための長年の Python 標準であるWSGIの精神的な後継者です。

    View Slide

  16. WSGI?

    16

    WSGI?

    View Slide

  17. WSGI?

    17

    Web
    Server
    Gateway
    Interface

    View Slide

  18. WSGI?

    18

    Web
    Server
    Gateway
    Interface

    View Slide

  19. WSGI?

    19

    Web
    Server
    Gateway
    Interface
    Web (同期的)

    サーバー

    ゲートウェイ

    インターフェース


    View Slide

  20. WSGIはPEPにある

    20


    View Slide

  21. ASGIはWSGIの後継

    21

    https://asgi.readthedocs.io/en/latest/introduction.html
    「なぜ WSGI をアップグレードしないのか」と疑問に
    思うかもしれません。
    これは何年にもわたって何度も尋ねられてきましたが、
    通常、問題は、WSGI の単一呼び出し可能なインターフェースが、
    WebSocket のようなより複雑な Web プロトコルには
    適していないということです。

    View Slide

  22. ASGIのPEP?

    22

    ASGIのPEPは?

    View Slide

  23. ASGIのPEP?

    23

    ASGIのPEPは?
    今の所予定なし
    (今後は不明)

    View Slide

  24. そもそもASGI?

    24

    WSGI ASGI

    View Slide

  25. そもそもASGI?

    25

    WSGI ASGI
    アプリケーションサーバと
    アプリケーション間の規約


    View Slide

  26. そもそもASGI?

    26

    アプリケーションサー
    バ

    アプリケーション

    フレームワーク

    WSGI

    View Slide

  27. そもそもASGI?

    27

    アプリケーションサー
    バ

    アプリケーション

    フレームワーク

    ASGI
    daphne


    View Slide

  28. そもそもASGI?

    28

    アプリケーションサー
    バ

    アプリケーション

    フレームワーク

    ASGI
    daphne
 LAPRASでは
    daphneを採用中

    View Slide

  29. そもそもASGI?

    29

    WSGIの最小ケース

    def application(environ, start_response):
    body = b'Hello world!\n'
    status = '200 OK'
    headers = [('Content-type', 'text/plain')]
    start_response(status, headers)
    return [body]

    View Slide

  30. そもそもASGI?

    30

    WSGIの最小ケース

    def application(environ, start_response):
    body = b'Hello world!\n'
    status = '200 OK'
    headers = [('Content-type', 'text/plain')]
    start_response(status, headers)
    return [body]
    呼び出し可能で
    2つの引数をとる

    View Slide

  31. そもそもASGI?

    31

    WSGIの最小ケース

    def application(environ, start_response):
    body = b'Hello world!\n'
    status = '200 OK'
    headers = [('Content-type', 'text/plain')]
    start_response(status, headers)
    return [body]
    第2引数のオブジェクトを
    ステータスとヘッダーとともに呼
    び出す

    View Slide

  32. そもそもASGI?

    32

    WSGIの最小ケース

    def application(environ, start_response):
    body = b'Hello world!\n'
    status = '200 OK'
    headers = [('Content-type', 'text/plain')]
    start_response(status, headers)
    return [body]
    レスポンスボディをiterableな
    バイト文字列を戻す

    View Slide

  33. そもそもASGI?

    33

    ASGIの最小ケース(httpの場合)

    async def application(scope, receive, send):
    assert scope['type'] == 'http'
    await send({
    'type': 'http.response.start',
    'status': 200,
    'headers': [
    [b'content-type', b'text/plain'],
    ],
    })
    await send({
    'type': 'http.response.body',
    'body': b'Hello, world!',
    })
    呼び出し可能な非同期オブジェクト scope: 接続の詳細を含むdict
    receive: クライアントからイベントを受信
    send: クライアントへメッセージを送信

    View Slide

  34. そもそもASGI?

    34

    ASGIの最小ケース(httpの場合)

    async def application(scope, receive, send):
    assert scope['type'] == 'http'
    await send({
    'type': 'http.response.start',
    'status': 200,
    'headers': [
    [b'content-type', b'text/plain'],
    ],
    })
    await send({
    'type': 'http.response.body',
    'body': b'Hello, world!',
    })
    scope: 接続の詳細を含むdict
    receive: クライアントからイベントを受信
    send: クライアントへメッセージを送信

    View Slide

  35. そもそもASGI?

    35

    ASGIの最小ケース(httpの場合)

    async def application(scope, receive, send):
    assert scope['type'] == 'http'
    await send({
    'type': 'http.response.start',
    'status': 200,
    'headers': [
    [b'content-type', b'text/plain'],
    ],
    })
    await send({
    'type': 'http.response.body',
    'body': b'Hello, world!',
    })
    WSGIでのstart_responseにあたる
    ヘッダーとステータスをクライアントへ
    送信

    View Slide

  36. そもそもASGI?

    36

    ASGIの最小ケース(httpの場合)

    async def application(scope, receive, send):
    assert scope['type'] == 'http'
    await send({
    'type': 'http.response.start',
    'status': 200,
    'headers': [
    [b'content-type', b'text/plain'],
    ],
    })
    await send({
    'type': 'http.response.body',
    'body': b'Hello, world!',
    })
    レスポンスbodyの送信

    View Slide

  37. そもそもASGI?

    37

    DEMO


    View Slide

  38. Django の ASGI/WSGI

    38


    View Slide

  39. DjangoのWSGI / ASGI

    39

    Djangoのwsgi.py/asgi.py

    asgi_sample/
    ├── __init__.py
    ├── asgi.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

    View Slide

  40. DjangoのWSGI / ASGI

    40

    Djangoのwsgi.py/asgi.py

    asgi_sample/
    ├── __init__.py
    ├── asgi.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py
    createprojectした時点で
    用意されている(3.x以降)

    View Slide

  41. DjangoのWSGI / ASGI

    41

    wsgi.py / asgi.py

    アプリケーションサーバへの
    Djangoのエントリポイント


    View Slide

  42. DjangoのWSGI / ASGI

    42

    wsgi.py 

    https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/gunicorn/

    View Slide

  43. DjangoのWSGI / ASGI

    43

    asgi.py 

    https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/daphne/

    View Slide

  44. DjangoのWSGI / ASGI

    44

    wsgi.py 

    import os
    from django.core.wsgi import get_wsgi_application
    os.environ.setdefault(
    'DJANGO_SETTINGS_MODULE', 'asgi_sample.settings'
    )
    application = get_wsgi_application()

    View Slide

  45. DjangoのWSGI / ASGI

    45

    django.core.handlers.wsgi.WSGIHandler

    class WSGIHandler(base.BaseHandler):
    :
    def __call__(self, environ, start_response):
    set_script_prefix(get_script_name(environ))
    signals.request_started.send(
    sender=self.__class__, environ=environ
    )
    request = self.request_class(environ)
    response = self.get_response(request)
    :
    start_response(status, response_headers)
    :
    return response

    View Slide

  46. DjangoのWSGI / ASGI

    46

    django.core.handlers.wsgi.WSGIHandler

    class WSGIHandler(base.BaseHandler):
    :
    def __call__(self, environ, start_response):
    set_script_prefix(get_script_name(environ))
    signals.request_started.send(
    sender=self.__class__, environ=environ
    )
    request = self.request_class(environ)
    response = self.get_response(request)
    :
    start_response(status, response_headers)
    :
    return response

    View Slide

  47. DjangoのWSGI / ASGI

    47

    asgi.py 

    import os
    from django.core.asgi import get_asgi_application
    os.environ.setdefault(
    'DJANGO_SETTINGS_MODULE', 'asgi_sample.settings'
    )
    application = get_asgi_application()

    View Slide

  48. DjangoのWSGI / ASGI

    48

    django.core.handlers.asgi.ASGIHandler

    class ASGIHandler(base.BaseHandler):
    """Handler for ASGI requests."""
    :
    async def __call__(self, scope, receive, send):
    # Serve only HTTP connections.
    # FIXME: Allow to override this.
    if scope["type"] != "http":
    raise ValueError(
    "Django can only handle ASGI/HTTP
    connections, not %s." % scope["type"]
    )
    async with ThreadSensitiveContext():
    await self.handle(scope, receive, send)

    View Slide

  49. DjangoのWSGI / ASGI

    49

    django.core.handlers.asgi.ASGIHandler

    class ASGIHandler(base.BaseHandler):
    """Handler for ASGI requests."""
    :
    async def __call__(self, scope, receive, send):
    # Serve only HTTP connections.
    # FIXME: Allow to override this.
    if scope["type"] != "http":
    raise ValueError(
    "Django can only handle ASGI/HTTP
    connections, not %s." % scope["type"]
    )
    async with ThreadSensitiveContext():
    await self.handle(scope, receive, send)

    View Slide

  50. Django Channels と ASGIの関係

    50


    View Slide

  51. Django Channels

    51

    https://channels.readthedocs.io/en/stable/

    View Slide

  52. Django Channels

    52

    https://channels.readthedocs.io/en/stable/
    WebSocketなどのプロトコルをサポートするDjango拡張
    ASGIをベースに構成されている

    View Slide

  53. Django Channels

    53

    https://github.com/django/channels/tags

    View Slide

  54. Django Channels

    54

    https://github.com/django/channels/tags
    現行は3.x系

    View Slide

  55. Django Channels

    55

    https://speakerdeck.com/denzow/djangoto
    vuedezuo-rukanbanapurikesiyon?slide=55

    View Slide

  56. Django Channels

    56

    https://speakerdeck.com/denzow/djangoto
    vuedezuo-rukanbanapurikesiyon?slide=55
    LAPRASではDjango 2.x + Channels 2.x系で導入
    現在Channels 3.x以降への移行を検討中

    View Slide

  57. Django Channels

    Channels 2.xはDjango 2.xの時代から


    View Slide

  58. Django Channels

    58

    Channels 2.xはDjango 2.xの時代から

    DjangoのASGIサポートは3.xから


    View Slide

  59. Django Channels

    59

    Channels 2.xはDjango 2.xの時代から

    DjangoのASGIサポートは3.xから

    2.xではChannelsがASGI全体を担う


    View Slide

  60. Django Channels

    60

    https://channels.readthedocs.io/en/2.x/deploying.html

    View Slide

  61. Django Channels

    61

    https://channels.readthedocs.io/en/2.x/deploying.html
    asgi.pyはないので
    自分でつくる

    View Slide

  62. Django Channels

    62

    https://channels.readthedocs.io/en/2.x/deploying.html
    asgi.pyはないので
    自分でつくる

    View Slide

  63. Django Channels

    63

    Channels 2.xはDjango 2.xの時代から

    DjangoのASGIサポートは3.xから

    2.xではChannelsがASGI全体を担う


    View Slide

  64. Django Channels

    64

    Channels 2.xはDjango 2.xの時代から

    DjangoのASGIサポートは3.xから

    3.xからChannelsはDjangoと協調してASGIを担う


    View Slide

  65. Django Channels

    65

    Django 3.x 以降は ASGI対応なんだから

    Channelsはいらないんじゃ?

    🤔

    View Slide

  66. Django Channels

    66

    Djangoのwsgi.py/asgi.py

    asgi_sample/
    ├── __init__.py
    ├── asgi.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

    View Slide

  67. Django Channels

    67

    django.core.handlers.asgi.ASGIHandler

    class ASGIHandler(base.BaseHandler):
    """Handler for ASGI requests."""
    :
    async def __call__(self, scope, receive, send):
    # Serve only HTTP connections.
    # FIXME: Allow to override this.
    if scope["type"] != "http":
    raise ValueError(
    "Django can only handle ASGI/HTTP
    connections, not %s." % scope["type"]
    )
    async with ThreadSensitiveContext():
    await self.handle(scope, receive, send)

    View Slide

  68. Django Channels

    68

    django.core.handlers.asgi.ASGIHandler

    class ASGIHandler(base.BaseHandler):
    """Handler for ASGI requests."""
    :
    async def __call__(self, scope, receive, send):
    # Serve only HTTP connections.
    # FIXME: Allow to override this.
    if scope["type"] != "http":
    raise ValueError(
    "Django can only handle ASGI/HTTP
    connections, not %s." % scope["type"]
    )
    async with ThreadSensitiveContext():
    await self.handle(scope, receive, send)
    DjangoのASGIはHTTPしかサポートしない

    View Slide

  69. Django Channels

    69

    https://channels.readthedocs.io/en/stable/deploying.html#configuring-the-asgi-application 


    View Slide

  70. Django Channels

    70

    https://channels.readthedocs.io/en/stable/deploying.html#configuring-the-asgi-application 

    asgi.pyを書き換えろ(自作しろ)

    View Slide

  71. Django Channels

    71

    :
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
    django_asgi_app = get_asgi_application()
    from chat import AdminChatConsumer, PublicChatConsumer
    application = ProtocolTypeRouter({
    # Django's ASGI application to handle traditional HTTP requests
    "http": django_asgi_app,
    # WebSocket chat handler
    "websocket": AllowedHostsOriginValidator(
    AuthMiddlewareStack(
    URLRouter([
    path("chat/", PublicChatConsumer.as_asgi()),
    ])
    )
    ),
    })

    View Slide

  72. Django Channels

    72

    :
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
    django_asgi_app = get_asgi_application()
    from chat import AdminChatConsumer, PublicChatConsumer
    application = ProtocolTypeRouter({
    # Django's ASGI application to handle traditional HTTP requests
    "http": django_asgi_app,
    # WebSocket chat handler
    "websocket": AllowedHostsOriginValidator(
    AuthMiddlewareStack(
    URLRouter([
    path("chat/", PublicChatConsumer.as_asgi()),
    ])
    )
    ),
    })
    httpはDjango自体のASGI対応に流す

    View Slide

  73. Django Channels

    73

    :
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
    django_asgi_app = get_asgi_application()
    from chat import AdminChatConsumer, PublicChatConsumer
    application = ProtocolTypeRouter({
    # Django's ASGI application to handle traditional HTTP requests
    "http": django_asgi_app,
    # WebSocket chat handler
    "websocket": AllowedHostsOriginValidator(
    AuthMiddlewareStack(
    URLRouter([
    path("chat/", PublicChatConsumer.as_asgi()),
    ])
    )
    ),
    })
    websocketプロトコルはChannelsに流す

    View Slide

  74. Django Channels

    74

    ASGIはサーバとアプリケーション間の非同期規格

    ASGI != WebSocket等のプロトコルサポート


    View Slide

  75. パフォーマンスの話

    75


    View Slide

  76. パフォーマンスの話

    76

    DjangoでWSGIをASGIに書き換えたら早くなるのか


    View Slide

  77. パフォーマンスの話

    77

    Djangoで同期ビューを非同期ビューに書き換えたら早
    くなるのか


    View Slide

  78. パフォーマンスの話

    78

    ● 3.0 → ASGIサポート(AsyncViewサポートなし)

    ● 3.1 → ファンクションベースのAsyncViewサポート

    ● 4.1 → クラスベースビューでのAsyncViewサポート


    View Slide

  79. パフォーマンスの話

    79

    ● 3.0 → ASGIサポート(AsyncViewサポートなし)

    ● 3.1 → ファンクションベースのAsyncViewサポート

    ● 4.1 → クラスベースビューでのAsyncViewサポート

    書き換えの土壌が整った!

    View Slide

  80. パフォーマンスの話

    80

    そもそも書き換えるには

    https://docs.djangoproject.com/en/4.1/topics/class-based-views/#asynchronous-class-based-views
    import asyncio
    from django.http import HttpResponse
    from django.views import View
    class AsyncView(View):
    async def get(self, request, *args, **kwargs):
    await asyncio.sleep(1)
    return HttpResponse("Hello async world!")

    View Slide

  81. パフォーマンスの話

    81

    そもそも書き換えるには

    https://docs.djangoproject.com/en/4.1/topics/class-based-views/#asynchronous-class-based-views
    import asyncio
    from django.http import HttpResponse
    from django.views import View
    class AsyncView(View):
    async def get(self, request, *args, **kwargs):
    await asyncio.sleep(1)
    return HttpResponse("Hello async world!")

    View Slide

  82. パフォーマンスの話

    82

    そもそも書き換えるには

    https://docs.djangoproject.com/en/4.1/topics/class-based-views/#asynchronous-class-based-views
    import asyncio
    from django.http import HttpResponse
    from django.views import View
    class AsyncView(View):
    async def get(self, request, *args, **kwargs):
    await asyncio.sleep(1)
    return HttpResponse("Hello async world!")
    非同期対応のライブラリに
    差し替える必要がある

    View Slide

  83. パフォーマンスの話

    83

    そもそも書き換えるには

    https://docs.djangoproject.com/en/4.1/topics/class-based-views/#asynchronous-class-based-views
    import asyncio
    from django.http import HttpResponse
    from django.views import View
    class AsyncView(View):
    async def get(self, request, *args, **kwargs):
    await asyncio.sleep(1)
    return HttpResponse("Hello async world!")
    同期処理はsync_to_asyncデコレータを
    使う必要もある

    View Slide

  84. パフォーマンスの話

    84

    そもそも非同期のメリットを得られるのは

    CPUバウンドな処理ではなくI/Oバウンドな処理

    例: 外部API連携、ファイルI/O..


    View Slide

  85. パフォーマンスの話

    85

    Djangoで一番I/Oに関わるのはORM


    View Slide

  86. パフォーマンスの話

    86

    Djangoで一番I/Oに関わるのはORM

    非同期対応は道半ば…

    View Slide

  87. パフォーマンスの話

    87

    Djangoで一番I/Oに関わるのはORM

    非同期対応は道半ばだが
    4.1で非同期用の
    インターフェース導入

    View Slide

  88. パフォーマンスの話

    88

    Djangoで一番I/Oに関わるのはORM

    今後よりネイティブにAsyncを
    使える未来が近い

    View Slide

  89. パフォーマンスの話

    89

    ASGIを全力で活かしたいが、いま時点で
    は書き換えのコストを払えない

    😭

    View Slide

  90. パフォーマンスの話

    90

    来年、Viewを全部Asyncしてみた、みたい
    な話で登壇できたらいいなぁ…

    😇

    View Slide

  91. まとめ

    91


    View Slide

  92. まとめ

    ● ASGIはWSGIの精神的後継

    ● WebSocketしたいなら今もChannelsが必要

    ● Djangoは3.0からASGIをサポートしサポート範囲がどんどん増えて
    いる

    ● Asyncによるメリットを得やすい土壌は整ってきた

    ○ Async ORMの今後の拡充に期待

    ○ LAPRASのViewを全部Asyncに書き換えたい人募集

    92


    View Slide

  93. 宣伝

    93

    このあとすぐ!

    View Slide