"Octopus"のクローン"Tako"を作ってRails5でshardingする話

この記事は [Ruby on Rails](http://qiita.com/advent-calendar/2016/ruby_on_rails) アドベントカレンダーの5日目です。


# RailsでDBのshardingをする話

負荷分散のためにRailsでDBのシャーディングをしたいときに、いくつか利用できるGemがあります。
例を挙げると、

 * [activerecord-sharding](https://github.com/hirocaster/activerecord-sharding)
 * [db-charmer](https://github.com/kovyrin/db-charmer)
 * [Octopus](https://github.com/thiagopradi/octopus)
 * [switch_point](https://github.com/eagletmt/switch_point)

などなど…そこそこあるのですが、この中でこれ書いている現在Rails 5 readyなのが[activerecord-sharding](https://github.com/hirocaster/activerecord-sharding)と[switch_point](https://github.com/eagletmt/switch_point)だけのようでした。


# で、自分たちはOctopusを使ってしまっていた

Rails 4.2を利用していて、[Octopus](https://github.com/thiagopradi/octopus)使っていて、からのー、Rails 5にアップグレード、うっ、頭が…

[Octopus](https://github.com/thiagopradi/octopus)はActiveRecordの標準の機能を拡張してシャーディングの機能を提供してくれるところが良いところなのですが、裏を返せばActiveRecordの実装に手を入れるということなのでRailsのバージョンアップコストがとても高いというデメリットがあります。

そこそこコードが成長してしまっていたので、今から他のGemに乗り換えるのは難しそうでした。([activerecord-sharding](https://github.com/hirocaster/activerecord-sharding)などは、独自メソッドやDSLをActiveRecordに生やすように実装されているので、ActiveRecordのもとの実装に影響されない。かわりに、コードのマイグレーションが必要だった)



# ぶっちゃけやりたいのはシャーディングだけで、そんなに多機能なGemじゃなくてよかった

Rails 5 readyなoctopusが欲しい…と思っていたところに本家issueにこんなコメントが

![image](https://qiita-image-store.s3.amazonaws.com/0/45408/b171ea5a-50a0-4e13-c616-c0aeb2a52a40.png)

もうOctopusメンテされないらしい…チャンス…!?

※と…思っていたのだけれど、Gem作ってからこの記事書いてる間にこんなコメントが

![image](https://qiita-image-store.s3.amazonaws.com/0/45408/d443a4e1-26ca-d3e6-5d32-36cbab8790e8.png)

ああっ…台無し…！？　でも、ここまできたので続けていきます


# Octopusはもうダメかもしれない :fearful:  …Tako作ろ…

ということで、Octopusのクローンの[Tako](https://github.com/the40san/tako)を作ってみました。
MITライセンスで公開しています。

[Tako](https://github.com/the40san/tako)で実現したかったことの要件をまとめると以下のような感じです

 * Octopusの#usingメソッドのように、与えられたブロック内またはそのメソッドチェイン内でシャードを指定してSQLを発行したい。
 * この際なのでActiveRecordの実装への依存を可能な限り減らしたい。
 * Rails標準のマイグレーションも可能な限り利用できるようにしたい。
    * 完全水平分割だけでいいのでシャード指定のマイグレーションとか要らない
 * Railsのバージョンアップがあったら、可能な限り早く対応したい。
 * Railsの対応バージョンは`<= 4.0`で5系対応。 3系はもう良いかなって…


# 使い方
## インストール
```rb:Gemfile
gem 'tako'
```

```
bundle install
```

or 

```
gem install tako
```

で利用できます

## DBのセットアップ

`config/shards.yml`を作成します。読み込む際に、ERBで一度パースされます。
`config/shards.yml`の内容は`Tako.config`で取得することができます。

```erb:config/shards.yml
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`と名前が重複するからです。

```rb
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のインスタンスが作成されたスコープにおけるシャードが、保存先のシャードになる実装になっています。

```rb
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)`で、ブロック内のコードが全シャードで実行されます

```rb
# 全シャードでMikeさんがお見えになる
Tako.with_all_shards do
  User.create(name: "Mike")
end
```



# Octopusからマイグレーションしたい場合

まずはじめに、ありがとうございます！

以下の手順でマイグレーションできます。
migration等でOctopusの機能を利用している場合は、避けていただくか、TakoにPRを送ってください。

1. `Octopus.using` または`Model.using`を`Tako.shard`または`Model.shard`に置換します
2. `config/shards.yml`をTako用に書き換えます





# 実装で苦労した所/妥協した所

## prependが使えない

他にActiveRecordを拡張するgemを利用していた場合、例えばsave!メソッドを`alias_method`または`alias`するようなgemが入っていると、prependで`super`を呼び出そうとして、`stack level too deep`になってしまいます。なので、今回[Tako](https://github.com/the40san/tako)の実装には同じくalias_methodが利用されています…


## ActiveRecord::Migratorが結構魔界

Railsのソース読む上で一番苦労したのがMigratorのコードを追うところでした。
Rails内部から利用される想定で書いてあるので、外からの呼び出しが辛かった覚えがあります。


## ConnectionPoolが利用できない

Thread使うことって、個人的にはあんまりないかなって思ったので、実装が楽な方を取りました。
[Tako](https://github.com/the40san/tako)では、connection_poolは利用できません。




# まとめ

以上、シャーディングに関するつらみとTakoの紹介でした。