オブジェクトプールパターン

生成コストの高いオブジェクトを事前に確保してプールし、再利用することでパフォーマンスを向上させるパターン

設計パターンパフォーマンス

オブジェクトプールパターンとは

オブジェクトプールパターンは、生成と破棄のコストが高いオブジェクトを事前に複数確保しておき、必要な時にプールから借り出し、使い終わったらプールに返却する設計パターンである。毎回の生成・破棄を避けることで、パフォーマンスとリソース効率を向上させる。

代表的な適用例

プールが効くのは、1 個の生成コストが 1 回の利用時間に比べて無視できないほど大きい場合である。TCP 接続や OS スレッドの確保はこの条件を満たすが、単にデータを保持するだけのオブジェクトはプールしても管理の手間の方が勝つ。

対象生成コストプールの例
DB コネクションTCP 接続 + TLS ハンドシェイク + 認証pg-pool, mysql2 pool
HTTP クライアントTCP 接続 + TLS ハンドシェイクNode.js http.Agent
スレッドOS スレッドの生成Java ThreadPoolExecutor
Workerプロセス/スレッドの生成piscina (Node.js)

基本的な実装

実装の核は、借り出し (acquire)・返却 (release)・返却時の状態リセットの 3 点である。リセットを忘れると、前の利用者が残した状態が次の利用者へそのまま漏れる。

class ObjectPool<T> {
  private available: T[] = [];
  private inUse = new Set<T>();

  constructor(
    private factory: () => T,
    private reset: (obj: T) => void,
    private maxSize: number,
  ) {
    for (let i = 0; i < maxSize; i++) {
      this.available.push(factory());
    }
  }

  acquire(): T | null {
    const obj = this.available.pop();
    if (!obj) return null; // プール枯渇
    this.inUse.add(obj);
    return obj;
  }

  release(obj: T): void {
    this.inUse.delete(obj);
    this.reset(obj); // 状態をリセットしてから返却
    this.available.push(obj);
  }

  get stats() {
    return { available: this.available.length, inUse: this.inUse.size };
  }
}

運用で最も多い事故は返却漏れである。例外が投げられた経路で release が呼ばれないと、借り出されたオブジェクトが inUse に残り続け、やがて acquire が常に null を返すようになる。借り出しは try/finally で囲み、返却が必ず通る位置に置く。statsinUse が減らないまま増え続けていないかを監視しておくと、漏れを稼働中に捕まえられる。

DB コネクションプール

最も一般的な適用例。Lambda では、ハンドラーの外側でコネクションプールを初期化すると、初期化コードは実行環境の作成時に 1 度だけ走り、以降の呼び出しでは同じプールが再利用される。

ただし、Lambda の実行環境は処理中に他のリクエストを受け付けず、並行するリクエストには別の実行環境が割り当てられる。実行環境 1 つが同時に使うコネクションは 1 本ということになり、プールサイズを 5 にしても残り 4 本は使われないまま DB 側の接続枠を占める。Lambda ではプールサイズを 1 本に寄せ、接続の集約は後述の RDS Proxy に任せる方が筋が良い。

プールサイズの設計

常駐プロセス (Web サーバーやワーカー) では、プールサイズが小さすぎると全コネクションが使用中で待ちが発生し、大きすぎると DB 側の接続数上限に達する。同時に処理されるリクエスト数が上限になるため、プールサイズの上限は「その 1 プロセスが同時に走らせたいクエリ数」で決まる。

Lambda の場合、同時実行数 × プールサイズ = DB への最大接続数になる。Lambda の同時実行数が 100 でプールサイズが 5 なら、最大 500 接続が DB に張られる。RDS Proxy を挟むと、共有されたコネクションのプールを Proxy 側が管理するため、DB の接続数を使い切らずに高い同時実行数まで伸ばせる。AWS のドキュメントでも、短い接続を頻繁に開閉する関数や大量の接続を開閉する関数には RDS Proxy の利用が推奨されている。

Flyweight パターンとの違い

両者はオブジェクトを使い回す点で似ているが、狙いも状態の扱いも逆である。

パターン目的オブジェクトの状態
Object Pool生成コストの回避状態を持つ (使用後にリセット)
Flyweightメモリの節約不変 (共有して読み取り専用)

実践的な知識は関連書籍でも得られる。

この記事は役に立ちましたか?

関連用語

関連する記事