データベースパフォーマンスを最大化するためのコネクションプールのチューニング
James Reed
Infrastructure Engineer · Leapcell

はじめに
ハイパフォーマンスアプリケーションの世界では、データベース接続がしばしばクリティカルなボトルネックとなります。アプリケーションがデータベースと対話する必要があるたびに、接続の確立と切断にオーバーヘッドが発生します。高い同時実行性と頻繁なデータベースアクセスを経験するアプリケーションでは、このオーバーヘッドがシステム全体のパフォーマンスとリソース利用率を大幅に低下させる可能性があります。これを軽減するために、PgBouncerのようなコネクションプーリングソリューションや、組み込みのアプリケーションレベルのプールは不可欠です。これらのツールは、開かれたデータベース接続のセットを管理し、アプリケーションがリクエストごとに新しい接続を作成するのではなく、それらを再利用できるようにします。しかし、コネクションプールを使用するだけでは十分ではありません。不適切に構成されたプールは、皮肉なことにパフォーマンスを悪化させたり、根本的な問題を隠したりする可能性があります。この記事では、PgBouncerとアプリケーションレベルのコネクションプールの両方のパラメータを最適化する技術と科学を探求し、ピーク時のデータベースパフォーマンスを達成し、アプリケーションがスムーズかつ効率的に実行されることを保証します。
コアコンセプトと原則
最適化戦略を詳しく説明する前に、コネクションプーリングの理解の基本となるいくつかのコア用語と原則を定義しましょう。
主要な用語
- コネクションプール: コネクションプーリングコンポーネントによって維持されるデータベース接続のキャッシュ。アプリケーションが接続を必要とするとき、プールから1つを要求します。使用後、接続は再利用のためにプールに返されます。
- PgBouncer: PostgreSQL用の軽量な単一プロセスコネクションプーラー。クライアントアプリケーションとPostgreSQLサーバーの間に配置され、データベースサーバー上の接続オーバーヘッドを大幅に削減します。
- アプリケーションレベルコネクションプール: アプリケーションコード内またはライブラリ(例:JavaのHikariCP、PythonのSQLAlchemyの
QueuePool
)を通じて直接実装されたコネクションプール。 - データベースサーバーの接続制限: PostgreSQLサーバーが受け入れるように構成されている同時データベース接続の最大数(
postgresql.conf
のmax_connections
)。この制限を超えると、接続エラーが発生します。 pool_size
(またはアプリケーションプールでのmaximum_pool_size
/max_connections
): プール自体が維持するデータベース接続の最大数。これはチューニングすべき重要なパラメータです。min_pool_size
(またはminimum_idle
): プールが維持しようとするアイドル接続の最小数。これにより、トラフィックが少ない期間でも接続がすぐに利用可能になります。idle_timeout
(またはmax_idle_time
): アイドル接続がプール内で保持される最大時間。リソースを回収するのに役立ちます。max_lifetime
(またはPgBouncerのconnection_timeout
、max_age
): 接続がプール内で存続できる最大時間。アイドル状態に関係なく。古い接続の問題を防いだり、定期的な再認証を確保したりするのに役立ちます。wait_timeout
(または取得のためのconnection_timeout
): 利用可能な接続がない場合に、クライアントがプールから接続を取得するのを待つ最大時間。これを超えると、タイムアウトエラーが返されます。- PgBouncerの接続モード:
- セッションプーリング(
pool_mode = session
): 接続は、クライアントの「セッション」の全期間(切断されるまで)クライアントに割り当てられます。これは最も安全なモードであり、直接接続と最も同様に動作します。 - トランザクションプーリング(
pool_mode = transaction
): 各トランザクション(COMMIT
またはROLLBACK
)の後、接続はプールに返されます。このモードはより高い接続再利用性を提供しますが、セッション固有の状態を注意深く処理する必要があります。 - ステートメントプーリング(
pool_mode = statement
): 各ステートメントの後、接続はプールに返されます。このモードは最も高い再利用性を提供しますが、セッションコンテキストの喪失により、多くのアプリケーションとの互換性がほとんどない、最も制限の厳しいモードです。
- セッションプーリング(
- サンダーハード問題: 利用可能なリソースが限られている場合に、多数のクライアントが同時にリソースを取得しようとし、競合とパフォーマンス低下を引き起こす問題。コネクションプールはこれを適切に管理する必要があります。
最適化の原則
コネクションプールの最適化の主な目標は、次の間のバランスを取ることです。
- 接続確立オーバーヘッドの最小化: 既存の接続を再利用することによって。
- データベースリソース利用率の最大化: 必要以上の接続を保持しないことによって。
- データベースサーバーの過負荷防止: アクティブな接続の総数を制限することによって。
- アプリケーションの応答性の確保: 接続を迅速に提供するか、リソースが実際に枯渇したときに迅速に失敗することによって。
PgBouncerパラメータの最適化
PgBouncerは、クライアント接続をより小さなサーバー接続セットに多重化するPostgreSQLの優れたフロントエンドとして機能します。
PgBouncer設定(pgbouncer.ini
)
[databases] mydb = host=127.0.0.1 port=5432 dbname=mydb auth_user=pgbouncer_user [pgbouncer] listen_addr = 0.0.0.0 listen_port = 6432 ; PgBouncer自体の接続制限 max_client_conn = 1000 ; PgBouncerが受け入れる最大クライアント接続数。高く設定すべきです。 default_pool_size = 20 ; 指定されていない場合のプールのデフォルトサイズ。 ; pool_size = 20 ; 'mydb'データベース用。default_pool_sizeをオーバーライドします。 ; コアプーリングパラメータ pool_mode = transaction ; ほとんどのWebアプリケーションで一般的です。「session」はセッション状態を持つアプリ用です。 reserve_pool_size = 2 ; emergencys用にPgBouncerが保持する接続数。 reserve_pool_timeout = 5.0 ; reserve接続を待つクライアントの秒数。 ; サーバー接続管理 server_reset_query = DISCARD ALL ; transaction/statementプーリングに重要です。 server_check_delay = 10 ; PgBouncerがサーバーのヘルスチェックを行う頻度 server_lifetime = 3600 ; サーバー接続が使用できる最大秒数。 ; クライアント接続管理 client_idle_timeout = 300 ; この時間アイドル状態のクライアント接続を閉じます。 client_login_timeout = 60 ; クライアントがログインする最大時間。 ; 高負荷のチューニング max_db_connections = 0 ; サーバー上のデータベースあたりの最大接続数。0はPgBouncerによる無制限を意味します(サーバーのmax_connectionsを使用)。 ; max_db_connections = 50 と pool_size = 20 を設定した場合、PgBouncerはDBに20接続のみ使用します。 ; max_db_connections = 20 と pool_size = 50 を設定した場合、PgBouncerは実質的に20で制限されます。 ; 通常、pool_sizeが特定のプールに対するDBへの実質的な制限となります。 max_user_connections = 0 ; サーバー側のユーザーあたりの最大接続数。 max_server_conn = 100 ; PgBouncerからすべてのバックエンドサーバーへの最大接続数。すべてのpool_sizeの合計以上であるべきです。
PgBouncerの最適化戦略:
pool_mode
:transaction
(ほとんどのWebアプリで推奨): 優れた接続再利用性を提供します。アプリケーションロジックがトランザクション間でリセットされないセッション固有の変数や一時テーブルに依存していないことを確認してください。「server_reset_query = DISCARD ALL
」は、セッション状態をクリーンアップするために不可欠です。session
: アプリケーションがセッション固有の設定、クライアントが管理しない準備済みステートメント、または複数のトランザクションにまたがる一時テーブルに強く依存している場合に使用します。接続再利用性は低いですが、セッションの一貫性を保証します。statement
(めったに使用されない): 各クエリが完全に独立しているアプリケーションにのみ使用されます。セッション状態が持続するため、エラーが発生しやすいですが、再利用性は非常に高いです。
pool_size
: これは最も重要なパラメータです。- 開始点: 一般的な目安は「
(CPUコア数 * 2) + 実効スピンドル数
」です。最新のSSDベースのシステムでは、「(CPUコア数 * 2) + 実行中のクエリのワーカープロセス数 / 2
」を検討してください。 - 監視が鍵: 適切な値(例:中程度の負荷のアプリケーションでは20〜50)から始めて、PgBouncerとPostgreSQLサーバーの両方のアクティブな接続を監視します。PgBouncerでのキューイング(
SHOW STATS;
またはSHOW POOLS;
を使用)を観察します。クライアントが繰り返し接続を待っている場合は、「pool_size
」を増やします。データベースサーバーのCPU負荷が高い場合は、それを減らします。 - データベースサーバーの
max_connections
を超えないこと: すべてのPgBouncerインスタンスのすべてのpool_size
値の合計は、データベースサーバーのmax_connections
未満であるべきです(バッファーを考慮)。
- 開始点: 一般的な目安は「
reserve_pool_size
: 少数の数(1〜2)は、メインプールが飽和したときのセーフティネットとして機能します。server_lifetime
: 適切な値(例:1時間、3600
秒)に設定します。これにより、アイドル状態のサーバー接続が定期的に閉じられ、再確立されるため、長時間持続する古い接続やデータベース側のメモリリークの問題が軽減されます。client_idle_timeout
: アイドル状態のクライアント接続を閉じることで、max_client_conn
スロットが解放されます。アプリケーションの最も長い予想アイドル時間よりわずかに長い値に設定します。max_client_conn
: PgBouncerに接続する最大予想同時クライアント数よりも高い数に設定します。これは、バックエンドデータベースではなく、PgBouncer自体の制限です。
アプリケーションレベルコネクションプールの最適化
多くのフレームワークやORMは、組み込みのコネクションプーリングを提供しています。ここでは、HikariCP(Java)とSQLAlchemy(Python)を例として使用します。
HikariCP(Java Spring Bootの例)
// application.properties または application.yml spring.datasource.url=jdbc:postgresql://localhost:6432/mydb spring.datasource.username=myuser spring.datasource.password=mypassword # HikariCP固有の設定 spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.idle-timeout=300000 ; 5分 spring.datasource.hikari.max-lifetime=1800000 ; 30分 spring.datasource.hikari.connection-timeout=5000 ; 接続を取得するまでの5秒 spring.datasource.hikari.pool-name=MySpringBootHikariPool spring.datasource.hikari.auto-commit=true ; Webアプリでは通常true
HikariCPの最適化戦略:
maximum-pool-size
(pool_size
に相当):- ガイダンス: 同じ「
(CPUコア数 * 2) + 実効スピンドル数
」の目安を使用します。ただし、PgBouncer経由で接続する場合は、直接PostgreSQLに接続する場合よりも一般的に低く設定する必要があります。すべてのアプリケーションインスタンスのmaximum-pool-size
の合計と、他の直接接続の合計は、そのデータベースのPgBouncerのpool_size
またはDBのmax_connections
よりも小さくなるはずです。 - 監視: データベースの同時接続数、アプリケーションのリクエストレイテンシ、「接続待ち」メトリクスを監視します。接続が頻繁に枯渇してタイムアウトを引き起こしている場合は、
maximum-pool-size
を徐々に増やします。軽いアプリケーション負荷の下でデータベースのCPUまたはI/Oが高い場合は、それを減らします。
- ガイダンス: 同じ「
minimum-idle
: 適切な数(例:5〜10)に設定します。アイドル状態の接続をいくつか準備しておくことで、トラフィックの急増時に新しい接続を起動する必要性が減りますが、リソースを浪費しないように高すぎないように設定してください。idle-timeout
:max-lifetime
よりも短く設定します。真にアイドル状態の接続を閉じます。5〜10分が良い値です。max-lifetime
: 古い接続の発生を防ぐために重要です。データベースのwait_timeout
またはPgBouncerのserver_lifetime
よりも短い時間に設定します。接続を定期的に再起動することは、データベースクラスター内の異なるデータベースノード間でロードバランシングを確保するためにも役立ちます(DBの前にロードバランサーを使用している場合)。30〜60分(1800000
-3600000
ミリ秒)の値が一般的です。connection-timeout
: アプリケーションがプールから接続を取得するのを待つ時間を決定します。短いタイムアウト(例:5秒)は、ハングするのではなく、迅速に失敗するためにユーザー向けのアプリケーションにはしばしば適しています。バックグラウンドジョブでは、より長いタイムアウトが許容される場合があります。
SQLAlchemy(Pythonの例)
from sqlalchemy import create_engine from sqlalchemy.pool import QueuePool import os DATABASE_URL = os.environ.get("DATABASE_URL", "postgresql://myuser:mypassword@localhost:6432/mydb") engine = create_engine( DATABASE_URL, poolclass=QueuePool, pool_size=20, # プール内の最大接続数 max_overflow=0, # pool_sizeを超えて0を許可(合計 = pool_size + max_overflow) pool_timeout=10, # 接続を取得するまで最大10秒待機 pool_recycle=1800, # 30分(1800秒)後に接続をリサイクル pool_pre_ping=True # 使用前に接続の有効性をチェック ) # 使用例 # with engine.connect() as connection: # result = connection.execute(text("SELECT 1")) # print(result.scalar())
SQLAlchemyの最適化戦略:
pool_size
: HikariCPと同様の原則。監視とワークロードに基づいてチューニングします。PgBouncerを使用している場合、このpool_size
はPgBouncerのpool_size
に関連するはずです。max_overflow
: このパラメータは、プールが一時的にpool_size
を超えて拡張することを許可してスパイクを処理します。即時の接続枯渇を防ぐことができますが、サイズ不足のpool_size
を隠したり、データベースにさらに圧力をかけたりする可能性もあります。0
または非常に小さな数値に保ち、pool_size
を正しくサイズ設定しておく方が安全な場合が多いです。pool_timeout
: プールから接続を取得するのを待つ時間。応答性(例:5〜10秒)を目指します。pool_recycle
:max_lifetime
に相当します。古い接続の発生を防ぐために不可欠です。データベースのwait_timeout
よりも短く設定します。pool_pre_ping=True
: これは便利ですが、各接続の使用前またはpool_checkin
時に小さなオーバーヘッド(SELECT 1
クエリ)が発生します。接続が生きていることを保証し、古い接続によるエラーを防ぎます。「サーバーが予期せず接続を閉じました」のようなエラーを頻繁に遭遇する場合は、これを使用してください。
両方のタイプのプールに関する一般的なベストプラクティス
- 監視、監視、監視: これをいくら強調しても足りません。アクティブな接続、アイドル接続、接続取得時間、待機時間、接続タイムアウト、データベースCPU/IOを追跡します。
pg_stat_activity
(PostgreSQL)、PgBouncerのSHOW STATS
とSHOW POOLS
、およびアプリケーションレベルのメトリクス(例:HikariCPメトリクスを備えたPrometheus)などのツールを使用します。 pool_size
のバランス: アプリケーション(複数可)がデータベースから引き出す接続の総数は、データベースサーバーのmax_connections
を超えてはなりません。PgBouncerを使用している場合、特定のデータベースに対するすべてのPgBouncerpool_size
の合計は、管理タスクのためのバッファーを残すことが理想的です。pool_size
を可能な限り小さく、必要な限り大きく保つ: より大きなプールは、アプリケーションとデータベースの両方でより多くのメモリを消費します。過剰なアイドル接続なしにピーク負荷を処理するのに十分な接続があるスイートスポットを見つけます。max_lifetime
/pool_recycle
を使用する: これは堅牢性のために不可欠であり、接続が悪い状態になる可能性のある一時的なネットワークの問題やデータベースの再起動による問題を回避します。また、データベース側のメモリ管理にも役立ちます。- 適切な
timeout
値を設定する: クライアントが接続を無期限に待たないようにします。リソースが実際に利用できない場合は迅速に失敗します。 - 多層アプローチを検討する: 多くの場合、PgBouncerとアプリケーションレベルのプーリングの組み合わせが最適です。PgBouncerは、多数のクライアントからの大量の短命な接続を処理し、アプリケーションプールは、しばしばPgBouncer自体へのより少ない数の接続に対して、アプリケーションコード内で堅牢な管理を提供します。
結論
PgBouncerまたはアプリケーションレベルのソリューションのコネクションプールパラメータの最適化は、万能なタスクではありません。アプリケーションのワークロードの深い理解、注意深い構成、および継続的な監視が必要です。pool_size
、idle_timeout
、max_lifetime
などのパラメータを戦略的にチューニングすることで、データベースのオーバーヘッドを大幅に削減し、アプリケーションの応答性を向上させ、効率的なリソース利用を確保し、最終的にはよりパフォーマンスが高く安定したシステムにつながります。成功の鍵は、リソースの可用性とデータベースの安定性のバランスを取り、観察されたパフォーマンスメトリクスに基づいて構成を適応させることに積極的であることです。