Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第4章 非同期処理と Uvicorn 運用設計

FastAPI が高いパフォーマンスを発揮できる理由は、Python の非同期処理機能(async / await)と、超高速な ASGI サーバー Uvicorn の組み合わせにあります。

本章では、非同期処理の正しい使い分け、やってはいけないアンチパターン、そして Uvicorn を本番環境で安定して稼働させるための運用設計を解説します。

4.1 async def と通常の def の使い分け

FastAPI では、エンドポイント関数を async def で定義することも、通常の def で定義することも可能です。

FastAPI は、関数の宣言方法に応じて内部での実行方式を自動的に切り替えます。

  • async def: 非同期 I/O ライブラリ(例: httpx による外部 API 通信、非同期 DB ドライバ asyncpg など)を await する場合に使用します。メインのイベントループ上で高速に並行実行されます。
  • 通常の def: 同期ライブラリ(例: 従来の同期型 DB ドライバ、ファイル読み書き、重い計算処理など)を扱う場合に使用します。FastAPI は自動的にスレッドプールへ処理を逃がし、メインのイベントループがブロックされるのを防ぎます。
import asyncio
from fastapi import FastAPI
import httpx

app = FastAPI()


# 非同期I/O: await可能な処理がある場合
@app.get("/async-fetch")
async def fetch_external_data():
    async with httpx.AsyncClient() as client:
        res = await client.get("https://httpbin.org/delay/1")
        return {"status": res.status_code}


# 同期処理: 外部スレッドプールで安全に実行される
@app.get("/sync-calculation")
def compute_heavy_math():
    result = sum(i * i for i in range(1_000_000))
    return {"result": result}

4.2 イベントループをブロックするアンチパターン

非同期プログラミングで最も注意しなければならないのが、async def の中でブロッキング処理(同期的な待ち時間)を実行してしまうことです。

例えば、async def の中で標準の time.sleep() や同期的な requests.get() を呼び出すと、イベントループ全体が停止し、その間に届いた他のすべてのユーザーのリクエストが完全に待たされてしまいます。

# 悪い例: イベントループ全体を止めてしまう
@app.get("/bad")
async def bad_endpoint():
    import time

    time.sleep(3)  # 全員のリクエストが止まる!
    return {"message": "bad"}


# 良い例: 非同期スリープを使用する
@app.get("/good")
async def good_endpoint():
    import asyncio

    await asyncio.sleep(3)  # 他のリクエストは並行して処理される
    return {"message": "good"}

4.3 Uvicorn の起動オプションと基本設定

Uvicorn は uvloop(C言語で書かれた超高速イベントループ)と httptools を内蔵した ASGI サーバーです。

開発環境と本番環境では、起動オプションを適切に使い分けます。

# 開発時: コードの変更を検知して自動再起動 (--reload)
uvicorn main:app --host 127.0.0.1 --port 8000 --reload

# 本番時: 複数ワーカープロセスで起動 (--workers)
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4

主なオプションの意味は以下のとおりです。

  • --host: 待ち受ける IP アドレスを指定します。コンテナ環境(Docker)では外部からの通信を受け付けるため 0.0.0.0 を指定します。
  • --port: ポート番号(既定値は 8000)を指定します。
  • --workers: 起動するワーカープロセスの数です。Python は GIL(Global Interpreter Lock)があるため、CPU コア数を有効活用するには複数プロセスで起動します。

4.4 本番環境でのプロセス管理とワーカー数設計

本番サーバーのスペックに応じて、適切なワーカー数を設計します。

推奨される一般的なワーカー数の計算式は以下のとおりです。

$$ \text{ワーカー数} = (2 \times \text{CPU コア数}) + 1 $$

例えば 2 コアのサーバーであれば 4〜5 ワーカーが目安です。

また、本番環境でプロセスの異常終了検知やグレースフルリスタートを行いたい場合は、プロセス管理機構として Gunicorn を親プロセスにし、ワーカーとして UvicornWorker を動作させる構成が広く採用されています。

gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000

4.5 リバースプロキシとヘルスチェック

本番運用では、Uvicorn の前段に Nginx や Cloudflare、AWS ALB などのリバースプロキシを配置し、SSL/TLS 暗号化解除や静的ファイル配信を任せます。

ロードバランサーからの死活監視に対応するため、必ず専用のヘルスチェック用エンドポイントを用意します。

@app.get("/healthz", tags=["System"])
def health_check():
    return {"status": "ok"}