この記事は Ruby on Rails アドベントカレンダーの5日目です。
RailsでDBのshardingをする話
負荷分散のためにRailsでDBのシャーディングをしたいときに、いくつか利用できるGemがあります。
例を挙げると、
などなど…そこそこあるのですが、この中でこれ書いている現在Rails 5 readyなのがactiverecord-shardingとswitch_pointだけのようでした。
で、自分たちはOctopusを使ってしまっていた
Rails 4.2を利用していて、Octopus使っていて、からのー、Rails 5にアップグレード、うっ、頭が…
OctopusはActiveRecordの標準の機能を拡張してシャーディングの機能を提供してくれるところが良いところなのですが、裏を返せばActiveRecordの実装に手を入れるということなのでRailsのバージョンアップコストがとても高いというデメリットがあります。
そこそこコードが成長してしまっていたので、今から他のGemに乗り換えるのは難しそうでした。(activerecord-shardingなどは、独自メソッドやDSLをActiveRecordに生やすように実装されているので、ActiveRecordのもとの実装に影響されない。かわりに、コードのマイグレーションが必要だった)
ぶっちゃけやりたいのはシャーディングだけで、そんなに多機能なGemじゃなくてよかった
Rails 5 readyなoctopusが欲しい…と思っていたところに本家issueにこんなコメントが
もうOctopusメンテされないらしい…チャンス…!?
※と…思っていたのだけれど、Gem作ってからこの記事書いてる間にこんなコメントが
ああっ…台無し…!? でも、ここまできたので続けていきます
Octopusはもうダメかもしれない …Tako作ろ…
ということで、OctopusのクローンのTakoを作ってみました。
MITライセンスで公開しています。
Takoで実現したかったことの要件をまとめると以下のような感じです
- Octopusの#usingメソッドのように、与えられたブロック内またはそのメソッドチェイン内でシャードを指定してSQLを発行したい。
- この際なのでActiveRecordの実装への依存を可能な限り減らしたい。
- Rails標準のマイグレーションも可能な限り利用できるようにしたい。
- 完全水平分割だけでいいのでシャード指定のマイグレーションとか要らない
- Railsのバージョンアップがあったら、可能な限り早く対応したい。
- Railsの対応バージョンは
<= 4.0
で5系対応。 3系はもう良いかなって…
使い方
インストール
gem 'tako'
bundle install
or
gem install tako
で利用できます
DBのセットアップ
config/shards.yml
を作成します。読み込む際に、ERBで一度パースされます。
config/shards.yml
の内容はTako.config
で取得することができます。
default: &default
adapter: mysql2
encoding: utf8
charset: utf8
collation: utf8_general_ci
reconnect: false
username: <%= ENV['MYSQL_USER_NAME'] %>
password: <%= ENV['MYSQL_ROOT_PASSWORD'] %>
host: <%= ENV['MYSQL_HOST'] %>
port: <%= ENV['MYSQL_PORT'] %>
database: tako_<%= Rails.env %>
tako:
<%= Rails.env %>:
shard1:
<<: *default
host: <%= ENV['MYSQL_SHARD1_HOST'] %>
database: tako_<%= Rails.env %>_shard1
shard2:
<<: *default
host: <%= ENV['MYSQL_SHARD2_HOST'] %>
database: tako_<%= Rails.env %>_shard1
以下のように rake db:tako:<command>
でマイグレーションを実行できます。
$ bundle exec rake db:tako:create
$ bundle exec rake db:tako:migrate
shards.ymlから全シャード情報を読み込み、それぞれに対してdb:migrateを掛けるようになっています。
各shard間でマイグレーションのバージョンが違っていてもいい感じで処理してくれます。
アプリ内での利用
Octopus.using
に相当するメソッドが、Tako.shard
になります。
usingにしなかった理由は、Module#using
と名前が重複するからです。
User.shard(:slave_one).where(:name => "Thiago").limit(3)
# => :slave_oneでクエリ実行
User.create(name: "Bob")
# => database.ymlに記載されているデフォルトのDBでクエリ実行
Tako.shard(:slave_two) do
User.create(name: "Mike")
# => :slave_twoでクエリ実行
end
User.shard(:slave_two) do
User.shard(:slave_one).create(name: "Mike")
# => :slave_oneでクエリ実行
end
アソシエーションでの対応
要するに、そのActiveRecord::Baseのインスタンスが作成されたスコープにおけるシャードが、保存先のシャードになる実装になっています。
user = User.shard(:shard01).create(name: "Jerry")
user.logs.create
# => この場合は、userと同じシャードである:shard01でlogが作成される
user.logs << Log.shard(:shard02).new
# => ただし、この場合は:shard02
life = user.build_life
life.save!
# => buildしてsaveの場合は、親?と同じ:shard01になる。
全シャードでの実行
Tako.with_all_shards(&block)
で、ブロック内のコードが全シャードで実行されます
# 全シャードでMikeさんがお見えになる
Tako.with_all_shards do
User.create(name: "Mike")
end
Octopusからマイグレーションしたい場合
まずはじめに、ありがとうございます!
以下の手順でマイグレーションできます。
migration等でOctopusの機能を利用している場合は、避けていただくか、TakoにPRを送ってください。
-
Octopus.using
またはModel.using
をTako.shard
またはModel.shard
に置換します -
config/shards.yml
をTako用に書き換えます
実装で苦労した所/妥協した所
prependが使えない
他にActiveRecordを拡張するgemを利用していた場合、例えばsave!メソッドをalias_method
またはalias
するようなgemが入っていると、prependでsuper
を呼び出そうとして、stack level too deep
になってしまいます。なので、今回Takoの実装には同じくalias_methodが利用されています…
ActiveRecord::Migratorが結構魔界
Railsのソース読む上で一番苦労したのがMigratorのコードを追うところでした。
Rails内部から利用される想定で書いてあるので、外からの呼び出しが辛かった覚えがあります。
ConnectionPoolが利用できない
Thread使うことって、個人的にはあんまりないかなって思ったので、実装が楽な方を取りました。
Takoでは、connection_poolは利用できません。
まとめ
以上、シャーディングに関するつらみとTakoの紹介でした。