ActiveRecord による Ruby での並列性とデータベース接続
最終更新日 2024年04月26日(金)
Table of Contents
Puma などのマルチスレッド Web サーバーまたは Unicorn などのマルチプロセス Web サーバーを使用して並列性を向上させる場合は、アプリがデータベースに対して保持する接続の数や、データベースが受け付けることができる接続の数に注意する必要があります。各スレッドまたはプロセスには、データベースへの異なる接続が必要です。これに対応するために、Active Record には、同時に複数の接続を保持できる接続プールが用意されています。
接続プール
デフォルトでは、Rails (Active Record) は、新しいスレッドまたはプロセスが SQL クエリを使用してデータベースにアクセスしようとした場合にのみ接続を作成します。Active Record では、アプリケーションあたりの接続の合計数がデータベース設定 pool
によって制限されます。これは、アプリがデータベースに対して保持できる接続の最大サイズです。データベース接続プールのデフォルトの最大サイズは 5 です。使用可能な数を超える接続を使用しようとすると、Active Record によってブロックされ、プールの接続の待ち状態になります。接続を取得できない場合は、タイムアウトエラーがスローされます。これは次のようになります。
ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5 seconds. The max pool size is currently 5; consider increasing it
このエラーを回避するには、接続設定をカスタマイズすることによって接続プールのサイズを手動で変更できます。その方法は似ていますが、接続設定の場所は、マルチスレッドまたはマルチプロセスのどちらの Web サーバーかによって異なる場合があります。
マルチスレッドサーバー
複数のスレッドで並列性を実現するサーバーの場合は、イニシャライザを使用してデータベースプールを設定することをお勧めします。Rails アプリケーションは起動すると、イニシャライザ内のコードを実行し、カスタマイズを使用して接続を確立します。
Rails 4.1+ の場合は、次の値を config/database.yml
で直接設定できます。
production:
url: <%= ENV["DATABASE_URL"] %>
pool: <%= ENV["DB_POOL"] || ENV['RAILS_MAX_THREADS'] || 5 %>
その他の古いバージョンの Rails を使用している場合は、イニシャライザを使用する必要があります。
# config/initializers/database_connection.rb
# Use config/database.yml method if you are using Rails 4.1+
Rails.application.config.after_initialize do
ActiveRecord::Base.connection_pool.disconnect!
ActiveSupport.on_load(:active_record) do
config = ActiveRecord::Base.configurations[Rails.env] ||
Rails.application.config.database_configuration[Rails.env]
config['pool'] = ENV['DB_POOL'] || ENV['RAILS_MAX_THREADS'] || 5
ActiveRecord::Base.establish_connection(config) # Establish connection is not needed for Rails 5.2+ https://github.com/rails/rails/pull/31241
end
end
すでにイニシャライザを使用している場合は、database.yml
方式へできるだけ早く切り替える必要があります。Unicorn や Puma (ハイブリッドモード) などのフォーキング Web サーバーを使用している場合、イニシャライザを使用するにはコードの複製が必要になります。イニシャライザ方式は、発生している内容に関する混乱が生じることがあり、サポートチケットが大量に消費される原因となります。
Puma Web サーバーを使用している場合は、pool
値を同等の ENV['RAILS_MAX_THREADS']
に設定することをお勧めします。複数のプロセスを使用している場合は、各プロセスに独自のプールが含まれるため、ワーカープロセスが ENV['RAILS_MAX_THREADS']
を超えない限りはこの設定で十分です。
マルチプロセスサーバー
Unicorn などのフォーキングサーバーの場合、マスタープロセスは Rails アプリケーションを起動 (さらに、イニシャライザをすべて実行) してからワーカーをフォークします。このため、マスタープロセスの before_fork
で接続を切断した後、after_fork
ブロックで接続を再確立することが必要です。
# config/unicorn.rb
before_fork do |server, worker|
# other settings
if defined?(ActiveRecord::Base)
ActiveRecord::Base.connection.disconnect!
end
end
after_fork do |server, worker|
# other settings
if defined?(ActiveRecord::Base)
ActiveRecord::Base.establish_connection # Establish connection is not needed for Rails 5.2+ https://github.com/rails/rails/pull/31241
end
end
Unicorn の場合、この接続設定は「Unicorn を使用した Rails アプリケーションのデプロイ」ガイドで説明されている通常の推奨される設定への追加になります。
Rails 4.1+ を使用している場合、ActiveRecord::Base.establish_connection
では、config/database.yml
に保存されている接続情報が使用されます。それ以外の場合は、一貫した接続情報を確保するために、イニシャライザでこの動作を複製する必要があります。
# config/unicorn.rb
# Use config/database.yml method if you are using Rails 4.1+
after_fork do |server, worker|
# other settings
if defined?(ActiveRecord::Base)
config = ActiveRecord::Base.configurations[Rails.env] ||
Rails.application.config.database_configuration[Rails.env]
config['pool'] = ENV['DB_POOL'] || 5
ActiveRecord::Base.establish_connection(config)
end
end
pool
を 5 つの接続、つまり DB_POOL
環境変数で指定された値に設定していることに注意してください。これで、Heroku で環境設定を設定することによって接続プールサイズを設定できます。たとえば、これを 10 に設定したい場合は、次のように実行します。
$ heroku config:set DB_POOL=10
これにより、各 dyno に 10 個の開かれた接続が割り当てられるわけではなく、新しい接続が必要な場合は、Rails プロセスあたり最大 10 個が使用されるまで新しい接続が作成されるようになるだけです。
プール内に十分な接続が存在する場合でも、データベースには、許可される接続の最大数がある可能性があります。
データベース接続の最大数
Heroku では、マネージド Postgres データベースが提供されます。階層型データベースごとにさまざまな接続制限があります。Essential 層データベースは 20 個の接続に制限されています。Standard 層以上のデータベースの制限数はそれ以上です。データベースは、アクティブな接続の最大数に達した後、新しい接続を受け付けなくなります。制限に達した結果、アプリケーションからの接続がタイムアウトになり、例外が発生する可能性があります。
スケールアウトするときは、アプリケーションに必要なアクティブな接続の数に注意することが重要です。各 dyno で 5 つのデータベース接続が許可されている場合は、より堅牢なデータベースのプロビジョニングが必要になるまでに、4 つの dyno にしかスケールアウトできません。
これで、接続プールを設定する方法や、データベースで処理できる接続の数を見つける方法がわかったので、各 dyno に必要な接続の適切な数を計算する必要があります。
必要な接続数の計算
アプリケーションコードでスレッドを手動で作成していない場合は、Web サーバーの設定を使用して、必要な接続の数を導き出すことができます。Unicorn Web サーバーは、複数のプロセスを使用してスケールアウトします。アプリケーションで新しいスレッドを開いていない場合は、各プロセスが 1 つの接続を受け取ります。そのため、Unicorn の設定ファイルで worker_processes
が 3
に設定されている場合は、次のようになります。
worker_processes 3
これにより、アプリはワーカーに 3 つの接続を使用します。つまり、各 dyno には 3 つの接続が必要になります。"Dev" プランを使用している場合は、6 つの dyno にスケールアウトできます。これは、アクティブなデータベース接続の数が最大 20 個のうちの 18 個になることを示します。ただし、一部の接続が不適切な状態または不明な状態になる可能性があります。このため、アプリケーションの pool
を 1
または 2
のどちらかに設定して、ゾンビ接続がデータベースを飽和させないようにすることをお勧めします。後述の「不適切な接続」のセクションを参照してください。
別の Web サーバーである Puma は、複数 (デフォルトでは 16) のスレッドを使用して並列性を取得します。つまり、例外なしで操作するには、プール内に 16 個の接続が必要になります。dyno がこれらの 16 個のスレッドのすべてを十分には活用していない可能性があるため、チューニングによって最適な数を見つけ、それを Procfile
で指定することができます。Puma で 5 つのスレッド、つまり最大 5 つの接続のみを使用するようにしたい場合は、次のように最大 5 つのスレッド 0:5
を使用するように設定できます。
web: bundle exec puma -t 0:5 -p $PORT -e ${RACK_ENV:-development}
どのアプリケーションにもそれぞれ異なるパフォーマンス特性や異なる要件があります。アプリのスレッドの数を適切にチューニングするには、本番同様の環境またはステージング環境でアプリの負荷をテストする必要があります。
アクティブな接続の数
開発環境では、データベースをチェックすることにより、アプリケーションによって取得される接続の数を確認できます。
$ bundle exec rails dbconsole
これにより、開発データベースへの接続が開かれます。その後、次を実行して Postgres データベースへの接続の数を確認できます。
select count(*) from pg_stat_activity where pid <> pg_backend_pid() and usename = current_user;
これにより、そのデータベース上の接続の数が返されます。
count
-------
5
(1 row)
接続は低速で開かれるため、localhost
にある実行中のアプリケーションを、カウントが増えなくなるまで複数回ヒットする必要があります。開発セットアップでは、アプリによる新しい接続の作成に必要な負荷を生成できない可能性があるため、正確なカウントを得るには、そのデータベースクエリを本番データベースの内部で実行する必要があります。
バックグラウンドワーカー
worker
プロセスタイプを使用し、Sidekiq などのバックグラウンドワーカーライブラリを使用している場合は、dyno タイプごとに設定を変えることが必要になる場合があります。デフォルトでは、Sidekiq は 10 個のスレッドを使用します。つまり、ワーカー上のデータベースを 10 を超えるスレッド数にするか、または Sidekiq プロセスで使用されるスレッド数が少なくなるように設定する必要があります。
Rails アプリが前のセクションから設定されている場合は、RAILS_MAX_THREADS
を別の値に設定できます。次に例を示します。
worker: RAILS_MAX_THREADS=${SIDEKIQ_RAILS_MAX_THREADS:-10} bundle exec sidekiq
この例では、heroku config:set SIDEKIQ_RAILS_MAX_THREADS=5
を実行する必要があります。
代わりに、Sidekiq での接続の数を変更したい場合は、Sidekiq を -c
フラグで起動し、SIDEKIQ_CONCURRENCY
などの別の環境設定を使用できます。
worker: bundle exec sidekiq -c ${SIDEKIQ_CONCURRENCY:-5}
この例では、バックグラウンドジョブを処理するために 5 つのスレッドのみを使用するよう Sidekiq に指示しています。
不適切な接続
接続がハングアップしたり、"不適切な" 状態になったりすることがあります。つまり、接続は使用できなくなるが、開かれたままになります。Unicorn などのマルチプロセス Web サーバーを実行している場合は、通常は 3 つのデータベース接続を消費する 3 Worker dyno が、時間の経過と共に 15 個もの接続 (プールあたり 5 つのデフォルト接続× 3 つの Worker) を保持するようになる可能性があります。この脅威を制限するには、接続プールを 1
または 2
に減らし、Rails 4 で使用可能な接続リーピングを有効にします。ただし、この機能は、このバグレポートの後にデフォルトで無効になりました。
'reaping_frequency'
は、Active Record に、接続がハングアップまたは停止しているかどうかを N 秒おきに確認し、それを終了するよう指示できます。時間の経過と共にアプリケーションでいくつかのハングアップした接続が発生する可能性はありますが、コード内のどこかの部分がハングアップした接続の原因になっている場合、リーパーはこの問題に対する永続的な解決になりません。
PgBouncer を使用した接続の制限
データベース接続の制限に達するまで、追加の dyno を使用して引き続きアプリケーションをスケールアウトできます。この時点に達する前に、PgBouncer buildpack を使用して、各 dyno に必要な接続の数を制限することをお勧めします。
PgBouncer では、データベーストランザクションで共有される接続のプールが保持されます。これにより、Postgres への接続 (通常は開いており、アイドル状態) が最小限に維持されます。ただし、トランザクションプーリングでは、名前付きプリペアドステートメント、セッションアドバイザリロック、リッスン/通知、またはセッションレベルで操作するその他の機能を使用できなくなります。詳細は、「PgBouncer buildpack FAQ for full list of limitations」(制限の完全なリストに関する PgBouncer buildpack の FAQ) を参照してください。
多くのフレームワークでは、PgBouncer を使用するためにプリペアドステートメントを無効にする必要があります。その後、PgBouncer buildpack をアプリに追加します。
プリペアドステートメントを無効にするか、またはフレームワークでそれが使用されていないことを確認する前に続行しないでください。Rails 3+ ではプリペアドステートメントが使用されます。
$ heroku buildpacks:add heroku/pgbouncer
第一言語の buildpack の記述があることも確認してください。
$ heroku buildpacks
1. heroku/ruby
2. heroku/pgbouncer
Ruby で想定されるものとは異なる言語を使用している場合、1 行目は異なります。
ここで、PgBouncer を起動するように Procfile
を変更する必要があります。Procfile
で、コマンド bin/start-pgbouncer-stunnel
を web
エントリの先頭に追加します。そのため、Procfile
が
web: bundle exec puma -C config/puma.rb
であった場合は、次のようになります。
web: bin/start-pgbouncer-stunnel bundle exec puma -C config/puma.rb
結果を Git にコミットし、ステージングアプリでテストした後、本番環境にデプロイします。
デプロイ時、出力には次の内容が表示されます。
=====> Detected Framework: pgbouncer-stunnel