株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。
Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
▶コーポレートサイト
このシリーズについて
「知ってはいる。けど、人に説明しろと言われると詰まる」——そんな歯がゆいDB用語を、1記事1用語・図解中心で解消していくシリーズです。
- 第1回:ACIDの「C(一貫性)」、説明できますか?
- 第2回:トランザクション分離レベル ―「READ COMMITTED」で結局なにが読めるの?
- 第3回:ロックとデッドロック ―「誰が」「何を」「どこまで」ロックしている?
- 第4回:「スキーマ」って結局なに? ― 同じ単語が文脈で別物を指している
- 第5回:正規化 ―「第3正規形まで」の「まで」って何?
- 第6回:インデックス ― 貼ったのに効かないのはなぜ?
- 第7回:実行計画 ― EXPLAINの出力、どこを見る?
- 第8回:N+1問題 ― なぜ「1回」で済まないのか
まず自己診断
前回はクエリの本数の話でした。今回は接続の数です。
application.yml に、こういう設定を書いたことがあると思います。
maximum-pool-size: 50
この 50 はどこから来ましたか。
「なんとなく」「多めにしておけば安心だから」
だとしたら、この記事の対象読者です。しかもこの直感、多くの場合は逆効果です。
なぜ接続は「高価」なのか
まず前提から。DBへの接続は、思っているより重い処理です。1本張るたびに、これだけのことが起きます。
実測:接続を使い回すかどうかで77倍
どれくらい高価なのか測ります。PostgreSQL 16.14(Docker postgres:16) で pgbench を使い、同じ処理を2通りで実行しました。10クライアント・10秒間の参照系ワークロードです。
# A: 接続を使い回す(プールがある状態に相当)
pgbench -U postgres -c 10 -j 4 -T 10 -S demo
# B: トランザクションごとに接続し直す(プールがない状態に相当。-C オプション)
pgbench -U postgres -c 10 -j 4 -T 10 -S -C demo
| スループット | 平均レイテンシ | |
|---|---|---|
| A. 接続を使い回す | 59,322 tps | 0.169 ms |
| B. 毎回接続し直す | 766 tps | 13.043 ms |
約77倍の差です。しかもこれは同一マシン内のDocker、つまりネットワーク往復がほぼゼロの条件でこれだけ開きます。
処理内容は完全に同じ。違うのは「接続を張り直すかどうか」だけ。接続の生成コストが、クエリ本体の実行時間を完全に上回っているわけです。
エンジン差:PostgreSQLは接続ごとに「プロセス」
なぜここまで高いのか。PostgreSQLの場合、接続1本ごとにOSのプロセスを1つforkするからです。実際に5本接続して確認しました。
8454 postgres: postgres demo [local] SELECT
8455 postgres: postgres demo [local] SELECT
8456 postgres: postgres demo [local] SELECT
8457 postgres: postgres demo [local] SELECT
8458 postgres: postgres demo [local] SELECT
5接続 = 5プロセス。 PIDが別々に振られています。プロセスの生成はスレッドより重く、メモリも各プロセスぶん必要です。
一方MySQLは接続ごとにスレッドを割り当てるモデルです。スレッドのほうが軽量なので、この点ではMySQLのほうが接続コストが低い傾向にあります。「PostgreSQLではコネクションプールがほぼ必須」と言われるのは、この構造の違いが理由です。
プールの仕組み:借りて、返す
そこで登場するのがコネクションプールです。あらかじめ接続を数本張っておき、使い回すという、それだけの仕組みです。
重要なのは、アプリが「返却」しても実際には切断されない点です。接続は張りっぱなしで、次の利用者に回されます。だから毎回の接続コストがゼロになります。
なお、接続が全部貸出中のときに借りに来たスレッドは待たされます。この「待ち行列」がのちほど効いてきます。
「困ったらプールを増やす」が効かない理由
さて本題です。レスポンスが遅い。プールサイズを増やせば直りそうな気がします。でも、たいてい直りません。
理由は単純で、DBが同時にこなせる仕事量には上限があるからです。CPUコア数もディスクも有限なので、接続だけ増やしても処理能力は増えません。
さらに、増やしすぎるとDB側の上限(PostgreSQLなら max_connections)に達して接続エラーになります。アプリが複数台あれば、その台数ぶんプールが存在することも忘れがちな落とし穴です。
実測:接続を12倍にしても、スループットは1.4倍
これを測りました。同じDBに対し、同時接続数だけを変えて書き込みを含むワークロードを10秒ずつ実行しています。
| 同時接続数 | スループット | 平均レイテンシ |
|---|---|---|
| 8 | 985 tps | 8.120 ms |
| 16 | 1,092 tps | 14.644 ms |
| 32 | 1,271 tps | 25.163 ms |
| 64 | 1,363 tps | 46.945 ms |
| 96 | 1,394 tps | 68.836 ms |
読み取ってほしいのはこの2点です。
- 接続を8→96と12倍にして、スループットは +41% しか増えていない
- その代わりレイテンシは8.120ms→68.836msと8.5倍に悪化した
つまり、増やした接続は仕事を増やしたのではなく、行列を長くしただけです。スループットが頭打ちなのに同時実行を増やせば、1本あたりの待ち時間が伸びる——ただそれだけのことが起きています。
ユーザーが体感する「遅い」はレイテンシです。プールを増やすと、まさにその指標が悪化する。 これが「困ったらプールを増やす」が逆効果になる構図です。
で、結局いくつにすればいい?
有名な指針が、Javaのコネクションプール実装 HikariCP の公式Wiki「About Pool Sizing」にあります。
connections = ((core_count * 2) + effective_spindle_count)
core_count はCPUコア数、effective_spindle_count は実効的なディスク本数(SSDや構成により考え方は変わります)。4コア+ディスク1本なら 4×2+1 = 9。つまり10本前後です。
「たった10本?」と思ったなら、それが歯がゆさの正体です。同Wikiには、この小さなプールで3000人規模のフロントエンドユーザーを6000 TPSでさばけるという試算も示されています。
なぜ小さくてよいのか。理由はこう説明されています。
「単一のCPUリソースにおいて、AとBを順番に実行するほうが、時間分割で"同時に"実行するより常に速い、というのはコンピューティングの基本法則である」
同Wikiでは、Oracleの実演としてプールサイズを減らしただけで応答時間が約100msから約2msへ、50倍以上改善した事例も紹介されています。
順序が大事です。**プールサイズは「最後に触るつまみ」**であって、最初に増やすものではありません。遅い原因がN+1(第8回)やインデックス(第6回)にあるなら、プールをいくら増やしても解決しません。
実測についての注記
正直に書いておくと、今回の数値はすべて同一マシン上のDockerコンテナ(16コア、max_connections=100、shared_buffers=128MB)での測定です。第8回と同じくネットワーク往復がほぼありません。
本番はアプリとDBが別マシンなので、接続確立にはネットワーク往復も加わり、「毎回接続し直す」コストは今回の77倍差よりさらに開く方向に働きます。一方で、最適なプールサイズはCPUコア数・ディスク性能・クエリの重さ・アプリの台数で変わるため、上の表の具体的な数値をそのまま持ち込むことはできません。
式は出発点、最終判断は自分の環境での計測——これはHikariCPのWikiでも強調されている点です。
まとめ:1行で説明するなら
「DB接続はTCP確立・認証・リソース確保を伴う高価な処理で、PostgreSQLでは接続ごとにプロセスがforkされます。だから毎回張り直さず、あらかじめ張った接続を借りて返すのがコネクションプールです。サイズは大きいほどよいわけではなく、DBの処理能力は有限なので増やしても行列が伸びてレイテンシが悪化するだけ。目安は
コア数×2+ディスク本数程度と意外に小さく、遅いときはプールを増やす前にクエリ側を疑います」
冒頭の maximum-pool-size: 50 に戻ります。この数字の根拠を聞かれたら、「コア数」と「計測結果」で答えられるか。 それがこの用語を理解しているかの分かれ目です。
次回
第10回:ビューとマテリアライズドビュー ——「どっちをいつ使う?」を図解します。
(シリーズ全11回の予定は第1回に掲載しています)
参考
- PostgreSQL 16 Documentation - Connections and Authentication(max_connections)
- PostgreSQL 16 Documentation - How Connections Are Established
- PostgreSQL 16 Documentation - pgbench
- HikariCP Wiki - About Pool Sizing
- MySQL 8.0 Reference Manual - Connection Interfaces
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!