先日、久しぶりに「ムーザルチャンネル」を視聴していたら、ザル氏が私と同じような障害に遭遇しており、思わず感動してしまいました。
今回はその障害の内容と、背景にある「コネクションプーリング」の仕組みについて記事にまとめようと思います。
1. 障害の内容について
話題の障害については、下記の動画(4:30秒あたりから)で語られています。
障害の内容を要約すると、以下のようになります。
- 誤った接続生成: トランザクション処理の内部で、既存のコネクションを使わず、誤って「新規セッション(別のコネクション)」を生成してクエリを実行してしまった。
- 多重接続の要求: 1回のリクエストで「トランザクション用」と「内部の新規クエリ用」の 2つの接続 を要求する状態になった。
- リソースの枯渇: 1つ目のコネクションを確保してトランザクションを開始し、その中で2つ目のコネクションを新規に取得しようとする。
- デッドロックの発生: プールの空き待ちが発生し、お互いに接続を解放できずレスポンスが返らなくなる。
2. コネクションプーリングとは?
私が障害対応を進める中で行き着いたキーワードがこの動画で何度も出てくる「コネクションプーリング」です。(お恥ずかしながら、私はそれまで知りませんでした)
コネクションプーリングの仕組み
データベースとの接続確立には、ネットワークの認証やメモリ確保などのオーバーヘッドがあり、意外と処理が重いです。
これを毎回行わずに済むよう、あらかじめ一定数の接続を保持(プール)しておくのが「コネクションプーリング」の役割です。
- 処理開始時: プールから空いている接続を借りる。
- 処理終了時: 接続を閉じずにプールへ返却し、次のリクエストで再利用する。
つまり、接続数の上限を超えてしまうと、「使いたい接続」と「返したい接続」が互いを待つ状態になり、システム全体が停止(デッドロック)してしまうのです。
※ライブラリやフレームワークがこのあたりをよしなに調整してくれますが、ザル氏のケースでは利用していたOSS側に不具合があったようです。
明言はされていませんが、TypeORMそのものか、あるいはTypeORMを利用している、OSSっぽいです。
3. 私のケース:シェルスクリプトとストアド
私の場合は、シェルスクリプトからストアドプロシージャを呼び出していました。
シェルを同時に10個ほど実行したところ、「1つのプロセスが内部で複数の接続を要求する(あるいは上限いっぱいの席を占有し続ける)」状態になり、DB側が新しい接続を受け付けられなくなりました。
ここで恐ろしいのは、DBが完全にダウンしているわけではないのに、「席を確保して待っている接続」と「席を空けるために完了したい接続」が互いに身動きが取れなくなる**ことで、サーバー全体がフリーズしたように見えてしまう点です。
4. PHP(Laravel風)での再現例
この事象をPHPで表現すると、以下のようなイメージです。
❌ 間違った例
DB::transaction(function ($currentConn) {
// 1つ目の接続を使用
$currentConn->table('users')->update(...);
// 【ここが間違い!】
// $currentConn を渡さず、スタティックに呼び出している。
// OtherClass側は「今トランザクション中であること」を判別できないため、
// 内部で勝手に新しい接続を生成し、2つ目の席を確保しに行ってしまう。
OtherClass::doSomethingSeparately();
});
✅ 正しい例
DB::transaction(function ($currentConn) {
// 1つ目の接続を使用
$currentConn->table('users')->update([...]);
// 【重要】OtherClassに「現在の接続インスタンス」を渡す
// これにより、OtherClass内部で新しい接続(コネクション)が作られるのを防ぐ
$other = new OtherClass();
$other->doSomething($currentConn);
});
間違いの例では、呼び出し先で「今使っている接続」という情報を捨ててしまっています。
そのため、呼び出し先が新しい接続を確保しに行き、コネクションの増殖を招いてしまいます。
正しい例では、新しい接続は作らず、同じ接続内で処理が行われます。
あとがき
データベースが完全に落ちているわけではないのに、「なぜか挙動が重い・止まる」という状況に遭遇したら、この記事を思い出していただけると幸いです。