Rails で RDS リードレプリカを活用した読み書き分離とスケールイン対応
概要
Rails アプリケーションで RDS のリードレプリカを活用し、重い SQL クエリをリードレプリカに流すことで、メインのデータベースの負荷を軽減する方法と、リードレプリカを安全にスケールインできるようにする。
1. データベース設定
まず、config/database.ymlで読み書き用のデータベース接続を分離します。
default: &default
adapter: mysql2
charset: utf8mb4
encoding: utf8mb4
collation: utf8mb4_general_ci
database: <%= ENV["DATABASE_NAME"] %>
username: <%= ENV["DATABASE_USERNAME"] %>
password: <%= ENV["DATABASE_PASSWORD"] %>
host: <%= ENV["DATABASE_HOST"] %>
port: <%= ENV["DATABASE_PORT"] %>
pool: <%= ENV["RAILS_MAX_THREADS"] || 5 %>
development:
primary:
<<: *default
database: app_development
primary_replica:
<<: *default
database: app_development
replica: true
production:
primary:
<<: *default
primary_replica:
<<: *default
host: <%= ENV["DATABASE_READER_HOST"] %>
replica: true
2. 読み書き分離モジュール
DbReadWriteSplitableモジュールを作成して、読み書きを分離する機能を提供します。
# app/models/concerns/db_read_write_splitable.rb
module DbReadWriteSplitable
extend ActiveSupport::Concern
class_methods do
def write(retry_count: 0, &block)
ActiveRecord::Base.connected_to(role: :writing, &block)
end
def readonly(retry_count: 0, &block)
ActiveRecord::Base.connected_to(role: :reading, prevent_writes: true, &block)
rescue ActiveRecord::StatementInvalid => e
# リーダーのスケールインによって接続エラーが発生した場合にコネクションをクリアして再リトライする
raise e if retry_count >= 5
if e.message =~ /MySQL server has gone away/i
ActiveRecord::Base.clear_active_connections!
readonly(retry_count: retry_count + 1, &block)
else
raise e
end
end
end
def write(...)
self.class.write(...)
end
def readonly(...)
self.class.readonly(...)
end
end
3. ApplicationRecord での利用
ApplicationRecordでこのモジュールを include し、データベース接続を設定します。
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
include DbReadWriteSplitable
self.abstract_class = true
connects_to database: { writing: :primary, reading: :primary_replica }
end
4. 使用方法
読み取り専用クエリ(リードレプリカ使用)
# 重い集計クエリをリードレプリカで実行
User.readonly do
User.joins(:orders)
.group(:id)
.select('users.*, COUNT(orders.id) as order_count')
.order('order_count DESC')
.limit(100)
end
# インスタンスメソッドからも利用可能
user.readonly do
user.orders.sum(:amount)
end
書き込みクエリ(プライマリ DB 使用)
# データの更新はプライマリDBで実行
User.write do
user.update!(name: 'New Name')
end
5. スケールイン対応の仕組み
問題の背景
AWS RDS のリードレプリカは負荷に応じて自動的にスケールイン/アウトします。スケールイン時に既存の接続が切断され、MySQL server has gone awayエラーが発生します。
解決方法
DbReadWriteSplitableモジュールでは以下の仕組みでスケールインに対応しています:
-
エラーハンドリング:
ActiveRecord::StatementInvalidをキャッチ -
エラー判定: エラーメッセージに
MySQL server has gone awayが含まれているかチェック -
接続クリア:
ActiveRecord::Base.clear_active_connections!で接続をクリア - 再試行: 最大 5 回まで自動的に再試行
rescue ActiveRecord::StatementInvalid => e
raise e if retry_count >= 5
if e.message =~ /MySQL server has gone away/i
ActiveRecord::Base.clear_active_connections!
readonly(retry_count: retry_count + 1, &block)
else
raise e
end
6. 環境変数の設定
本番環境では以下の環境変数を設定します:
# プライマリDB
DATABASE_HOST=your-primary-db-host
DATABASE_NAME=your_database_name
DATABASE_USERNAME=your_username
DATABASE_PASSWORD=your_password
# リードレプリカ
DATABASE_READER_HOST=your-reader-db-host
7. 注意点
データの整合性
- リードレプリカは非同期でデータを複製するため、最新のデータが反映されていない可能性があります
- リアルタイム性が重要な処理では、プライマリ DB を使用してください
接続プール
- 読み書きで別々の接続プールを使用するため、接続数の設定に注意してください
-
pool設定を適切に調整して、リソースの無駄遣いを避けてください
エラーハンドリング
- スケールイン時の再試行回数(デフォルト 5 回)は、アプリケーションの要件に応じて調整してください
- 他のデータベースエラーと区別するため、エラーメッセージの判定条件を適切に設定してください
まとめ
この実装により、以下のメリットが得られます:
- パフォーマンス向上: 重い読み取りクエリをリードレプリカに分散
- 可用性向上: スケールイン時の自動再接続でサービス継続
- 運用負荷軽減: 手動での接続管理が不要
RDS のリードレプリカを活用することで、データベースの負荷分散と可用性の向上を実現できます。