Pythonで使うGranian入門:Uvicornを置き換えるRust製ASGI Webサーバー

Python tutorial - IT technology blog
Python tutorial - IT technology blog

背景:Uvicornが限界を迎え始めた時

約半年前、私たちのコアAPIクラスターは毎日11時から13時のピーク時間帯に度々過負荷に陥っていました。当時の技術スタックは極めて一般的で、Kubernetes上でGunicornによって管理されるUvicornワーカーの背後にFastAPIを配置した構成でした。しかし、トラフィックが急増して毎秒12,000リクエストに達すると、システムが不安定になり始めました。PodのCPU使用率は90〜95%まで跳ね上がり、p99レイテンシは35msから一気に420msにまで悪化しました。

ボトルネックはデータベースやクエリではなく、PythonのHTTPサーバーランタイム層そのものにありました。FastAPIやuvloopがどれほど高度に最適化されていても、Python自身がHTTPパース、ソケット管理、イベントループのスケジューリング全般を担わなければなりません。同時接続数が増加するにつれて、PythonのGIL(グローバルインタプリタロック)とオーバーヘッドの弱点が顕著に現れてしまったのです。

そこで私たちはGranianを検証し、本番環境への導入を決断しました。

GranianはRustで完全に記述されたPython向けのHTTPサーバーであり、定評のある2つのネットワークライブラリhyperとtokioをベースに構築されています。このアーキテクチャにより、PythonはI/Oの負荷から完全に解放されます。TCP接続の受付、HTTP/1やHTTP/2のパース、TLS終端といった処理はすべてネイティブなRust層で処理されます。Pythonアプリケーションは、すでにパース済みのリクエストペイロードを受け取ってビジネスロジックを実行するだけで済みます。

その効果は歴然でした。同一のPod構成(2 vCPU、2GB RAM)において、Granianに移行したことでアプリケーションは2倍の負荷に耐えられるようになりました。p99レイテンシは80ms未満にまで低下し、メモリ消費量も驚くほど安定しました。

Granianのインストール

Granianはビルド済みのバイナリwheelとしてPyPI上で提供されています。サーバーにcargoやrustcをインストールすることなく、すぐに利用可能です。

# pipでのインストール
pip install granian

# Poetryを使用する場合
poetry add granian

# uvを使用する場合(wheelのダウンロードが極めて高速なため推奨)
uv pip install granian

インストール後にバイナリを確認します:

granian --version

動作検証用に、FastAPIでシンプルなmain.pyを作成します:

from fastapi import FastAPI

app = FastAPI(title="Granian Performance Demo")

@app.get("/")
async def root():
    return {"status": "ok", "server": "Granian", "engine": "Rust Tokio"}

@app.get("/compute")
async def compute():
    # 軽量な処理をシミュレート
    total = sum(i * 2 for i in range(1000))
    return {"result": total}

本番環境向けの詳細設定

Granianは3つのインターフェースをサポートしています。ASGI(FastAPI、Starlette)、WSGI(Flask、Django)、そしてRSGI(最大のスループットを実現するためにGranianの作者が独自に設計したインターフェース仕様)です。現在のほとんどのプロジェクトでは、ASGIが最も安全で互換性に優れた選択肢となります。

1. 基本的な起動方法

granian --interface asgi main:app --host 0.0.0.0 --port 8000

2. WorkerとThreadの最適化設定

Granianはworkers(独立したプロセス)とthreads(各ワーカー内のTokioランタイムスレッド)を組み合わせて動作します。これは、Uvicornのシングルスレッドイベントループ構造に対する大きなアドバンテージです:

granian --interface asgi main:app \
    --host 0.0.0.0 \
    --port 8000 \
    --workers 2 \
    --threads 2 \
    --backlog 4096 \
    --http 1 \
    --opt

主要パラメータの解説:

  • --workers: 独立したワーカープロセス数。Kubernetesなどのコンテナ環境(一般的に2 vCPUを割り当てる場合)では、2が理想的な数値です。ワーカー数を増やしすぎると、CPUのコンテキストスイッチのオーバーヘッドが発生するため避けてください。
  • --threads: 各ワーカー内のTokioスレッド数。これらのスレッドはPythonのGILに束縛されることなく、Rust層でネットワークI/Oを並行処理します。
  • --backlog 4096: TCPの接続キュー長。この設定により、一時的なトラフィックスパイクが発生してもクライアント接続を切断することなく受け止めることができます。
  • --opt: Rust(PyO3)とPythonランタイム間の内部データ変換を最適化するフラグを有効にします。
  • --http 1: NginxやIngress Controllerの背後に配置されている場合にHTTP/1.1の使用を強制し、プロトコルネゴシエーションのオーバーヘッドを削減します。

3. 本番環境向けDockerfileの構築

以下は、レイヤーキャッシュを最適化した実用的かつ軽量なDockerfileの構成例です:

FROM python:3.11-slim as base

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=off

WORKDIR /app

# コンテナのヘルスチェック用にcurlをインストール
RUN apt-get update && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

EXPOSE 8000

# 2ワーカー、2スレッドを設定し、未使用の場合はWebSocketを無効化
CMD ["granian", "--interface", "asgi", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2", "--threads", "2", "--no-ws"]

--no-wsフラグはWebSocketサブシステムを完全に無効化します。一般的なREST APIのみを提供する場合は、このフラグを指定することで接続ごとのメモリ消費とCPUサイクルをさらに節約できます。

ベンチマークと実測評価

インターネット上のベンチマーク結果を鵜呑みにせず、本番トラフィックを切り替える前に必ず自身のインフラ環境で測定を行ってください。

1. wrkによるスループット計測

同一プライベートネットワーク内の別ノードからwrkを実行してください。負荷テストツールがWebサーバーとCPUリソースを奪い合ってしまうのを防ぐため、同一マシン上での実行は避ける必要があります:

# 内部IPに対して8スレッド・200同時接続で30秒間実行
wrk -t8 -c200 -d30s http://10.0.1.50:8000/compute

軽量な処理を想定した/computeエンドポイントでのテストシナリオにおいて、Gunicorn + Uvicornワーカー構成が約16,200 RPSであったのに対し、Granianは約28,500 RPSに達しました(約75%の向上)。最も印象的だったのはレイテンシの分布です。Granianのp95およびp99のグラフは非常に平坦で、イベントループの滞留に起因するスパイクは一切見られませんでした。

2. メモリ消費のモニタリング

各ワーカーのメモリ使用量を直接確認します:

# プロセスの詳細確認
ps aux | grep granian

# topコマンドによるリアルタイムのリソース監視
top -p $(pgrep -d',' -f granian)

実際の運用では、アイドル時のGranianワーカーあたりのメモリ使用量は55〜70MB程度に抑えられていました。負荷を5,000 RPSまで引き上げた際も約95MBまで増加した後に横ばいとなり、Rust層におけるメモリリークの兆候はまったく見られませんでした。

3. アクセスログの管理

高負荷時にアクセスログをコンソールへ同期的に出力すると、コンテナのI/Oボトルネックとなる恐れがあります。本番環境ではデフォルトのアクセスログを無効化し、デバッグ時にのみ有効化することをお勧めします:

# 本番環境での実行:I/Oを最適化するためログレベルをwarningに設定し、アクセスログを無効化
granian --interface asgi main:app \
    --host 0.0.0.0 \
    --port 8000 \
    --log-level warning \
    --no-access-log

Granianは、現代のPython ASGIシステムにとって極めて有力なアップグレードの選択肢です。Pythonのコードを一行も書き換えることなく、Rustの圧倒的なパフォーマンスを手に入れることができます。

Share: