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作る
動作確認
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を自作しよう。
たとえば今回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からアクセスされてることも確認
コメント