Spring Bootアプリケーションのパフォーマンスチューニングをする機会があったのですが、その際に読んだHikariCP(Java製のコネクションプールライブラリ)のドキュメントに興味深い記述があったので紹介します。
オリジナルのドキュメント: About Pool Sizing
https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing
DBコネクションは(ほとんどの場合)多すぎる
英語が苦手でなければ、まず以下の動画を見てください。ここでは、DBコネクションの数を2000 => 96に減らすことで、レスポンスタイムが100ms => 2msに改善するデモが行われています。
このデモ内では、Webアプリケーションを想定して、約10000のユーザが同時にアクセスするようなシナリオを想定しています。このような場合でも、WebアプリケーションからDBサーバへの接続は100足らずで問題なく、コネクション数を減らした方が高速に処理できます。
なぜこのようなことが起こるのでしょう?
CPUのタイムスライシング
複数の仕事を同時に行う必要がある場合、CPUはそれらを少しずつ時間を区切って実行します。これをタイムスライシングといいます(基本情報技術者試験にも出題されてますね)。
タイムスライシングによってCPUは複数の仕事を同時に行えますが、それが仕事を最速で終わらせる方法とは限りません。2つの仕事があるとき、それを順番にやる場合と同時にやる場合では、同時にやる場合の方がスイッチングコストの分遅くなります。
DB接続数が多い場合、DBサーバのCPUは多くのタスクを同時に行う必要があり、レイテンシが大きくなります。
パフォーマンス観点からコネクション数を考える
では、DBサーバのパフォーマンスを最適化できるコネクション数はどの程度なのでしょうか? HikariCPのドキュメントでは、PostgreSQLのドキュメントを参照し、
connections = ((core_count * 2) + effective_spindle_count)
コネクション数 = (コア数 * 2) + スピンドル数
と記載しています。
スピンドルというのはHDDの軸のことです。DBサーバがSSDを搭載している場合、スピンドル数は0とみなすことができます。なぜなら、スピンドル数の分コネクション数を増やしているのは、HDDではI/Oの待ち時間を考慮する必要があるからです。この点、SSDはHDDよりI/Oが速いので、HDDのようにI/Oの待ち時間を考慮してコネクション数を増やす必要はありません。
現在では多くのDBサーバがSSDを使っているので、「コネクション数 = コア数 * 2」がDBサーバのパフォーマンスを最大限に発揮できるコネクション数であると考えられます。
デッドロック回避のためのコネクション数
1つのactor(プロセス、スレッドなど、アプリケーションの実行単位)が多くのDBコネクションを使うと、コネクションが解放されず、他のactorがコネクションを取得できなくなります。これを「Pool-locking」と呼んでいます。
これは主にアプリケーションの側で解決すべき問題であると考えられます。しかし、アプリケーションの改修が難しくコネクションプールの設定で調整する必要がある場合、以下の式によって、最低限必要なコネクション数を求めることができます。
コネクション数 = アクターの最大数 * (同時に保持する必要があるコネクション数 - 1) + 1
例えば、最大で8つのスレッドが最大で3つのコネクションを保持する必要がある場合、最低限必要なDBコネクションの数は 8 * (3-1) + 1 = 17となります。
Webアプリケーションのように、DBコネクションをすぐに解放するような場合には、パフォーマンス重視のコネクション数にすることが望ましいです。一方で、実行時間の長いバッチなど、DBコネクションを保持し続ける必要のあるアプリケーションでは、デッドロックによって処理が止まることがないようにする必要があります。
このように、コネクションプールのサイズはアプリケーションの特性を考慮に入れて決める必要があります。
複数のサーバからDB接続する場合
ここまで読んで、アプリケーションサーバが複数ある場合はどうするの? と思った方もいるでしょう。
HikariCPのドキュメントには記載がありませんが、HikariCPのissueで複数のアプリケーションが1つのDBサーバを共有している場合について議論されています。
https://github.com/brettwooldridge/HikariCP/issues/1023
このissueによると、アプリケーションサーバの台数増にしたがって、1アプリケーションサーバあたりのコネクションプールサイズは減らすのが基本的な考え方になります。
つまり、
1アプリケーションサーバあたりのコネクション数 = DBサーバのコア数 * 2 / アプリケーションサーバの台数
ということです。
しかし、実際に全てのアプリケーションサーバがコネクションを限界まで使い切るとは限りません。これについてはプロファイラを使って、各アプリケーションが実際に同時に利用するコネクション数の最大数を求め、それをコネクションプールの上限に設定することが望ましいです。
HikariCPでは Dropwizard Metrics を使ってコネクションプールのメトリクスを取得できます(Dropwizardというフレームワークの名前が付いてますが、Spring Bootでも利用できます)。