[Rails]リードレプリカ使う

2023-02-12

rails

RDBを書込と読込のインスタンスに分けてGETリクエストは読込専用で処理。そんなことがしたいメモ。

環境

Windows11 Pro
WSL2(Ubuntu 20.04.5)
DockerDesktop
$ dc exec app ruby -v
ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]
$ dc exec app rails -v
Rails 7.0.4.2
$ mysql --version
mysql Ver 8.0.32

手順

書込と読込のdb接続情報(role)を分ける
読込専用ユーザー作る(開発環境用)
ロールの自動切り替えを有効にする
必要ならResolver作る
動作確認

書込と読込のdb接続情報(role)を分ける

config/database.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
development:
  primary:
    <<: *default
    database: read_replica_development
    username: ... # writable_user
    password: ...
    host: xxx # ライターインスタンスを指定
  readonly:
    <<: *default
    database: read_replica_development
    username: ... # readonly_user
    password: ...
    host: yyy     # リーダーインスタンスを指定
    replica: true # dbタスクを実行させない(migrationとか)

application_record

1
2
3
4
5
class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class

  connects_to database: { writing: :primary, reading: :readonly } # ロールを分けた
end

読込専用ユーザー作る(開発環境用)

わざわざレプリケーションせんでもreadonlyユーザー作ればコト足りると思いますので。

1
2
CREATE USER 'readonly_user'@'%' IDENTIFIED BY 'password';
GRANT SELECT, PROCESS ON *.* TO 'readonly_user'@'%';

上のクエリをmysqlコンテナの /docker-entrypoint-initdb.d 以下にマウントする。
mysql 8未満だと GRANT SELECT, PROCESS ON *.* TO 'readonly_user'@'%' IDENTIFIED BY 'password'; でよいハズ。

ロールの自動切り替えを有効にする

公式ドキュメントの通りに bin/rails g active_record:multi_db を実行
以下をコメントアウト

1
2
3
4
5
Rails.application.configure do
  config.active_record.database_selector = { delay: 2.seconds }
  config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
  config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
end

何かしらwriteした人が2秒以内にreadしにきたらprimaryに向けるということ。
GETとHEADはreadonly、ソレ以外はprimaryに向かう。
その実体は ActiveRecord::Middleware::DatabaseSelector

「何かしらwriteした人」を解決するためにsessionを使うということ。
sessionやcookieの代わりにredisやelasticacheなど使う場合はresolverを自作しよう。

必要ならResolver作る

たとえば今回api_onlyだったためそのままだとsessionが使えませんでした。
なので以下のようにしてmiddlewareを追加します。

config/application.rb

1
2
  config.middleware.insert_after Rack::Head, ActionDispatch::Cookies
  config.middleware.insert_after ActionDispatch::Cookies, ActionDispatch::Session::CookieStore

これでcookieをsessionストアとしてResolverが振り分けてくれます。

最初 config.middleware.use としてresolverがsession使えなくて「なんで!?」ってなってたけど
この記事でinsert_afterなど順番を操作できることを知りました。感謝!
https://tech.drecom.co.jp/ac2021-rails-api-only-setup-session-store-redis/

動作確認

GETとPOSTのroleを確認してみます

controller

delayのあたりはActiveRecord::Middleware::DatabaseSelector::Resolverをパクりました

1
2
3
4
5
6
7
8
9
10
11
def index
  timestamp = session[:last_write]
  delay = timestamp ? (Time.zone.now - (Time.at(timestamp / 1000, (timestamp % 1000) * 1000))).round(3) : 0

  render json: { role: ActiveRecord::Base.current_role, count: Dog.count, delay: }
end

def create
  @dogs =  Dog.create(dog_params)
  render json: { role: ActiveRecord::Base.current_role }
end

callしてみる

1
2
3
4
5
6
7
8
9
10
11
$ curl GET -c cookie.txt -b cookie.txt http://localhost:3000/dogs
{"role":"reading","count":1,"delay":0}

$ curl POST -c cookie.txt -b cookie.txt http://localhost:3000/dogs -d "dog[name]=柴犬"
{"role":"writing"}

$ curl GET -c cookie.txt -b cookie.txt http://localhost:3000/dogs
{"role":"writing","count":2,"delay":1.5}

$ curl GET -c cookie.txt -b cookie.txt http://localhost:3000/dogs
{"role":"reading","count":2,"delay":3.512}

readはreplica
writeはprimary
write直後のreadはprimary
writeして2秒経過後のreadはreplica
目論見通り! めでたい!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
> SELECT * FROM INFORMATION_SCHEMA.PROCESSLIST ORDER BY id DESC LIMIT 10\G
*************************** 1. row ***************************
     ID: 20
   USER: root
   HOST: 172.30.0.4:59166
     DB: read_replica_development
COMMAND: Sleep
   TIME: 1
  STATE:
   INFO: NULL
*************************** 2. row ***************************
     ID: 19
   USER: readonly_user
   HOST: 172.30.0.4:34962
     DB: read_replica_development
COMMAND: Sleep
   TIME: 35
  STATE:
   INFO: NULL
...

念のためreadonly_userからアクセスされてることも確認

コメント

投稿する

投稿したコメントはご自身で削除できません

不適切なコメントと判断した場合は管理側で削除することがあります