仕組みから学ぶ Web
応用読了目安 15#コネクションプール#接続#サーバーレス#RDS Proxy

コネクションプール — 増やしたら、もっと詰まった

DB の接続が足りないと言われてプールを増やしたら、さらに詰まった。接続の総数は「1台あたり × 台数」で、サーバーレスでは台数が勝手に増える。

この記事の進み方

What — 接続を借りて、返す

DB への接続を作るのは、思っているより高くつきます。TCP の接続、TLS のハンドシェイク、認証、そして DB 側での準備。PostgreSQL は 接続1本ごとに専用のプロセスを起動します。

リクエストのたびにこれを繰り返すと、クエリそのものより接続の準備に時間を使うことになります。そこで、接続を作っておいて使い回すのがコネクションプールです。

Figureプールの中の接続
空きプール

プールの中で、次に借りられるのを待っている

貸し出し中アプリ

リクエストが借りて、クエリを投げている

トランザクション中アプリ

BEGIN から COMMIT まで、この接続を占有している

借りるのを待つ次のリクエスト

空きが無いので、他の誰かが返すのを待っている

1/4
空きリクエストが接続を借りる貸し出し中接続を作り直すのではなく、作ってある接続を渡す。数ミリ秒から数十ミリ秒の準備を省ける。

接続は「借りて、使って、返す」。返さない処理が1つあれば、その接続は他の誰にも使えない。プールが空になれば、DB が暇でも待たされる。

コネクションプール
DB への接続をあらかじめ作っておき、リクエスト間で使い回す仕組み。
max_connections
DB が同時に受け付ける接続の上限。超えると新しい接続は拒否される。
取得待ちタイムアウト
プールに空きが無いとき、接続を借りるまで待つ上限時間。超えるとアプリ側でエラーになる。
外部プーラー
アプリと DB の間に置く、接続をまとめる専用の中継。RDS Proxy や PgBouncer など。

プールは1台ごと。DB から見えるのは合計

プールの設定はアプリの1台(1プロセス、1実行環境)ごとです。DB が受け止めるのは、その合計です。

DB への接続数 = 1台あたりのプールの最大 × 台数

サーバーを4台並べて各20本なら80本です。ここまでは設定を書いた人にも見えています。

問題は台数が自分の知らないところで増えるときです。オートスケールで台数が増える。デプロイ中に新旧が同時に立つ。そしてサーバーレスでは、同時実行数がそのまま台数になります。

確認 — ここまで読めたか

Lambda の同時実行数が最大 500 まで伸びます。各実行環境はプールの最大を 10 本に設定しています。DB の max_connections は 400 です。DB に向かう接続は理論上最大で何本になりますか?

まず選ぶ(解答例は a〜d の記号で説明します)

Why — なぜ増やすと悪化するのか

「接続が足りない」と言われると、増やしたくなります。ところが、接続は増やすほど DB を遅くする面があります。

DB は、接続の数だけ速くなるわけではない

DB が実際に仕事をするのは CPU とディスクです。8コアの DB に 400 本の接続から同時にクエリが来ても、同時に進められるのはせいぜい数本から十数本です。残りは順番を待ちます。

待つ場所が DB の中か、アプリのプールの手前か、の違いです。そしてDB の中で待つほうが高くつきます

  • 接続1本ごとにメモリを使う(PostgreSQL なら1本ごとにプロセス)
  • 大量のクエリが同時に走ると、ロックの取り合いと切り替えのコストが増える
  • 待っているクエリが増えるほど、1本あたりの処理も遅くなる

プールを小さくして、アプリ側で順番を待たせたほうが、全体として速く終わることがよくあります。経験則として「コア数の2倍にディスク数を足した程度」という目安が知られていますが、これは出発点で、答えではありません。

接続を「使う時間」ではなく「握る時間」が効く

プールの本数を決めるのは、同時に何件のリクエストがあるかではありません。同時に何本の接続が握られているかです。

クエリ自体が 5ms で終わっても、その前後で接続を握ったまま別のことをしていれば、その間ずっと1本を塞ぎます。

クエリだけなら:    [借りる][SELECT 5ms][返す]                         → 5ms 握る
外部 API を挟むと: [借りる][BEGIN][INSERT][決済 API 3秒][UPDATE][COMMIT][返す] → 3秒 握る

同じ10本のプールでも、前者なら毎秒2,000件、後者なら毎秒3件しか捌けません。DB の CPU は暇なのに、API は遅い。このときログに出るのは「接続の取得待ちでタイムアウト」で、DB の負荷グラフを見ても原因は見つかりません。

確認 — ここまで読めたか

DB の CPU 使用率は 15% で余裕があります。一方で API の応答が数秒に伸び、アプリのログには「接続の取得待ちでタイムアウト」が出ています。最も疑うべきものはどれですか?

まず選ぶ(解答例は a〜d の記号で説明します)

サーバーレスでは、プールがほとんど意味を持たない

Lambda の実行環境は一度に1件のリクエストしか処理しません。その中にプールを作っても、同時に使われる接続は1本です。

それでいて、実行環境は使われていない間も接続を握ったまま凍結されます。しばらく呼ばれなければ回収されますが、そのときに接続を閉じる機会は無く、DB 側では接続がタイムアウトするまで残ります。

そして同時実行数が増えれば、実行環境が増え、接続が増えます。アプリ側でプールを作っても、DB から見た接続数は抑えられません。

ここで効くのが外部プーラーです。アプリと DB の間に置き、アプリからの大量の接続を、DB への少数の接続にまとめます。

Compare接続の持ち方

毎回作って、毎回閉じる

connect() → query() → close()
接続の準備
**毎回かかる**
DB から見た接続数
同時リクエスト数と同じ
向いている場面
ごく低頻度のバッチ
握る時間
最短
注意
同時に使われる本数
凍結中
  • 接続の準備(TCP・TLS・認証・プロセス起動)がクエリより重くなりがち
  • 同時リクエストが増えれば、そのまま DB の上限に届く
  • PostgreSQL は接続ごとにプロセスを起動するので、特に重い

DB から見た接続数を誰が抑えるか、で選ぶ。台数が勝手に増える環境では、アプリの中のプールでは抑えられない。

演習 — まず自分で判断する

解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。

演習 — 設計判断を問う

キャンペーンに備えて、Lambda から DB への接続をどう組みますか?

与えられた条件
  • API は Lambda、DB は PostgreSQL。DB の max_connections は 400
  • 普段の同時実行数は 30 前後。キャンペーン時は 600 を超える見込み
  • クエリの大半は 20ms 以内に終わる
  • DB のインスタンスを大きくする予算は、キャンペーン期間だけなら取れる
  • キャンペーンは来週。大きな作り直しはできない
この軸で考える
  • · DB から見た接続数を、何が決めているか
  • · 実行環境1つが同時に使う接続は何本か
  • · 同時実行数が見込みを超えたとき、どこで止まるか
まず選ぶ(解答例は a〜d の記号で説明します)

演習 — 説明できるか

「DB を大きくすれば解決するのでは」と言うプロダクトマネージャーに、なぜそれだけでは足りないのかを説明してください

与えられた条件
  • 相手は技術的な詳細より、費用と再発防止に関心がある
  • キャンペーン期間だけ DB を大きくする予算は承認済み
  • 来週までにできることと、その後にやるべきことを分けて伝えたい

読み終わりましたか?

読了にすると、これを前提とする記事がロードマップで開放されます。

この記事を前提にしている記事