MLOps環境では、推論APIが高頻度でDBアクセスを行います。その際、アプリが直接Auroraに多数のコネクションを張ると、接続のたびに無駄なコストが発生し、パフォーマンス低下の原因になりやすいです。


🔍 なぜ接続プールが必要?

  • 接続確立のオーバーヘッド
    DBは1接続ごとにCPUやメモリを確保します。短期接続が増えるとすぐに資源が圧迫されます。
  • スパイクに弱い
    Lambdaやマイクロサービスでは、同時に数百の接続が一気に増えることがあり、その瞬間に遅延が発生します。
  • クエリ自体は軽くても遅い
    実は「SQL実行」よりも「接続確立」の方が遅延要因になるケースが多いのです。

💡つまり、“クエリ最適化より前に接続最適化” が効く場面がある、ということです。

📖 具体例
イメージしてください。100人のユーザーが同時に推論APIを叩くと、100本の接続が一気にAuroraに殺到します。そのたびに接続準備が必要になり、DBサーバーは「接続準備」だけで疲弊します。接続プールを挟めば、アプリは“既に開かれているドア”を使うだけなので、待ち時間がほとんど発生しません。


🚀 解決策:Aurora + 接続プール

✅ RDS Proxy

  • AWS公式のマネージド接続プーラ
  • フェイルオーバー時もセッションを維持でき、堅牢性が高い
  • 運用負荷が少なく、設定も簡単

✅ PgBouncer

  • 軽量でシンプルなOSSプーラ
  • 短命セッションが多い推論API環境に向いている
  • RDS Proxyより柔軟にチューニング可能

⚙️ 設計のポイント

  • 短命リクエストは必ずプール経由
  • Writer / Reader でプールを分ける
    読み取り専用トラフィックは Reader プールに集約
  • DB側の max_connections を意識
    プールがリソースを食い潰さないよう調整
  • アプリは「常に接続済み」の状態で使える設計にする

📊 イメージ図(概念):

[App] ──▶ [Connection Pool] ──▶ [Aurora Writer]
   │
   └──▶ [Connection Pool] ──▶ [Aurora Reader]

👀 分かりやすい比喩

  • 接続なしの状態 → 毎回レストランで新規にテーブルを組み立てて椅子を並べるようなもの。
  • 接続プールあり → すでに用意された席にお客さんを案内するだけ。

📝 接続例(RDS Proxy経由)

# 書き込み用
DB_WRITER_HOST=my-proxy-writer.proxy-xxxx.ap-northeast-1.rds.amazonaws.com

# 読み取り用
DB_READER_HOST=my-proxy-reader.proxy-xxxx.ap-northeast-1.rds.amazonaws.com

🎯 実際の設定目安

  • RDS Proxy
    • 最大接続数:Auroraの max_connections の 70〜80% を上限に設定
    • Idle timeout:30秒前後(短命リクエストに最適)
    • Connection borrowing:常時有効
  • PgBouncer
    • pool_mode = transaction(短命クエリに最適)
    • max_client_conn:想定同時リクエスト数の 2〜3倍
    • default_pool_size:アプリ1インスタンスあたり 20〜50 程度から調整開始
  • Lambda × Aurora(典型的な構成)
    • Lambda Concurrency:数百までスケールするなら必ず RDS Proxy 経由
    • プールで吸収できない場合は Aurora Reader/Writer を分離し、読み込み負荷を分散

🧪 現場で使える設定の目安(まずはここから)

数字は初期値の提案です。実トラフィックの p95 同時実行数と待ち行列の伸びを見ながら、1~2週間で微調整します。

0) 事前の接続予算を決める

  • Aurora の max_connections を確認し、20% は常に予備に残す。
  • 例:max_connections = 2000 の場合 → 利用上限 1600 をアプリ用に割り当て。
  • Writer : Reader = 3 : 7 を出発点に配分(更新が少なく読み取りが多い前提)。

1) RDS Proxy(推奨)

  • エンドポイント分割:Writer / Reader で別の Proxy を作る。
  • 接続プール系
    • Connection Borrow Timeout: 3–5s
    • Idle Client Timeout: 30–60s(API系は短め)
    • Idle Connection Timeout: 60–300s(Auroraへの接続を維持)
    • 最大接続(Target Max Connections): Writer/Reader の接続予算内に設定
  • ピニング回避:長期トランザクション/セッション変数に注意。できるだけピン止めが起きないORM/SQLにする。
  • 認証:可能なら IAM 認証 + Secrets Manager。
  • 監視DatabaseConnections, ClientConnections, CurrentBorrowCount, EndpointHealthy をダッシュボード化。

2) PgBouncer(Aurora手前の自前プール)

基本は transaction モード。

[pgbouncer]
pool_mode = transaction
max_client_conn = 5000           ; フロント側の同時接続上限
default_pool_size = 20           ; 1 DB/ユーザー毎のサーバ接続プール数
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 5s
query_wait_timeout = 3s
server_idle_timeout = 60s
idle_transaction_timeout = 5s
server_reset_query = DISCARD ALL
ignore_startup_parameters = extra_float_digits,search_path,application_name

目安の考え方

  • default_pool_sizeインスタンスあたり p95 同時SQL数 ÷ 2 を起点(上限 30)。
  • query_wait_timeout待機を短く切る(スパイク時の劣化を局所化)。
  • 長時間トランザクションが必要なら、その処理だけsession モード用プールを別定義。

3) アプリ別クライアント設定(出発点)

Python / SQLAlchemy

engine = create_engine(
    url,
    pool_size=20,           # 10–30 から開始
    max_overflow=10,        # スパイク吸収
    pool_pre_ping=True,     # 死亡検知
    pool_recycle=300,       # 5分で再接続
)

Node.js / pg

const pool = new Pool({
  max: 20,                  // 10–30
  idleTimeoutMillis: 30000, // 30s
  connectionTimeoutMillis: 5000,
  statement_timeout: 2000,  // Aurora側でもクエリタイムアウト設定
  query_timeout: 4000
});

Go / database/sql

db.SetMaxOpenConns(30)       // 20–50
db.SetMaxIdleConns(10)       // 10–20
db.SetConnMaxIdleTime(60*time.Second)
db.SetConnMaxLifetime(10*time.Minute)

Django(psycopg2)

CONN_MAX_AGE = 60  # 秒。0は都度接続、APIは30–120が目安

4) Lambda + Aurora の目安

  • RDS Proxy は必須。直張りは避ける。
  • プロビジョンド並列数 (Provisioned Concurrency) が大きい場合、同数だけコネクションが生まれる前提で接続予算を計算。
  • SDK/ドライバはグローバル初期化し、ハンドラ内で毎回 new しない。
  • タイムアウト:ハンドラ timeout < Proxy Borrow Timeout < ALB/ApiGW の順で短く。

5) ECS/EKS(常駐ワーカー)の目安

  • 1タスク/Pod あたりの DBプール = 10–30 から開始。
  • 必要サーバ接続数 = (レプリカ数 × プールサイズ)。Writer/Reader の接続予算内に収まるように。
  • Gunicorn/uvicorn:ワーカー数×アプリの内部同時実行数がプール上限を超えないよう注意。

6) クエリとトランザクションの作法

  • 短く・小さく:トランザクションは秒単位以内で完了させる。
  • 長時間バッチは別系統へ:OLTPと競合させない(Reader/別DB/別時間帯)。
  • SET LOCAL で済む設定にし、セッション変数依存を減らす(ピニング抑制)。

7) 監視・アラートの閾値(初期)

  • E2E遅延 p95:SLO 基準(例:< 200ms)。
  • Proxy Borrow 待ち p95< 100ms。超過が10分継続で Warning。
  • Connection利用率:接続予算の 70% 超で Warning、85% 超で Critical。
  • Query timeout/エラー率:5xx / error-rate > 1% でアラート。

🧮 ざっくり計算式(初期プールサイズ)

  • インスタンスあたりプール ≈ ceil(p95 同時リクエスト数 × DB操作率 × 0.7)
    • 例:1 Pod の p95 同時リクエスト 40、DBに触るのが 50% → 40×0.5×0.7 ≈ 14プール15–20 を設定。

🧮 プールサイズ自動計算シート(サンプル)

トラフィックの基本指標から自動でプール値を出す小さな計算シートをイメージしておくと便利です。ExcelやGoogle Sheets、あるいはコードに埋め込んで使えます。

項目説明
p95 同時リクエスト数401 Pod あたりのピーク同時リクエスト数
DB操作率0.5リクエストのうちDBにアクセスする割合
安全係数0.7バッファを考慮した係数
推奨プールサイズ=CEIL(40×0.5×0.7) = 1415〜20から調整開始

🐍 Pythonスクリプト例(自動計算)

import math

def suggest_pool_size(p95_concurrency: int, db_ratio: float, safety: float = 0.7):
    return math.ceil(p95_concurrency * db_ratio * safety)

# 例: p95=40, DB比率=50%
print(suggest_pool_size(40, 0.5))  # → 14

このスクリプトを Jenkins/GitHub Actions や Terraform Variable に組み込み、負荷テスト結果から初期プールサイズを自動で再計算できるようにすると便利です。


📥 計算シート(Excel)

  • この記事に合わせた Connection Pool Planner を用意しました。Inputs シートの数値を編集すると、Results に**推奨プールサイズ・接続予算(Writer/Reader)**が自動計算されます。
  • まずは p95 同時実行数・DB操作率・Pod数・max_connections を入れて、結果を起点に微調整してください。

ダウンロード:本文下のリンクから取得できます。


✅ 今日のまとめ

MLOpsでは「クエリの速さ」よりも「接続の安定性」がボトルネックになることが多い。
ここで示した接続予算 → プール初期値 → 監視の三点セットを最初に組み込み、実測に合わせて段階調整すると、小さな手数で大きな安定化が得られる。

投稿者 kojiro777

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です