Amazon RDSのリードレプリカ+Rails4.0+multi_dbを試してみました

Webで負荷分散する場合、アプリケーションは簡単にスケールアウト出来ますが、DB(がRDBMSの場合)は1つに集約するパターンが多いと思います。
しかしこの構成だとどうしてもDBが性能面でのボトルネックとなりがちで、それを解消する1つのスケーリングテクニックとしてReadをレプリカへ分散させるというものがあります。
今回はそれをAmazon RDSのリードレプリカとmulti_dbというgemを使って試してみました。

すでにDBはRDS(MySQL)を利用しているがリードレプリカは利用していない、ということをスタート地点としています。

Amazon RDSのリードレプリカ

Amazon Web Services ブログ: Amazon RDSの新機能:Read Replica(リードレプリカ)の発表

リードレプリカとはその名の通り、RDSの読取り専用複製インスタンスです。
RDSマスタから定期的にデータ同期され、Readはレプリカを参照することによってボトルネックになりがちなDBの負荷を軽減することができます。
リードレプリカを導入した構成はこんなイメージになります。(今回レプリカは1つで検証しました。)

リードレプリカの作成はAWS Management Consoleから簡単に行えます。AWS にログインして[AWS Management Console]-->[Services]-->[RDS]-->[Instances] といってRDSマスタのインスタンスを右クリック、「Create Read Replica」を選択します。

その後、インスタスタイプやらAZやらを選択して「Yes, Create Read Replica」をクリックするとリードレプリカが作成されます。作成が完了するとInstancesの一覧にレプリカのインスタンスが追加されます。(今回はmulti-dbがマスタ、multi-db-readがレプリカとなっています)

たったこれだけでMySQLレプリケーションの設定も必要なくレプリカが作成されました。(RDSすごい!)

multi_dbの導入

GitHub - schoefmann/multi_db: Connection proxy for ActiveRecord for single master / multiple slave database deployments

multi_dbとはReadクエリ(SELECT)をslaveへ、Writeクエリ(INSERT/UPDATE/DELETE)をmasterへ振り分けてくれるgemで主にRailsで用いられます。レプリカのインスタンスが全滅していた場合は自然にmasterに繋がるようになっています。
導入はとても簡単です。

(1)Gemfileに以下を追記します。

gem 'multi_db'

(2)bundle install後、config/database.yml と config/environments/.rb を編集します。

●config/database.yml
データベース定義にリードレプリカを追加します。リードレプリカは「_slave_database」のように定義します。(今回はdevelopmentで実施)

development_slave_database:
  adapter: mysql2
  encoding: utf8
  database: multi_db_development_read
  host: yyyyyyyy.ap-northeast-1.rds.amazonaws.com
  username: root
  password: replica_password

●config/environments/.rb
multi_dbを利用するためのコードを追記します。(今回はdevelopment.rbに追記)

  config.after_initialize do
    MultiDb::ConnectionProxy.setup!
  end

さあコレでOK、と思って rails console を実行したらエラー。

prompt$ rails console
/versions/2.0.0-p195/lib/ruby/gems/2.0.0/gems/multi_db-0.3.1/lib/multi_db/connection_proxy.rb:57:in `setup!': uninitialized constant ActiveRecord::Observer (NameError)

どうやらActiveRecord::Observer が初期化されていないのが原因のようですが・・・なんでだろう?と悩んでいたらこんなページを発見。

GitHub - rails/rails-observers: Rails observer (removed from core in Rails 4.0)

Rails Observers (removed from core in Rails 4.0)

どうやらRails 4.0ではObserversは削除されたみたいでした。

合わせてリリースノートRuby on Rails 4.0 Release Notes — Ruby on Rails Guides を見てると

In Rails 4.0, several features have been extracted into gems. You can simply add the extracted gems to your Gemfile to bring the functionality back. 
Active Record Observers (GitHub, Commit)

とあったのでGemfileにobserversを追加。

gem "rails-observers"

再び rails console を実行したところうまくいきました。

prompt$ rails console
Loading development environment (Rails 4.0.0)
irb(main):001:0> 

リードレプリカとmulti_dbの動作検証

ここまでで無事起動出来たので本題の「ちゃんと振り分けが出来ているのか?」ということを検証してみました。ここではnameという属性だけを持つUserというmodelを利用しています。

prompt$ rails console
Loading development environment (Rails 4.0.0)
irb(main):001:0> users = User.all
[MULTIDB] hijacking connection for User
  User Load (0.4ms)  SELECT `users`.* FROM `users`
=> #<ActiveRecord::Relation [#<User id: 1, name: "悟空">, #<User id: 2, name: "天津飯">, #<User id: 3, name: "カリン様">]>
irb(main):002:0> 

rails consoleにてReadクエリ(SELECT)を発行したところ、リードレプリカの方へ振り分けられました。ポイントは

[MULTIDB] hijacking connection for User

の部分で、multi_dbがコネクションをハイジャックしたことが分かります。

irb(main):002:0> User.create(name: "クリリン")
   (0.3ms)  BEGIN
  SQL (0.5ms)  INSERT INTO `users` (`name`) VALUES ('クリリン')
   (0.4ms)  COMMIT
=> #<User id: 4, name: "クリリン">
irb(main):003:0> 

次にWriteクエリ(INSERT)を発行してみると先ほどのmulti_dbのログは出力されておらず、マスタの方へ振り分けられていました。

もう少しきめ細かい制御を

これで終わってしまったら面白くないので、もう少し突っ込んでみたいと思います。
リードレプリカはマスターが更新されてから同期されるため、少なからず遅延があります。
そのため、例えば

  1. データの差分を与えられる(「身長が5cm伸びました」)
  2. 既存データをSELECTして取得(「現在の身長は170cmです」)
  3. 取得したデータに差分を付加し、UPDATEする(「170+5=175cm」としてUPDATEする)

といったようなユースケースが考えられる場合、SELECTがレプリカ、UPDATEがマスタでは完全な整合性が保たれないことがあります。
そういった場合はSELECTもマスタを向けたいものです。で、multi_dbで出来るのかと調べてたらバッチリその機構が用意されていました。(GitHub - schoefmann/multi_db: Connection proxy for ActiveRecord for single master / multiple slave database deployments の「Forcing the master for certain actions」「Forcing the master for certain models」を参照)


(1)Model単位でマスタDBへ向ける

config/environments/.rb を編集します。

Modelを1つだけ指定(User)

  config.after_initialize do
    MultiDb::ConnectionProxy.master_models = ['User']
    MultiDb::ConnectionProxy.setup!
  end

Modelを複数指定(User、Company)

  config.after_initialize do
    MultiDb::ConnectionProxy.master_models = ['User', 'Company']
    MultiDb::ConnectionProxy.setup!
  end

(2)Controllerのアクション単位でマスタDBへ向ける

各controller.rbを編集します。

アクションを1つだけ指定(index)

  around_filter(:only => :index) { |c,a| ActiveRecord::Base.connection_proxy.with_master { a.call } }

アクションを複数指定(index、show)

  around_filter(:only => [:index, :show]) { |c,a| ActiveRecord::Base.connection_proxy.with_master { a.call } }

これで特定のmodel、特定のアクションでリードレプリカを利用しない指定が出来ます。

まとめ

今回はAmazon RDSのリードレプリカとそれをRailsから利用するためのmulti_dbについて書いてみました。
multi_dbの各modelやcontrollerのアクション毎にslaveへ向ける/向けないという設定が簡単に出来るところがなかなかイケてるなぁと思いました。

昔私は同じようなことを「素のMySQL + Spring&Hibernate」で実現したことがあるのですが、それに比べるとめっちゃラクでしたね。(素のMySQLだと当然自分でレプリケーション設定をしなくちゃいけないし、Spring&Hibernateでmulti_db並みのきめ細かい制御をしようとするとモンキーパッチ的なことを実施しなくちゃならなかったり・・・)
こういったことをサクッと実践させてくれるRDSとmulti_dbに感謝しながら今日はおしまいとしたいと思います。