404 Not Found

404 Not Found


nginx

パフォーマンス最適化 — 100から100万QPSへ

パフォーマンス最適化は高速道路の建設のようなもの - 最も混雑している区間 (ボトルネック)から修正し, 各区間の完了後に結果を測定するのであり, 道路全体を無差別に拡幅するわけではありません。見込みのある最適化は無駄であり, 計測に基づく最適化こそが最も効率的です。

1. 学ぶ内容


2. Aliceの実話

(1) 悩み:単一サーバーの500 QPSでは足りない

PriceTrackerの本番運用開始後, 単一サーバーでは500 QPSしか処理できませんでしたが, セールイベント時にはトラフィックが5,000 QPSに急増し, APIレスポンスがタイムアウトしていました。Charlieはサーバーを追加しましたが, 改善は微々たるものでした - データベースコネクションプールが枯渇し, 同期コードがイベントループをブロックし, JSONシリアライズがCPUの40%を消費していました。

(2) 体系的なパフォーマンス最適化アプローチ

パフォーマンス最適化は推測ではなく, 計測 → ボトルネック特定 → 最適化 → 検証のサイクルです。コードレベル (非同期/シリアライズ)からデータベースレベル (インデックス/コネクションプール), そしてアーキテクチャレベル (Workers/負荷分散)まで, 段階的に問題に取り組みます。

(3) 成果

PriceTrackerのQPSは500から10,000 (単一サーバー)に最適化され, 負荷分散クラスタで100万QPSに到達。P99レイテンシは500 msから30 msに短縮されました。


3. パフォーマンス最適化のレベル

(1) 3層最適化モデル

100%
graph TD
    L3[インフラ層] --> L2[データベース層]
    L2 --> L1[アプリケーション層]
    
    L1 --- A1[async/await - ブロック検出]
    L1 --- A2[シリアライズ - orjson]
    L1 --- A3[圧縮 - GZip]
    
    L2 --- D1[インデックス - クエリ最適化]
    L2 --- D2[コネクションプール - サイズチューニング]
    L2 --- D3[クエリパターン - N+1防止]
    
    L3 --- I1[Workers - Gunicorn設定]
    L3 --- I2[ロードバランサー - Nginx]
    L3 --- I3[オートスケーリング - K8s HPA]

(2) QPS向上のパス

100%
flowchart LR
    A[500 QPS\nベースライン] -->|async + orjson| B[2000 QPS]
    B -->|DBインデックス + プール| C[10000 QPS]
    C -->|4 Workers| D[40000 QPS]
    D -->|Nginx LB x 5| E[200000 QPS]
    E -->|K8s x 5 pods| F[1000000 QPS]
フェーズ 最適化手法 QPS 向上倍率
ベースライン デフォルト設定 500 1x
アプリケーション層 async + orjson 2,000 4x
データベース層 インデックス + コネクションプール 10,000 20x
Workers 4 Workers + Gunicorn 40,000 80x
負荷分散 Nginx 5インスタンス 200,000 400x
K8s弾力性 5 Pods オートスケーリング 1,000,000 2000x

4. アプリケーション層の最適化

(1) 非同期ブロッキングのトラブルシューティング

(1) ▶ サンプル:イベントループをブロックする同期コード

PYTHON
import asyncio
import time

# BAD: 同期I/Oがイベントループをブロック
async def get_price_bad(product_id: int):
    time.sleep(0.1)  # 他のすべてのリクエストを100msブロック!
    return {"product_id": product_id, "price": 9.99}

# GOOD: asyncioでI/Oを処理
async def get_price_good(product_id: int):
    await asyncio.sleep(0.1)  # 非ブロッキング, 他のリクエストは継続
    return {"product_id": product_id, "price": 9.99}

出力:

TEXT
# 関数定義成功

(2) ▶ サンプル:run_in_executorで同期コードをラップ

PYTHON
import asyncio
from functools import partial

# 非同期化できない同期関数
def sync_scrape_price(url: str) -> float:
    # requestsライブラリを使用 (同期のみ)
    import requests
    response = requests.get(url, timeout=10)
    return parse_price(response.text)

async def get_price_async(product_id: int):
    loop = asyncio.get_event_loop()
    # スレッドプールで同期関数を実行 - イベントループをブロックしない
    price = await loop.run_in_executor(
        None,  # デフォルトスレッドプール
        partial(sync_scrape_price, f"https://api.example.com/prices/{product_id}"),
    )
    return {"product_id": product_id, "price": price}

出力:

TEXT
# 関数定義成功

(2) JSONシリアライズ最適化

(3) ▶ サンプル:jsonの代わりにorjsonを使用

PYTHON
# インストール:uv add orjson
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse

app = FastAPI(default_response_class=ORJSONResponse)

@app.get("/api/v1/products")
async def list_products():
    # orjsonは標準ライブラリjsonより3〜10倍高速
    return {"products": [{"id": i, "name": f"Product {i}"} for i in range(100)]}

出力:

TEXT
# 関数定義成功
シリアライズライブラリ 速度 説明
json (標準ライブラリ) ベースライン 純粋なPython実装
orjson 3〜10倍 Rust実装, datetimeを自動処理
ujson 2〜3倍 C実装, 一部互換性の問題あり

(4) ▶ サンプル:GZipMiddleware圧縮

PYTHON
from fastapi import FastAPI
from starlette.middleware.gzip import GZipMiddleware

app = FastAPI()
app.add_middleware(GZipMiddleware, minimum_size=1000)  # 1KB以上のレスポンスを圧縮

@app.get("/api/v1/products")
async def list_products():
    # 大きなJSONレスポンスが自動的に圧縮される
    return {"products": [...]}  # 50KB → gzipで~5KB

出力:

TEXT
# 関数定義成功

5. データベース層の最適化

(1) インデックス戦略

(1) ▶ サンプル:PriceTrackerのインデックス設計

PYTHON
from sqlalchemy import Index

class Product(Base):
    __tablename__ = "products"
    __table_args__ = (
        # よく使うフィルター用の単一カラムインデックス
        Index("ix_products_category", "category"),
        Index("ix_products_created_at", "created_at"),
        # category + 価格範囲クエリ用の複合インデックス
        Index("ix_products_category_base_price", "category", "base_price"),
        # アクティブな製品のみの部分インデックス (PostgreSQL固有)
        Index("ix_products_active", "created_at", postgresql_where=("deleted_at IS NULL")),
    )
    ...

出力:

TEXT
# 実行成功
インデックスタイプ 適したクエリ
単一カラムインデックス 等価/範囲フィルター WHERE category = ?
複合インデックス 複数条件の組み合わせ WHERE category = ? AND price > ?
部分インデックス 条件付きサブセット WHERE deleted_at IS NULL
カバリングインデックス テーブル検索の回避 INCLUDE (name, price)

(2) コネクションプールチューニング

(2) ▶ サンプル:本番レベルのコネクションプール設定

PYTHON
engine = create_async_engine(
    DATABASE_URL,
    pool_size=25,           # Workerごとの持続的接続数
    max_overflow=10,        # プール枯渇時の追加接続数
    pool_timeout=30,        # 利用可能接続の待機時間
    pool_recycle=1800,      # 30分後に接続を再利用
    pool_pre_ping=True,     # 使用前に接続をテスト
    echo=False,             # 本番ではSQLログを無効化
)

出力:

TEXT
# 実行成功
パラメータ 推奨値 説明
pool_size CPUコア数 x 2 + 1 基本接続数
max_overflow pool_size x 0.5 バースト時の最大追加接続数
pool_timeout 30s タイムアウト
pool_recycle 1800s MySQL/PGがアイドル接続を閉じるのを防止
pool_pre_ping True 切断された接続の使用を防止

6. 並行性モデルの最適化

(1) Workers設定

(1) ▶ サンプル:Gunicorn + Uvicorn Workers

BASH
# 本番:GunicornがUvicornワーカーを管理
gunicorn app.main:app \
    --workers 9 \
    --worker-class uvicorn.workers.UvicornWorker \
    --bind 0.0.0.0:8000 \
    --timeout 120 \
    --graceful-timeout 30 \
    --access-logfile - \
    --error-logfile -

出力:

TEXT
# コマンド実行成功
DOCKERFILE
# Dockerfile内
CMD ["gunicorn", "app.main:app", \
     "--workers", "9", \
     "--worker-class", "uvicorn.workers.UvicornWorker", \
     "--bind", "0.0.0.0:8000"]
設定 式/値 説明
Worker数 (2 x CPU) + 1 4コア = 9ワーカー
worker-class UvicornWorker ASGIワーカー
timeout 120s タイムアウトによるWorker強制終了
graceful-timeout 30s グレースフルシャットダウンのタイムアウト
max-requests 10,000 メモリリーク防止のための自動再起動

❓ よくある質問

Q パフォーマンスのボトルネックはどう特定しますか?
A ツールチェーンを使用します - cProfileでCPUホットスポットを分析, py-spyでリアルタイムサンプリング, Prometheusでレイテンシ分布を監視, スロークエリログでデータベースを分析します。まず計測し, それから最適化します。
Q run_in_executorのスレッドプールのサイズはどのくらいですか?
A デフォルトはmin(32, os.cpu_count() + 4)です。CPU集約型タスクには, GILの制限を回避するためにProcessPoolExecutorを使用してください。
Q orjsonはPydanticと互換性がありますか?
A はい, 互換性があります。FastAPIのdefault_response_class=ORJSONResponseは, Pydanticモデルをorjsonで自動的にシリアライズし, model_dump()の出力を生成します。
Q コネクションプールとWorker数のバランスはどう取りますか?
A 各ワーカーには独自のコネクションプールがあります。9ワーカー x 25接続 = 225データベース接続。PostgreSQLのデフォルトmax_connectionsは100なので, pool_sizeを適宜増減させる必要があります。
Q GZip圧縮にパフォーマンスオーバーヘッドはありますか?
A 圧縮はCPUリソースを消費しますが, ネットワーク伝送時間を短縮します。1 KBを超えるJSONレスポンスの場合, 圧縮のメリットはCPUオーバーヘッドを大幅に上回ります (圧縮率は通常5〜10倍です)。
Q 最適化の効果はどう確認しますか?
A Locustやk6で負荷テストを行い, 最適化前後のQPS, P50/P95/P99レイテンシ, CPU/メモリ使用量を比較します。一度に変更する変数は1つだけにしてください。

📖 まとめ


📝 練習問題

  1. 基本問題 (難易度 ⭐):orjsonをインストールし, FastAPIのデフォルトレスポンスクラスをORJSONResponseに変更し, curlでjsonとorjsonのレスポンスタイムを比較してください。ヒント:default_response_class=ORJSONResponse
  2. 応用問題 (難易度 ⭐⭐):PriceTrackerのproductsテーブルにインデックスを追加し (category, created_at, およびcategorybase_priceの複合インデックス), インデックス追加前後で同じクエリの実行時間を比較してください。ヒント:Index("ix_name", "col1", "col2") + EXPLAIN ANALYZE
  3. チャレンジ (難易度 ⭐⭐⭐):総合パフォーマンス最適化 - orjsonシリアライズ + GZipMiddleware + コネクションプールチューニング + Gunicorn 4ワーカー。Locustで負荷テストを実行し, 最適化前後のQPSとP99レイテンシを比較してください。ヒント:locust -f locustfile.py --host=http://localhost:8000

---|

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%