はじめに
何となく知ってるようであまり知らないコネクションプール。
そもそもコネクションプールとは何で、何に気を付け、何を設定すればよいのかをまとめた。
書き手の知識の都合でJavaベースで書いているが、DBアクセスがある言語なら、大きな差はないはず。
コネクションプールとは?
アプリのコードからデータベースにアクセスする際、ふつう、次のような手順を踏む。
- アプリとデータベースとの接続を確立する
- トランザクションを開始する
- SQLを実行する
- トランザクションを終了する
- データベースとの接続を切断する
このうち、1.のデータベースに接続する処理は処理負荷が大きく、時間もかかる。
複数のリクエストを同時に処理するシステムではこの時間や負荷が無視できないため、それらを軽減する仕組みが必要となる。これがコネクションプールである。接続プールともいう。
コネクションプールの機能は次の2つ。
- トランザクション終了(4.)のあと、確立した接続の切断(5.)をせず、一か所で保存(プール)する
- 新規リクエストが来た際、プールしてある接続を貸し出す
また、コネクションプール側で同時最大接続数なども管理するので、大量アクセス時のデータベースサーバへの負荷を抑える働きも持つ。
Javaでのコネクションプールの使用方法
アプリケーションサーバや開発フレームワーク側で設定する。
基本的に開発者が使うAPI(JDBC APIや、Spring等のフレームワークのAPI)の裏側で動くものであり、プログラマがコネクションプールを意識する必要はない。
処理終了時に、使い終わった接続をコネクションプールに返す必要があるが、これも特別なプログラミングは必要なく、通常通り接続クローズをすればよい。
Javaの主なコネクションプールプロダクト
Webアプリの場合は、TomcatやWildFlyなど、アプリケーションサーバに備わっているコネクション管理機能を使うのが一般的。
アプリケーションサーバ上で動かさない場合や、アプリケーションサーバに依存せずにコネクションプールを使う場合は、専用ライブラリを使う。
- Commons DBCP:JavaのOSSコミュニティである、Apache Software Foundationが開発しているライブラリ。バージョン1と2は別物で、APIや設定項目名が違うので注意。
- Tomcat JDBC Connection Pool:Tomcatに組み込むために作ったライブラリ。古いバージョンのCommons DBCPの性能改善版。
- HikariCP:高速さを重視したライブラリ。SpringBoot2.0以降ではこれが標準で組み込まれている。
詳細な設定方法はChatGPTを使うとよい。製品名を指定して設定方法を聞けば、ほぼ正解を返してくれる。
コネクションプールのトラブルについて
概ね以下のように分類できる。
-
平常運用で発生した場合
- 原因1:コネクションリーク
- 原因2:DBやNWダウンの考慮漏れ
-
高負荷時に発生した場合
- 原因3:プールしているコネクション数の不足
- 原因4:コネクションリーク(原因1)が普段から発生しており、高負荷時に「障害」として現れた
原因1:コネクションリーク
「借りたものを返さない人がいると、次に借りる人に迷惑がかかる」
接続をクローズしていない処理があると、本来プールに戻るはずの接続が戻らず、プールが枯渇してしまう。
事象
ユーザが無限に待たされる、接続タイムアウトを原因とするデータベース接続エラーが頻発するなど。
分析方法
アプリケーションサーバの管理ツールやライブラリのデバッグログなどで、プールされている接続、利用されている接続の数を可視化する。
特別大きな負荷がかかっていないのにプールされている接続数が減り続けているようならコネクションリークと考えられる。業務ログなどと突き合わせ、どの処理のときにこの問題が起こっているか確認する。
対応方法
当該処理のコードを見直し、コネクションがクローズされるように修正する。
根本的には、プログラミング側でコネクションのクローズ漏れを防ぐように徹底するか、プログラミングによらない自動クローズをする必要がある。
Springを始めとする一般的な開発フレームワークでは、自動クローズの仕組みがあり、接続クローズを意識しなくていいようになっているはずなので、フレームワーク機能の利用を徹底する。
そうした機能がない場合は、try-with-resources構文(Java7以降) などを用いて、Connection#closeをプログラマが書かなくてよいようにする。
原因2:DBやNWダウンの考慮漏れ
「ずっと保留してたら、相手から切られてた」
データベースがダウンして自動復旧したり、ネットワーク接続が一瞬途切れて復旧した場合、プールしてあった接続は無効となる。
この無効になった接続が貸し出されてしまうことで、接続エラーとなる。
昔(オンプレミス)のときは、データベースがダウンした時点でサービス停止し、手動復旧を待つというシステムもあったが、昨今はクラウド上のデータベースを利用することが多い。
クラウド上のデータベースは自動復旧が前提なので、この問題に設計レベルで対応しておく必要がある。
事象
接続タイムアウトによるクエリ実行エラーなどが発生する。
分析方法
DBやネットワークの死活監視をしているなら、そのログなどと突き合わせる。
なお、コネクションプールの設定で「接続が有効かどうか」をチェックしてから貸し出す検証処理(バリデーション)を行うように設定されていれば、原因2の可能性は低い。
対応方法
プール側で「接続が有効かどうか」をチェックしてから貸し出す検証処理(バリデーション)を設定しておく。設定方法は後述。
原因3:プールしているコネクション数の不足
「今日はやたら行列ができてるので、諦めて帰ろう」
コネクションプール数には限りがある。
この上限を超えた大量のリクエストが来たことで、大量の待ちが発生した。
事象
大量のリクエストが来た際に、ユーザが無限に待たされる、接続タイムアウトを原因とするデータベース接続エラーが頻発するなど。
分析方法
Webサーバの負荷状況とエラー発生タイミングを突き合わせ、極端な高負荷時であれば、まずはこの可能性を疑う。
タイミングがズレている場合や、大量のリクエストといえど設計上十分耐えうるリクエスト数で発生した場合は、原因4を疑う。
対応方法
最大コネクション数を増やす。
事前に防ぐために、最大負荷を想定した負荷テストを実施しておく。
原因4:コネクションリーク(原因1)が高負荷時に障害として現れた
「普段なら返却が多少遅れてもいいけど、忙しいときにされるとねぇ…」
普段から問題が起こっているが、高負荷な状況になって顕在化するケース。
高負荷時に発生した場合、原因3のみを疑ってしまうことが多いので注意が必要。
- 平常処理がリークしており、高負荷時に本来使えるはずのコネクション数が確保できない
- 月末締め処理などがリークしている場合、ピーク時や、ピークを過ぎた後に発生することがある
事象
原因1、原因3と同じ。
分析方法
原因1と同じ。
検証用アプリケーションで負荷テストを再実施し、コネクションの増減を確認する。高負荷な処理を行った後、コネクションが一定時間でプールに戻っていくことを確認する。元に戻らないようであれば、その原因を探っていく。
対応方法
原因3同様、コネクションプールを増やすことで暫定対応はできる。
根本対応をするには、原因1と同様、コネクションリークを探し出して修正する必要がある。
コネクションプールの設定
重要な設定は次の通り。製品やライブラリによって呼び名は異なるが、概ねこれらの設定をしておけばよい。
- 最大コネクション数
プールできる最大の接続数。これを超えるとアプリ側で接続待ちが発生してしまう。一方で大きすぎるとDBサーバやネットワークの負荷になり、全体のスローダウンにつながるおそれがある。
接続数の厳密な計算はあまり意味がない1ので、「アプリの最大スレッド数以上」かつ「DBのデフォルトの最大コネクション数以下」の値とする。
その後、最大負荷時に待ちやスローダウンが共に発生しないよう、負荷テストで調整する。
-
最小コネクション数
アプリがDBを全く使っていなくても保持される接続数。
平常時と最大負荷時がかけ離れたアプリであれば平常時にあわせ、ピークがほぼないアプリであれば最大コネクション数と同じ値にすればよい。 -
接続タイムアウト時間
接続が全て使い切られており、待ちが発生したときに、どのくらいの時間で諦めてエラーとするかの設定。
通常はこれを設定してないと無限の待ちが発生してしまう。
高負荷時にユーザをどれくらい待たせてよいかというUXの視点から設定する。 -
検証方式
先に述べた「接続が有効かどうか」をプール側で事前にテストする処理(バリデーション)の有効化設定。
検証には「接続時」「定期的」の2つがあり、一般的にはそれぞれ有効にするか無効にするかを設定する。デフォルトでは両方が無効になっていることが多い。- 接続時
アプリ側に接続を貸し出す際に、内部で検証クエリを実行し、タイムアウトした場合には新規に接続を確立して返す。 - 定期的
プールしている接続に対して定期的に検証クエリを実行し、タイムアウトした接続は破棄する。
DBがダウンしたら停止してよいシステムなら無効のままでもよいが、クラウド上のDBやクラスタを組んだDBなど、DB自動復旧を前提とするシステムなら、少なくとも接続時検証は必須である。
定期的監視は、専用のスレッドやバックグラウンド処理が必要になるので、誤差の範囲ではあるが負荷が増大する。UXに若干の影響を与える「リクエスト時の新規接続確立」を抑えられるので、応答性能が重要なアプリなら設定しておくとよい。 - 接続時
-
検証クエリ
接続時に実行する検索用SQL。「SELECT 1」や「SELECT 1 FROM dual」など、データベース製品にあわせて、最もシンプルなSQLを記述する。 -
検証タイムアウト時間
検証クエリの応答待ち時間。この時間を超えて応答がない場合、検証失敗と判断する。 -
コネクション生存期間
有効無効問わず、プールされ続けている接続を一定時間で破棄するための設定。
わざわざ有効なものを破棄する理由として、負荷分散の正常化が挙げられる2。
-
1つの待ち行列を複数のエージェントで処理するので、M/M/s型の待ち行列となる。コールセンターのエージェント数を求める際に用いられる、アーランC式が近い。ただし、負荷によるサーバ性能やNW性能の低下など、M/M/sモデルでは考慮していない制約があること、コールセンターと異なり窓口数(s)を増やすコストが低いことから、厳密に計算する意味はほとんどない。 ↩
-
複数のインスタンスで負荷分散しているDB(ACTIVE-ACTIVE構成)で、インスタンスAがダウンした場合、Aとの接続は無効となり、全ての接続はインスタンスBに繋ぎなおされる。Aが復帰した際に、AとBの負荷を均等にするために、Bとの接続を一部切断してAにつなぎなおす処理が必要となる。
コネクション生存期間を設定しておくことで、この処理が自動的に行われる。 ↩