RailsアプリケーションのConnection Poolとパフォーマンスへの影響を理解する
Webアプリケーションのボトルネックはたくさんある
最近、Webアプリケーションのパフォーマンスについて考えたり、教えてもらったりする機会が増えた、
その中で、今まであまり意識していなかったWebアプリケーションのアプリケーション - DB間のボトルネックについて知ったので、記す。
Connection Poolとは?
1 Requestの処理ごとにDBへの接続をイチから要求するのではなく、既に接続が確立されているConnectionを使い回すことを指す。
例えば、あらかじめ5つのConnectionをPoolに保持しておく。するとアプリケーションはDBへの接続の初期化・終了処理をせずに同時に5プロセスまでDBにアクセスすることができる。 この仕組みを利用することで、アプリケーション - DB間のやりとりが簡略化され、結果としてパフォーマンスの向上に繋がることがある。 ただし、Connection PoolはDB側で用意されたConnection limitを超えて用意することはできず、それを超えようとするとタイムアウトが発生してしまう。
Connection Limitを増やすには
以下、PostgreSQLでの話。
PostgreSQLの設定にはmax_connections
というものがあり、この値を超えない限りはConnection Poolを増やすことができる。
このmax_connection
は無尽蔵に大きくできるものではなく、同時接続数が増えれば増えるほどDB側のメモリを消費していく。DBが動いているサーバのメモリサイズに応じて適切に設定してあげる必要がある。
ちなみに、Heroku-Add onのPostgreSQLでは一番高いプランでもlimitが500と決められている。
Connection Poolを設定してみる
さて、このConnection PoolはRailsの設定で変更ができる。
具体的には、config/database.yml
のpool
という部分の数字を変更してやれば良い。Railsのデフォルトは5
。
# config/database.yml default: &default adapter: sqlite3 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000
しかし、この数字はどう扱えばいいのか?もちろん、デカくすればいいというものではない。
前述の通り、DB側のmax_connections
を超えてConnectionを維持することはできない。さらに、Railsはpool
の設定に書かれている数字までConnectionを増やして良いものとして認識するので、この数字を適当に設定してしまうと、アプリケーションにはアクセスできるのに、アプリケーションからDBに接続する部分がタイムアウトになってしまうという残念な結果になる。
RailsはデフォルトでPumaをWebサーバーとして利用するので、以下、Puma限定の話。
ここに書かれている通り、
If you are using the Puma web server we recommend setting the pool value to equal ENV['RAILS_MAX_THREADS']. When using multiple processes each process will contain its own pool so as long as no worker process has more than ENV['RAILS_MAX_THREADS'] then this setting should be adequate.
ENV['RAILS_MAX_THREADS']
と同じ数値を設定してやれば良いらしい。
この環境変数はconfig/puma.rb
のThread設定に使われている。
# config/puma.rb # Puma can serve each request in a thread from an internal thread pool. # The `threads` method setting takes two numbers: a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
さらに、上記のHeroku記事にあるように、Railsはプロセス毎にConnection Poolを持つらしい。 Pumaは1 Worker毎にプロセスをForkする。
最終的に、thread = pool
, worker * thread = connection limit
となるように設定してやる必要がある。
なお、Pumaが使おうとするThread数・Worker数は、Railsを起動した時のログに表示される。 Worker数を設定しなかった、あるいはWorker数が1の場合、PumaはSingle Modeとして起動する。
ちなみに、Workerが複数存在するモードをClustered Modeと呼ぶ。
web_1 | => Booting Puma web_1 | => Rails 6.0.2.1 application starting in development web_1 | => Run `rails server --help` for more startup options web_1 | [8] Puma starting in cluster mode... web_1 | [8] * Version 4.3.1 (ruby 2.6.3-p62), codename: Mysterious Traveller # Pumaが使おうとするThread数が表示される ここではmin, max共に101 web_1 | [8] * Min threads: 101, max threads: 101 web_1 | [8] * Environment: development # Worker数が表示される。ここでは10 # 101 Thread * 10 Workerなので、最大1010のConnction Limitが必要になる # もちろんそんなクソデカConnection Limitを設定してはいけない web_1 | [8] * Process workers: 10 web_1 | [8] * Phased restart available web_1 | [8] * Listening on tcp://0.0.0.0:3000 web_1 | [8] Use Ctrl-C to stop web_1 | [8] - Worker 0 (pid: 19) booted, phase: 0 web_1 | [8] - Worker 1 (pid: 22) booted, phase: 0 web_1 | [8] - Worker 2 (pid: 46) booted, phase: 0 web_1 | [8] - Worker 3 (pid: 51) booted, phase: 0 web_1 | [8] - Worker 9 (pid: 85) booted, phase: 0 web_1 | [8] - Worker 4 (pid: 66) booted, phase: 0 web_1 | [8] - Worker 5 (pid: 71) booted, phase: 0 web_1 | [8] - Worker 8 (pid: 84) booted, phase: 0 web_1 | [8] - Worker 7 (pid: 83) booted, phase: 0 web_1 | [8] - Worker 6 (pid: 82) booted, phase: 0
Puma自体のThread, Worker数の最適な数値については、利用しているサーバーのスペックによって変化が大きいので割愛。
おわりに
単一のアプリケーションサーバーであればこれで終わりだが、世の中ではアプリケーションサーバーのインスタンスをスケールさせることがよくある。
なので、そのようなケースではmax_scale_size * worker * thread = connection limit
という計算で良い、のかな...?
実際にそれでいい感じに動くのか試していないので、今後要検証。