Djangoで作成したWebアプリケーションをCloud Runで実行して,Cloud SQL postgresに接続する負荷テストを実施していたところ,以下のエラーが発生しました.
psycopg2.OperationalError: FATAL: remaining connection slots are reserved for non-replication superuser connections
どうやらデータベースの最大同時接続数を超えたようです.
Cloud RunでMax instancesを8,Concurrencyを8,gunicornでworkersを1,threadsを8に設定しており,db-f1-microの最大同時接続数は25です*1.
1個のCloud Runインスタンスからデータベースに対して1個の接続ならば,8 < 25 なので上記のエラーは発生しないはずですが,どうやら1個のスレッドに対して1個の接続が存在するために8 * 8 > 25 のためエラーとなったようです.また,データベースのCPU使用率は50%程度に上昇していました.
データベースの同時接続数を減らす
Gunicornのドキュメントにworker_class
の説明があり,同期と非同期について記載されています.
Settings — Gunicorn 20.0.4 documentation
Design — Gunicorn 20.0.4 documentation
非同期処理に対応したworkerを使用するにあたって,psycopg2に対応している必要があります.非同期処理で複数のリクエストを処理している1個のprocessに対して1個のデータベース接続を確立するようにできれば,データベースの同時接続数を減らすことができます.このことは,psycogreenに詳しく記載があります.
要約すると,eventletはそのままpsycopg2に対応していて,gevent はmonkeypatchすれば対応するようで,uWSGIはそのまま対応しているようです*2.
eventletを試しましたが問題があり起動しませんでしたので,uWSGIを試しました.以下のソースコードがCloud Runで動作しました.
uwsgi.conf.yaml
uwsgi: http: 0.0.0.0:$(PORT) chdir: /app/ module: sample.wsgi:application http-keepalive: 620 master: true processes: 1
settings.py
DATABASES
のCONN_MAX_AGE
を指定するのが重要です.
Databases | Django documentation | Django
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'sample-db', 'USER': 'sample-user', 'PASSWORD': "password", 'HOST': os.environ.get("DB_HOST"), 'PORT': os.environ.get("DB_PORT"), 'CONN_MAX_AGE': 60 } }
requirements.txt
django==3.1.5 psycopg2-binary==2.8.6 uWSGI==2.0.19.1
Dockerfile
... CMD ["uwsgi","uwsgi.conf.yaml"]
Quickstart for Python/WSGI applications — uWSGI 2.0 documentation
Cloud Runでmax instance 8, concurrency 4 に設定し,データベースの読み出しを行うリクエストを用いて負荷試験を行ったところ,少なくとも600 rpsの性能が確認され,このときのデータベースの同時接続数は最大で11でした.1個のインスタンスが複数のリクエストを処理するために1個のデータベース接続を用いてることが分かります.この間のデータベースのCPU使用率は10%以下で,効率よくデータベースにアクセスできるようになりました.
Djangoのソースコードは一切変更することなく,起動方法をGunicornからuWSGIに変更するだけで,パフォーマンスが向上したのは驚きです.
トレンド
Google Trendsで比較すると,最近はGunicornの方が人気のようですが,みーはuWSGIが気に入りました.