DBプーリングがリモート環境だと遅かったので
はじめに
E2E テストの高速化をしたくて、目をつけたのが「DB 接続のコスト」でした。
そこでコネクションプーリングを導入すれば速くなるかと考えてましたが、予想外の結果が得られました。
本記事では、PostgreSQL に対するコネクションプーリングとテスト並列化の効果を、ローカル環境とリモート環境の両方で実測検証しています。
TL;DR
- ローカル環境: プーリング + 並列化で最大 47%の改善 ✅
- リモート環境: プーリングの効果はほぼゼロ、むしろ微減 ❌
- 並列化は両環境で有効: 2 Worker で約 50%短縮
- ネットワークレイテンシが支配的な環境では、プーリングより並列化を優先すべき
検証環境
Playwright の Worker プロセスごとに pg-pool を経由して DB に接続する構成で検証しました。
環境スペック
| 項目 | ローカル | リモート |
|---|---|---|
| Database | PostgreSQL 15 (Docker) | PostgreSQL 15 (クラウド) |
| Network | localhost | インターネット経由 |
| Worker 数 | 1 / 2 | 1 / 2 |
| データ | Products テーブル 100,000 件 | 同左 |
| 反復回数 | 各テスト 100 回 | 同左 |
テストシナリオ
3 段階の負荷レベルでベンチマークを実施しました。
| レベル | クエリ数 | 内容 | 特徴 |
|---|---|---|---|
| Light | 1 | PK による単純 SELECT | 接続コストが支配的になりやすい |
| Medium | 4 | INSERT + JOIN + Range SELECT | 一般的な OLTP ワークロード |
| Heavy | 9 | Bulk INSERT + GROUP BY + 集計関数 | クエリ処理自体が重い |
ベンチマーク結果
ローカル環境(Docker)
| レベル | Workers | Pooling | 実行時間 | vs No-Pool | vs Serial 基準 |
|---|---|---|---|---|---|
| Light | 1 | OFF | 3.72s | - | - |
| Light | 1 | ON | 2.57s | 31.0% 🚀 | - |
| Light | 2 | OFF | 2.76s | - | 25.8% |
| Light | 2 | ON | 2.23s | 19.1% | 40.1% 🚀 |
| Medium | 1 | OFF | 4.74s | - | - |
| Medium | 1 | ON | 3.21s | 32.3% 🚀 | - |
| Medium | 2 | OFF | 3.15s | - | 33.5% |
| Medium | 2 | ON | 2.50s | 20.6% | 47.3% 🚀 |
| Heavy | 1 | OFF | 8.27s | - | - |
| Heavy | 1 | ON | 7.24s | 12.4% | - |
| Heavy | 2 | OFF | 5.49s | - | 33.6% |
| Heavy | 2 | ON | 4.62s | 15.8% | 44.1% 🚀 |
ローカル環境では、プーリングによる改善効果が明確に出ています。特に Light テストでは 31%、Medium では 32%の改善が見られました。
リモート環境(クラウド DB)
| レベル | Workers | Pooling | 実行時間 | vs No-Pool | vs Serial 基準 |
|---|---|---|---|---|---|
| Light | 1 | OFF | 63.54s | - | - |
| Light | 1 | ON | 68.57s | -7.9% ❌ | - |
| Light | 2 | OFF | 32.19s | - | 49.3% |
| Light | 2 | ON | 33.21s | -3.2% ❌ | 47.7% |
| Medium | 1 | OFF | 63.21s | - | - |
| Medium | 1 | ON | 62.59s | 1.0% | - |
| Medium | 2 | OFF | 32.08s | - | 49.3% |
| Medium | 2 | ON | 32.35s | -0.8% ❌ | 48.8% |
| Heavy | 1 | OFF | 63.12s | - | - |
| Heavy | 1 | ON | 63.20s | -0.1% | - |
| Heavy | 2 | OFF | 32.20s | - | 49.0% |
| Heavy | 2 | ON | 31.20s | 3.1% | 50.6% 🚀 |
予想外の結果: リモート環境ではプーリングの効果がほぼゼロ、Light テストではむしろ悪化しています。
なぜリモート環境でプーリングが効かないのか
原因:ネットワークレイテンシの支配
【ローカル環境】
接続確立: 1-5ms ← プーリングで削減できる
クエリ実行: 1-10ms
───────────────
接続コストの割合が大きい → プーリング効果あり
【リモート環境】
接続確立: 10-30ms ← プーリングで削減できる
ネットワークRTT: 50-100ms × クエリ数 ← 削減できない
クエリ実行: 1-10ms
───────────────
ネットワーク遅延が支配的 → プーリング効果が埋もれる
リモート環境では、クエリごとのネットワークラウンドトリップ(RTT)が実行時間の大部分を占めます。接続確立のコストを削減しても、全体に対する影響が小さいのです。
プーリングが逆効果になるケース
Light テストで-7.9%と悪化した理由は、プール管理自体のオーバーヘッドです。
- コネクションの取得・返却処理
- プール状態の管理
- アイドルコネクションの維持
クエリが軽く、ネットワーク遅延が大きい環境では、これらのオーバーヘッドがメリットを上回ることがあります。
並列化は両環境で有効
注目すべきは、並列化の効果は両環境で一貫して有効だったことです。
| 環境 | 並列化による改善(Heavy, Pooling ON) |
|---|---|
| ローカル | 36.2% (7.24s → 4.62s) |
| リモート | 50.6% (63.20s → 31.20s) |
リモート環境では、ネットワーク待ち時間を並列化で隠蔽できるため、むしろ効果が大きくなっています。
ロック競合の回避
並列化の恩恵を受けるには、Worker 間でのロック競合を避ける必要があります。
問題
Worker A と Worker B が同時に同じ行に書き込もうとすると、行ロック待機が発生し、並列化の効果が打ち消されます。
解決策 1:ユニークな PK を生成する
方法 A: UUID を使う
const productId = crypto.randomUUID();
await db.query("INSERT INTO products (id, name) VALUES ($1, $2)", [
productId,
"Test Product",
]);
方法 B: for ループでインデックスを活用する
テストの反復回数が決まっている場合、ループインデックスをそのまま PK に使う方法が確実です。
// 100回の反復テストで、各イテレーションにユニークなIDを割り当て
for (let i = 0; i < 100; i++) {
const productId = i + 1; // 1〜100
await db.query("INSERT INTO products (id, name) VALUES ($1, $2)", [
productId,
`Product ${i}`,
]);
}
UUID は衝突の心配がほぼなく手軽ですが、インデックス効率やデバッグのしやすさを考えると、連番の方が扱いやすいケースもあります。
解決策 2:Worker ID で PK 範囲を分離する
より積極的な方法として、Worker ID に基づいて PK の範囲を物理的に分離します。
function generateUniqueId(workerId: number, sequence: number): number {
// Worker 0: 100,000,000〜199,999,999
// Worker 1: 200,000,000〜299,999,999
return (workerId + 1) * 100_000_000 + sequence;
}
これにより、単一 DB でありながら論理的にパーティションされた状態を作り出し、ロック競合ゼロを実現できます。
実務での課題:外部依存データの制約
外部 API(決済サービス、CRM など)のモックが特定の ID しか受け付けないケースへの対処法です。
シナリオ例
モックサーバーが user_id: 1 のリクエストにしか正常応答しない場合、Worker 1(user_id: 100...)からのリクエストはエラーになります。
解決策
-
モックのパターンマッチ化: 固定 ID ではなく正規表現(
user_id: [0-9]+)でマッチさせる -
データプールの割り当て: モック側の有効データをリスト化し、
WorkerID % Nで各 Worker に割り当てる
環境別の最適化指針
| 環境 | 推奨アプローチ | 理由 |
|---|---|---|
| ローカル / CI (Docker) | プーリング + 並列化 | 接続コスト削減の効果が大きい |
| リモート DB(低レイテンシ) | プーリング + 並列化 | 両方の効果が期待できる |
| リモート DB(高レイテンシ) | 並列化のみ | プーリングのオーバーヘッドがメリットを上回る可能性 |
CI 環境での推奨: GitHub Actions 等で DB をサイドカーコンテナとして起動する構成なら、ローカル相当のレイテンシになるためプーリングが有効です。外部のマネージド DB に接続する場合は、並列化を優先してください。
まとめ
今回の検証で得られた知見をまとめます。
-
プーリングは万能ではない - ネットワークレイテンシが大きい環境では効果が薄い、または逆効果になることがある
-
並列化は一貫して有効 - ローカルでもリモートでも、Worker 数を増やすことで実行時間を短縮できる
-
環境に応じた最適化を - 「とりあえずプーリング」ではなく、実際の環境で計測して判断すべき
E2E テストの高速化を検討する際は、まず並列化から始めて、ローカル環境であればプーリングを追加するのが効率的なアプローチです(レイテンシが支配的な場合が多いので)。
ただし、同一セッションでバッチ処理をするなどリクエスト回数を大幅に抑えて、多数の処理をする場合はリモートでもプーリングは有効です。
Discussion