Cloud Run 用に Python アプリケーションを最適化する

このガイドでは、Python プログラミング言語で作成された Cloud Run サービスの最適化と、最適化の一部に関連したトレードオフの理解に役立つ背景情報について説明します。このページの情報は、Python にも適用される全般的な最適化のヒントを補完するものです。

これらの従来の Python ウェブベース アプリケーションのベスト プラクティスと最適化の多くは、以下を中心に展開されています。

  • 同時リクエストの処理(スレッドベースと非ブロッキング I/O の両方)。
  • バックグラウンド タスクへのトレースや指標の送信など、重要性が低い機能に接続プールとバッチ処理を適用してレスポンス レイテンシを短縮する。

コンテナ イメージを最適化する

コンテナ イメージを最適化すると、負荷を軽減し、起動時間を短縮できます。次の方法でイメージを最適化できます。

  • アプリが実行時に必要とするものだけをコンテナに入れる
  • WSGI サーバーを最適化する

アプリが実行時に必要としているものだけをコンテナに入れます

コンテナに含まれているコンポーネントがサービスの実行に必要かどうかを検討してください。コンテナ イメージを最小限に抑えるには、いくつかの方法があります。

  • 小さなベースイメージを使用する
  • サイズの大きいファイルをコンテナ外に移動する

小さなベースイメージを使用する

Docker Hub には、コンテナ内のソースから Python をインストールしない場合に使用できる公式の Python ベースイメージが数多く用意されています。これらは Debian オペレーティング システムをベースにしています。

Docker Hub の python イメージを使用している場合は、slim バージョンの使用を検討してください。これらのイメージには、ホイールの作成に使用されるパッケージ(アプリケーションに不要なパッケージなど)が数多く含まれていないため、イメージのサイズは小さくなります。たとえば、Python イメージには、GNU C コンパイラ、プリプロセッサ、コア ユーティリティが含まれています。

ベースイメージに含まれているパッケージをサイズの大きい順に 10 個まで表示するには、次のコマンドを実行します。

DOCKER_IMAGE=python # or python:slim
docker run --rm ${DOCKER_IMAGE} dpkg-query -Wf '${Installed-Size}\t${Package}\t${Description}\n' | sort -n | tail -n10 | column -t -s $'\t'

これらの低レベルパッケージは少数のため、slim ベースのイメージは、潜在的な脆弱性に対する攻撃対象領域も少なくなります。これらのイメージには、ソースからホイールを作成するために必要な要素が含まれていないことがあります。

特定のパッケージを再び追加するには、Dockerfile に RUN apt install 行を追加します。Cloud Run でのシステム パッケージの使用方法の詳細をご覧ください。

Debian 以外のコンテナ用のオプションもあります。python:alpine オプションを使用すると、コンテナのサイズをかなり小さくできますが、Python パッケージの多くは、alpine ベースのシステムをサポートするコンパイル済みのホイールを備えていない可能性があります。サポートは改善されていますが(PEP-656 を参照)、継続的に変更されています。パッケージ マネージャー、シェル、その他のプログラムを含まない distroless base image の使用も検討できます。

サイズの大きいファイルをコンテナ外に移動する

メディア アセットなどのサイズの大きいファイルをベースコンテナに含める必要はありません。

Google Cloud には、Cloud Storage などの複数のホスティング オプションがあり、このような大きなアイテムを保存することもできます。こうしたサービスにサイズの大きなアセットを移動し、実行時にアプリケーションから参照できるようにします。

WSGI サーバーを最適化する

Python は、WSGI 標準 PEP-3333 の実装により、アプリケーションとウェブサーバーの連携方法を標準化しました。サンプル ドキュメントでよく使用されている WSGI サーバーの一つが gunicorn です。

gunicorn を最適化する

Dockerfile に次の CMD を追加して、gunicorn の呼び出しを最適化します。

CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app

この設定を変更する場合は、アプリケーションごとにワーカーとスレッドの数を調整します。たとえば、使用可能なコア数と同じ数のワーカーを使用して、パフォーマンスが改善されることを確認してから、スレッド数を調整します。ワーカーまたはスレッドが多すぎると、コールド スタートのレイテンシが長くなる、メモリの消費量が増える、1 秒あたりのリクエストが少なくなる、などのデメリットが生じる可能性があります。

デフォルトでは、gunicorn は起動時にワーカーを生成し、アプリケーション コードを評価する前でも指定されたポートをリッスンします。この場合、Cloud Run のデフォルトの起動プローブは、$PORT でリッスンを開始するとすぐに、コンテナ インスタンスを正常な状態としてマークします。このため、サービスにカスタム起動プローブを設定する必要があります。

この動作を変更する場合は、--preload 設定を使用して gunicorn を呼び出し、リッスンを開始する前にアプリケーション コードを評価します。これには次のような利点があります。

  • デプロイ時に重大なランタイムバグを特定する
  • メモリリソースを保存する

これを追加する前に、アプリケーションがプリロードする対象を検討する必要があります。

他の WSGI サーバー

コンテナで Python を実行する場合、gunicorn の使用に制限はありません。コンテナ ランタイムの契約に従い、コンテナが HTTP ポート $PORT をリッスンしている場合は、WSGI または ASGI ウェブサーバーを使用できます。

一般的な代替手段としては、uwsgiuvicornwaitress があります。

たとえば、app オブジェクトを含む main.py というファイルの場合、次の呼び出しで WSGI サーバーを起動します。

# uwsgi: pip install pyuwsgi
uwsgi --http :$PORT -s /tmp/app.sock --manage-script-name --mount /app=main:app

# uvicorn: pip install uvicorn
uvicorn --port $PORT --host 0.0.0.0 main:app

# waitress: pip install waitress
waitress-serve --port $PORT main:app

これらは、DockerfileCMD exec 行として追加できます。また、Google Cloud buildpacks を使用する場合は Procfileweb: エントリとして追加することもできます。

アプリケーションを最適化する

Cloud Run サービスコードにより、起動時間とメモリ使用量を最適化することもできます。

スレッドを減らす

メモリを最適化するには、スレッド数を減らし、非ブロッキング型の事後対応戦略を使用して、バックグラウンド アクティビティを回避します。また、一般的なヒントのページで説明されているように、ファイル システムへの書き込みも回避します。

Cloud Run サービスでバックグラウンド アクティビティをサポートする場合は、リクエストの外部でもバックグラウンド アクティビティを実行できて、CPU にアクセスできるように、Cloud Run サービスの CPU を常に割り当てます。

起動タスクを減らす

Python ウェブベース アプリケーションでは、起動中に完了するタスクが多数あります。たとえば、データのプリロード、キャッシュのウォームアップ、接続プールの確立などです。これらのタスクは順次実行すると、遅くなる原因になります。ただし、同時に実行するには、CPU コアの数を増やす必要があります。

Cloud Run は現在、コールド スタート インスタンスをトリガーするために実際のユーザー リクエストを送信しています。新しく開始したインスタンスにリクエストが割り当てられているユーザーは、大幅な遅延が発生する可能性があります。Cloud Run には現在、準備未完了のアプリケーションに対するリクエスト送信を回避するための「準備」チェックがありません。

次のステップ

その他のヒントについては、以下を参照してください。