はじめに
ある日、運用しているRailsアプリケーションでアクセスが予想外に増えたことでデータベースが悲鳴を上げ、サービスが不安定になってしまいました。
コスト面の観点からなるべくスケールはしたくなかったので、Railsの機能を使ってデータベースアクセスの負荷を分散しました。
この記事では、その体験を踏まえつつ、Railsでのデータベース負荷分散の基本的な概念や実装方法について、Rails6以前とRails6以降の違いを含めてまとめていきたいと思います。
Railsでのデータベース負荷分散
Railsのデフォルト設定では、1つのプライマリデータベース(以下プライマリとします)に接続する仕組みになっていますが、そのままだとアクセスが増えたときにプライマリに負荷が集中し、レスポンスが遅くなる恐れがあります。
これを解決する方法として、プライマリと読み取り専用のレプリカを使ったプライマリ/レプリカ構成を採用するのが有効です。
これによってデータベースへのアクセスを効率よく分散させることができ、以下のメリットが得られます。
-
パフォーマンスの向上
処理速度が速くなり、レスポンスの遅延が減ることが期待できます。 -
可用性の向上
もしプライマリに障害が発生しても、読み取り処理が止まりにくく、システム全体の安定性が担保されます。
具体的な導入手順
Rails6以前と6以降とでアプローチが変わってきます。
Rails6以降ではActiveRecordがマルチデータベース構成をサポートしているため、標準機能を優先するのが一般的です。
一方で、Rails6以前のプロジェクトや、標準機能でカバーしづらい特殊な要件がある場合などは、switch_pointのようなGemの活用を検討すると良いかと思います。
Rails6以前:switch_pointによる負荷分散
switch_pointのGemを利用するといいでしょう。
switch_pointとは
処理によってプライマリとレプリカを切り替えられるRailsのGemで、これを使用することで、書き込み操作はプライマリに、読み取り操作はレプリカに振り分けることができます。
設定方法
1.Gemを追加します。
# gem 'switch_point'
2.switch_pointの設定を追加します。
SwitchPoint.configure do |config|
config.define_switch_point :default,
readonly: :"#{Rails.env}_read",
writable: :"#{Rails.env}_write"
end
3.database.ymlに接続情報を設定します。
default: &default
adapter: mysql2
username: <%= ENV['DATABASE_USERNAME'] %>
password: <%= ENV['DATABASE_PASSWORD'] %>
host: <%= ENV['DATABASE_HOST'] %>
database: <%= ENV['DATABASE_NAME'] %>
encoding: utf8
# Development環境
development:
<<: *default
development_read:
<<: *default
host: <%= ENV['DATABASE_READ_HOST_DEVELOPMENT'] %>
# Staging環境
staging:
<<: *default
host: <%= ENV['DATABASE_HOST_STAGING'] %>
staging_read:
<<: *default
host: <%= ENV['DATABASE_READ_HOST_STAGING'] %>
# Production環境
production:
<<: *default
production_read:
<<: *default
host: <%= ENV['DATABASE_READ_HOST_PRODUCTION'] %>
設定としてはこれでOKで、
例えばモデルでswitch_pointを利用する場合は、以下のように記述します。
class User < ApplicationRecord
use_switch_point :user
end
# 書き込み
User.use(:writable) do
User.create!(name: "test_user")
end
# 読み取り
User.use(:readonly) do
User.find_by(name: "test_user")
end
これで、書き込みはプライマリへ、読み取りはレプリカへ自動的に分散されます。
レプリカはプライマリとの同期遅延が発生する(レプリカでのデータ取得が数秒遅れたりする)可能性があるため、直前の書き込み結果を即座に取得するケースでは注意が必要です。
また、最新データが必要な場合も、レプリカでは情報が古くなることもあるためプライマリを利用する方が安全かもしれません。
コントローラーで全体的にindexメソッドのみを読み取り専用にする場合は以下のような記述します。
class ApplicationController < ActionController::Base
around_action :use_readonly_index
private
def use_readonly_index
if action_name == 'index'
# 読み取り専用DBで処理を実行
ActiveRecord::Base.use(:readonly) do
yield
end
else
yield
end
end
end
indexアクション以外の条件分岐が不要な場合は
class ApplicationController < ActionController::Base
around_action :use_readonly_index, only: :index
private
def use_readonly_index
ActiveRecord::Base.use(:readonly) do
yield
end
end
end
でもいいですね。
switch_pointで接続を切り替えた時に接続がプールに保持されることで意図しないプライマリやレプリカへの接続が残ってしまい、次のデータベース操作に影響を与える可能性があります。
その場合は、必要に応じてclear_active_connections!を呼び出して、使い終わった接続をプールから明示的にクリアすることで、意図しない接続が残らないようにすることができます。
・ApplicationControllerでクリアする例
class ApplicationController < ActionController::Base
around_action :use_readonly_index
private
def use_readonly_index
if action_name == 'index'
# 読み取り専用DBで処理を実行
ActiveRecord::Base.use(:readonly) do
yield
end
else
yield
end
ensure
# 処理後に使い終わった接続を明示的にクリア
ActiveRecord::Base.clear_active_connections!
end
end
Rails6以降:ActiveRecordのマルチデータベース機能
Rails6以降では、ActiveRecordがマルチデータベース構成をデフォルトでサポートしています。
この機能を活用することで、外部Gemを利用せずにデータベース負荷分散が可能になります。
設定方法
- データベース設定
default: &default
adapter: mysql2
username: <%= ENV['DATABASE_USERNAME'] %>
password: <%= ENV['DATABASE_PASSWORD'] %>
encoding: utf8
# Development環境
development:
primary:
<<: *default
host: <%= ENV['DATABASE_HOST_DEVELOPMENT'] %>
database: <%= ENV['DATABASE_NAME_DEVELOPMENT'] %>
replica:
<<: *default
host: <%= ENV['DATABASE_READ_HOST_DEVELOPMENT'] %>
database: <%= ENV['DATABASE_NAME_DEVELOPMENT'] %>
# Staging環境
staging:
primary:
<<: *default
host: <%= ENV['DATABASE_HOST_STAGING'] %>
database: <%= ENV['DATABASE_NAME_STAGING'] %>
replica:
<<: *default
host: <%= ENV['DATABASE_READ_HOST_STAGING'] %>
database: <%= ENV['DATABASE_NAME_STAGING'] %>
# Production環境
production:
primary:
<<: *default
host: <%= ENV['DATABASE_HOST_PRODUCTION'] %>
database: <%= ENV['DATABASE_NAME_PRODUCTION'] %>
replica:
<<: *default
host: <%= ENV['DATABASE_READ_HOST_PRODUCTION'] %>
database: <%= ENV['DATABASE_NAME_PRODUCTION'] %>
ActiveRecordのconnected_toメソッドを使って接続を切り替えます。
# 書き込み
Article.connected_to(role: :writing) do
Article.create!(title: "New Article")
end
# 読み取り
Article.connected_to(role: :reading) do
Article.find_by(title: "New Article")
end
Amazon Auroraの注意点
Amazon Auroraを使っている場合ですが、ライター(プライマリ)インスタンスは1つで稼働していてスケールアウトができないため、どうしても負荷が集中しがちです。
ですが、リーダー(レプリカ)インスタンスはスケールアウトできるので、リーダーの数を増やしたりして読み取りの負荷をうまく分散できます。
Aurora×Railsでデータベースの負荷分散を考えるときは、ライターインスタンスにかかる負担を減らして、リーダーインスタンスをうまく使うのがポイントです。
さいごに
データベース負荷分散は、Railsアプリケーションのパフォーマンスと安定性を向上させる上で非常に有効です。
特にアクセスが増加しているアプリケーションや、読み取り操作が多いシステムには効果的かと思います。
Railsで今後負荷分散を実施する場合のご参考になれば幸いです。