Makaraを選んだ経緯
DBアクセスのRead/Writeを分けるGemとしてswitch_pointが有名です。しかし、既存コードを変更しなければならず、導入コストの観点から、私たちのプロジェクトでは使えませんでした。
他に有名なものとてOctopusがあります。Partial Replicationの設定をすることでデフォルトの接続先をマスターに向けることができ、既存コード変更なしで導入できます。使い方もModelにusing
を付けるだけと快適です。ただ、2点問題がありました。
-
rake db:setup
でコケる。どうやらdb:create
とdb:schema:load
を同時に行うと上手く動作しない -
rake db:migrate
とrake db:rollback
でレプリカにar_internal_metadata
とschema_migrations
のテーブルが作成される
2点目の、レプリカなのに書き込まれてしまう点が致命的で、私たちのプロジェクトでは使えませんでした。
困っていた折、Scaling Readsという記事に出会い救われました。Makaraの使い方を共有します。
Makaraとは
開発元ブログより
MakaraはRead/Writeを分けるActive Recordのアダプタだ
こんな機能があります。
- 複数DBへのRead/Write分離
- スレーブ障害時のフェイルオーバー
- スレーブ接続断時の自動再接続
- マスターないしスレーブへの接続を分離しないオプション(sticky)
- Mysqlやpostgresに対応
- Middlewareを提供 for releasing stuck connections
- スレーブDBへの荷重接続(Weighted connection pooling)
以前はOctopus使ってた。エラーハンドリングやログ出力がイケてないから改善したぜ。
また、マスターの変更がすぐにレプリカに反映されないとき'RecordNotFound'エラーが出てしまう。マスターに書き込んだら読み込みもマスターからの方が安全なんだ。だから俺たちは'sticky'という概念を導入したぜ。
使い方
distribute_reads
ブロックで囲むとレプリカへ接続されます。囲わないとマスターへ接続されます。
distribute_reads { User.last } # replica
User.first # primary
Makara公式のReadmeを読むと、本来の使い方は自分でProxyを作るようです。しかし、参考にしたブログではProxyクラスをprepend
で拡張しています。
distribute_reads
実行時に:distribute_reads
フラグをON。これがONならレプリカへ接続。OFFならマスターへ接続。ということをやっています。
注意すべきは、トランザクションを貼ると強制的にマスター接続になる点です。
/makara/blob/master/lib/makara/proxy.rb
def _appropriate_pool(method_name, args)
...
elsif in_transaction?
@master_pool
# yay! use a slave
else
@slave_pool
end
end
導入方法
GemfileにMakaraを追加
gem 'makara', '~> 0.4'
config/database.yml
をMakara設定に変更。マスターとレプリカの設定を記載。
production:
adapter: 'mysql2_makara'
database: 'MyAppProduction'
makara:
blacklist_duration: 5
master_ttl: 5
master_strategy: round_robin
sticky: true
connections:
- role: master
name: primary
host: master.sql.host
- role: slave
name: replica
host: slave.sql.host
config/initializers/makara.rb
を新規作成。Makara::Context.generate
はv0.4以降廃止されたので、テストケースを参考にseed
を手動作成。
Makara::Cache.store = :noop
module DefaultToPrimary
def _appropriate_pool(*args)
return @master_pool unless Thread.current[:distribute_reads]
super
end
end
Makara::Proxy.send :prepend, DefaultToPrimary
module DistributeReads
def distribute_reads
previous_value = Thread.current[:distribute_reads]
begin
Thread.current[:distribute_reads] = true
seed = Time.now.to_f
Makara::Context.set_current(mysql: seed)
yield
ensure
Thread.current[:distribute_reads] = previous_value
end
end
end
Object.send :include, DistributeReads