今まで、Alpine Linux + uWSGI で Django のイメージを作ることが多かったのですが、
Alpine Linux で Python を実行すると遅い問題があります。
また、その Alpine 上で起動する HTTPサーバには、 uWSGI を使っていたのですが、設定が複雑で、Kubernetes でサービスするには冗長な気になっいました。
そのため、Django サービスの環境を一通り変更しました。
ベースイメージは、Alpine から Debian ベースの Python に変更し、HTTP サーバは Daphne を使うことにしました。
追記: Daphne だと並列リクエストが受けられないため、後日 Uvicorn に変えました
マルチステージビルドにし、Python のフルイメージで pipenv install ( pipenv sync ) を行い、成果物を python のスリムイメージを元とした公開用ステージにコピーする形としました。
今まで Alpine にソースコードや依存関係、uWSGI を含めて入れてビルドした場合、私のアプリでは 277MB。
python:3.10-bullseye + python:3.10-slim-bullseye のマルチステージビルドした場合、最終イメージは 313MB。
容量としては若干増えましたが、ほぼかわらないサイズでした。
Dockerfile は下に書きます。
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 は、連続のリクエストが来た時に順番にコルーチンで処理します。すべて 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 は以下のようになりました。
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 の設定の 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 でサービスする仕組みを構築する必要があります。
コメント