DB接続プール vs リクエストごと生成・破棄の使い分け
接続プールとは
DB接続をあらかじめ一定数作成しておき、リクエストごとに「借りて返す」仕組み。 Railsがデフォルトで採用している方式。
# Rails database.yml
production:
pool: 5
checkout_timeout: 5
比較
| 接続プール(Rails等) | リクエストごと生成・破棄 | |
|---|---|---|
| 接続コスト | 初回のみ。以降は借りる/返すだけ | 毎回 connect + close |
| 同時接続数 | 制御できる(pool_size=5 等) | リクエスト数に比例して増える |
| メモリ使用 | 一定(プールサイズ分) | リクエストに比例 |
| 実装の複雑さ | プール管理が必要 | シンプル |
| 障害時の影響 | プール内の不良接続が再利用されるリスク | 毎回新規なので不良接続が残らない |
| 向いている DB | PostgreSQL, MySQL(接続が重い) | SQLite(接続が軽い) |
接続プールのメリット
接続確立が重い DB で効く。PostgreSQL や MySQL は接続時に TCP ハンドシェイク、認証、セッション初期化が走るので、毎回やると数十ms かかる。
接続プールのデメリット
- プール枯渇: 全接続が貸出中だとリクエストがブロックされる
- コネクションリーク: 返却し忘れるとプールが徐々に枯渇する
- 不良接続: DB 再起動後に古い接続が残って壊れた接続を掴むことがある
- 設定のチューニング: pool_size を適切に設定しないとパフォーマンスが出ない
DB別の接続コスト
PostgreSQL: ~20-50ms → プール必須
MySQL: ~10-30ms → プール推奨
SQLite: ~0.01ms → プール不要
SQLiteはファイルをopen()するだけで、TCPもネゴシエーションもないので、プールの管理コストの方が高くつく。
FastAPI でのリクエストごと生成・破棄パターン
def get_db() -> Generator[sqlite3.Connection, None, None]:
conn = get_connection() # リクエストごとに新しい接続を作成
try:
yield conn # ルートハンドラに接続を渡す
finally:
conn.close() # レスポンス返却後に必ず接続を閉じる
FastAPIのyield依存関係がライフサイクルを保証する。yield前がセットアップ、yieldの値が注入対象、finallyがクリーンアップ。
PostgreSQLに変える場合(SQLAlchemy + プール)
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine("sqlite:///myweb.db", pool_size=5)
SessionLocal = sessionmaker(bind=engine)
def get_db():
db = SessionLocal() # プールから接続を借りる
try:
yield db
finally:
db.close() # プールに返す(破棄ではない)
まとめ
- 接続が重い DB(PostgreSQL, MySQL)→ 接続プール
- 接続が軽い DB(SQLite)→ リクエストごと生成・破棄
- Railsがプールをデフォルトにしているのは PostgreSQL/MySQL 前提だから