Django の Docker 環境を Alpine + uWSGI から Debian + Daphne に変えた → やっぱり uvicorn

投稿者: ytyng 1年, 7ヶ月 前

今まで、Alpine Linux + uWSGI で Django のイメージを作ることが多かったのですが、
Alpine Linux で Python を実行すると遅い問題があります。

また、その Alpine 上で起動する HTTPサーバには、 uWSGI を使っていたのですが、設定が複雑で、Kubernetes でサービスするには冗長な気になっいました。

そのため、Django サービスの環境を一通り変更しました。

ベースイメージは、Alpine から Debian ベースの Python に変更し、HTTP サーバは Daphne を使うことにしました。

追記: Daphne だと並列リクエストが受けられないため、後日 Uvicorn に変えました

Docker イメージの変更

Alpine → Python (Debian) に変更

マルチステージビルドにし、Python のフルイメージで pipenv install ( pipenv sync ) を行い、成果物を python のスリムイメージを元とした公開用ステージにコピーする形としました。

今まで Alpine にソースコードや依存関係、uWSGI を含めて入れてビルドした場合、私のアプリでは 277MB。
python:3.10-bullseye + python:3.10-slim-bullseye のマルチステージビルドした場合、最終イメージは 313MB。
容量としては若干増えましたが、ほぼかわらないサイズでした。

Dockerfile は下に書きます。

HTTPサーバの変更

uWSGI → Daphne に変更

uWSGI は非常に良いライブラリだと思いますが、チューニング項目が多く、使っていく上で少し疲れます。

uWSGI は特性上、レスポンスを繰り返すごとにメモリが増えていく傾向があり、それを防ぐためにある一定回数のリクエストを受け取った時にワーカーを再起動させることができます(max-requests)。メモリリークと言いましたか? 違います。GCです。

ワーカーの再起動時、そのワーカーは一時的にサービス不能になりますが、他のワーカーが生きていれば全体的なサービス停止は防げます。しかし、一定リクエスト回数ごとに動作するため、近いタイミングで発生することが多く、結局サービスが停止してしまします。

再起動のリクエスト数閾値をワーカーごとにずらすオプション(max-requests-delta)があり、それを適用すればサービス停止は避けられるはず…なのですが、この設定は最新ビルドで使えません。何年も前にドキュメントに掲載されているのに、ずっと使えてなかった(効いてるものと思っていた) オプションです。

そのため、これを期に別のアプリケーションサーバに変えることにしました。

候補としては、定番の guinicorn の他に、FastAPI で使われる Uvicorn, Hypercorn, あとは Django チームが開発している Daphne というものがあり、今回は Daphne にしました。

Daphne, Uvicorn, Hypercorn, は、ともに ASGI をサポートしており、Django 3 以降の主流のサーバとなっています。

今回は、 Django を使うため、Django チームが開発している Daphne を採用しました。

追記: Daphne → Uvicorn に変えました

Daphne は、連続のリクエストが来た時に順番にコルーチンで処理します。すべて async の View であれば問題無いと思うのですが、既存サービスではそうなってないため、並列リクエストが処理できない問題がありました。

シングル Pod で動かすアプリも多いため、応答があまり良くなかったので、複数ワーカーの起動が簡単な Uvicorn に変えました。

静的コンテンツ配信

uWSGI から Daphne に変更するにあたり、静的コンテンツをどうやってサービスするかの問題があります。

実際に多くのお客さんが使う大規模なサービスでは、静的コンテンツは CloudFront + S3 のような構成でサービスすることが多いと思います。その場合は問題にはなりませんが、社内ツールや管理サイトのような小さなサービスは、よりシンプルな形での静的ファイルサービスをしたい所です。

uWSGI は、static-map という静的ファイルを簡易的にサービスする機能があり、社内ツール等に使う分にはぴったりでいつも使っていました。ただし、 Daphne には静的ファイルのサービスはありません。

Daphne の前に Nginx で受け、Nginx 内で静的コンテンツと Django リクエストを振り分けるのも良いと思いますが、デーモンをあまり増やしたくはなかったため、別の方法を探しました。

比較的最近人気のあるソリューションと思われるのは、WhiteNoise というアプリケーションです。

WhiteNoise は、Python で書かれた静的コンテンツサーバで、Django の MiddleWare に差し込んで簡単に使うことができます。

Python で静的コンテンツサーバを実行するなんてナンセンスじゃないか、という気がしなくもないですが、その回答は公式ドキュメントにあります。

https://whitenoise.evans.io/en/stable/#infrequently-asked-questions

S3 でも Nginx でもない選択肢として、まさに私のニーズとしてはぴったりでした。

Dockerfile

Dockerfile は以下のようになりました。

FROM python:3.10-bullseye AS builder

# Pipfileをコピー
COPY Pipfile /tmp/Pipfile
COPY Pipfile.lock /tmp/Pipfile.lock

# pipenv sync
# プロジェクトによっては pipenv install --system --ignore-pipfile --deploy
RUN python3 -m pip install pipenv \
&& PIPENV_PIPFILE=/tmp/Pipfile pipenv sync --system \
&& python3 -m pip install uvicorn
# 前: && python3 -m pip install daphne


FROM python:3.10-slim-bullseye

# ビルダーステージから、MySQL クライアントに必要な SO をコピー
COPY --from=builder \
/usr/lib/x86_64-linux-gnu/libmariadb.a \
/usr/lib/x86_64-linux-gnu/libmariadb.so.3 \
/usr/lib/x86_64-linux-gnu/
COPY --from=builder /usr/lib/x86_64-linux-gnu/libmariadb3/ \
/usr/lib/x86_64-linux-gnu/libmariadb3/
# ビルダーステージから、Pipenv でインストールしたライブラリをコピー
COPY --from=builder /usr/local/lib/python3.10/site-packages \
/usr/local/lib/python3.10/site-packages
COPY --from=builder /usr/local/lib/python3.10/lib-dynload \
/usr/local/lib/python3.10/lib-dynload
COPY --from=builder /usr/local/bin /usr/local/bin

# SOのシンボリックリンクを作っておく
RUN ln -s /usr/lib/x86_64-linux-gnu/libmariadb.a \
/usr/lib/x86_64-linux-gnu/libmariadbclient.a \
&& ln -s /usr/lib/x86_64-linux-gnu/libmariadb.so.3 \
/usr/lib/x86_64-linux-gnu/libmariadb.so \
&& ln -s /usr/lib/x86_64-linux-gnu/libmariadb.so.3 \
/usr/lib/x86_64-linux-gnu/libmariadbclient.so

COPY my_app /var/app/my_app
RUN chown -R nobody:nogroup /var/app

USER nobody
WORKDIR /var/app/my_app
RUN cd /var/app/my_app && python3 ./manage.py collectstatic --noinput
EXPOSE 8002
CMD ["uvicorn", \
"my_app.asgi:application", \
"--host", "0.0.0.0", \
"--port", "8002", \
"--workers", "4" \
]

# 前: CMD ["daphne", "-b", "0.0.0.0", "-p", "8002", "my_app.asgi:application"]

Django で WhiteNoise を動かす

Django の設定の MIDDLEWARE の中に、

whitenoise.middleware.WhiteNoiseMiddleware

を追加することで、静的ファイルがホストされます。

Using WhiteNoise with Django - WhiteNoise 6.2.0 documentation

キャッシュ時間の延長

WhiteNoise のキャッシュヘッダーの寿命 ( max-age ) は、デフォルトで 

60 if not settings.DEBUG else 0

となっています。

http://whitenoise.evans.io/en/stable/django.html#WHITENOISE_MAX_AGE

60秒だと短いと思いますので、7日に変更します。

WHITENOISE_MAX_AGE = 86400 * 7

(本番用 settings の中で設定)

メディアサーバ

WhiteNoise は、Django の MEDIA_URL をサーブできるようにはなっていません。

http://whitenoise.evans.io/en/stable/django.html#serving-media-files

理由は上記ページに書いてある通りです。そのため、メディアを扱う場合は、django-storages などと連携し、S3 や nginx でサービスする仕組みを構築する必要があります。

現在未評価

コメント

アーカイブ

2024
2023
2022
2021
2020
2019
2018
2017
2016
2015
2014
2013
2012
2011