Rails のコネクションプールについて( Puma の Worker 数も変えてみた)

Railsのコネクションプール経由で行っているDB接続について、少し調べたのでメモを残します。

実行環境

Rails 6.1.3.1

Connection Pool とは

コネクションプールとは、Railsの処理がデータベースにアクセスするたびにコネクション接続と切断を行って負荷が高くなったり、パフォーマンスが低下するのを防ぐために、予め決められた上限数を考慮してデータベースとの間に作っておく接続のグループのことです。

tech-book.precena.co.jp

データベースに接続した状態を、メモリ上にあらかじめ確保(キャッシュ)しておき、データベースにアクセスするタイミングで、Poolから利用可能な接続を再利用します。
毎回接続して切断するより、DB接続というコストの高い処理をあらかじめ行うことで、DB接続にかかる時間を短縮でき、効率よく処理を行うことができるというメリットがあるとのこと。

Connection pool の設定について

Connection pool は、database.yml で設定します。

default: &default
  adapter: mysql2
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

pool はアプリがデータベースに対して保持できる接続の最大数です。
上記でデフォルトの 5 を設定しています。

また、ActiveRecord はスレッドごとに個別のデータベース接続を使用します。
そのため、puma.rb において、スレッドはデフォルトのまま 5 として pool と同じ値になるようにしています。(スレッド数 = pool 数)

max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }

さて、接続を使用した後、自動的に解放されますが、使用可能な数を超える接続を使用しようとすると、Active Record によってブロックされ、残りのリクエストはプールの接続の待ち状態になってしまいます。

動作確認

接続待ちの状態になることを実際に見てみたいと思います。
Promise.all() を用いて 10 リクエストを並列実行されるようにしてみます。

Promise.all(
    [...new Array(10)].map((_, i) => {
        api.get(`http://localhost:3000/home/${index + 1}`)
    })
)

Rails側では、リクエストを受け付け、DB処理を行います。
わかりやすいように、処理時間を5秒ほど伸ばします。

class HomeController < ApplicationController
    def index
        @user = User.find params[:id]
        sleep 5
    end
end

先ほど並列実行数(pool)として 5 を設定しているので、10 リクエストのうち残りの 5 つのリクエストが待機されるはずです。

同時接続数 5 の 10 リクエスト実行結果

画像から最後の 5 リクエストは遅れて返ってきていることがわかります。

次に、pool 数を変更してみます。変更されれば単純に実行時間にも影響が出てきます。
pool 数を 3 にしてみます。

default: &default
  adapter: mysql2
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 3 } %>

同時接続数 3 の 10 リクエスト実行結果
10リクエストの中で 3 ずつ並列処理されていることがわかります。

なお、処理時間が長いなどで接続を取得できない場合は、アプリケーションからの接続がタイムアウトになり、例外が発生する可能性があります。

could not obtain a connection from the pool within 5.000 seconds (waited 5.001 seconds); all pooled connections were in use excluded from capture: No host specified, no public_key specified, no project_id specified
Completed 500 Internal Server Error in 5017ms (Views: 5.4ms | ActiveRecord: 0.0ms | Allocations: 158211)

Puma の Worker 数を変更する

Worker 数 ≒ (子)プロセス数と捉え、スレッド数 = pool 数 は 3 でそのままとし、その状態で Worker 数を変更してみます。
puma.rb でコメントアウトされていた以下の記述をアンコメントし有効化します。

workers ENV.fetch("WEB_CONCURRENCY") { 2 }

すると 同時並列数は 3(スレッド数) × 2(worker数) = 6 に増えていることがわかります!

同時接続数 3 の 10 リクエスト実行結果( 2 worker)

参考URL

railsguides.jp

devcenter.heroku.com

qiita.com