LoginSignup
28
22

More than 5 years have passed since last update.

ActiveRecordでDBの水平分割をする話

Last updated at Posted at 2016-12-18

この記事はRuby on Rails Advent Calendar 2016の19日目の記事です。

概要

いまゲームのAPIサーバとしてRailsを使っているのですが、dbを水平分割する必要があってgemを探していました。

最終的に自前でgemを作ったのですが、その調査内容と作成した経緯についてお話します。

要件

(なるべく)ノーメンテでスケールアウトしたい

ゲームでシャードを追加するような状況で一番想定されるのが、初期導入によるユーザ数の爆発的な増加が上げられます。

このときにメンテをするのはビジネス的な損失が非常に大きいので、なるべくスケールアウトはノーメンテでやりたいですね。

(なので、スケールインはメンテありでも問題ありません)

自動で振り分けるのでなくある程度自前でハンドリングしたい

ゲームではアイテムやスキル、フレンドなど「キャラクター*N個」のデータになるものがほとんどです。

そのため、「キャラクター*N個」の方のテーブルのレコード数が先に性能限界を迎えやすいのですが、なるべくキャラクターデータの保存されているシャード番号とキャラクターの関連しているデータのシャード番号は揃えておく方が都合が良いことが多いです(調査のときや、スケールインのときなど)。

そのため、自前のルールで振り分けるシャードを決定出来るというのが要件に上げられました。

調査

いろいろ調べて見つけたのが、クックパッドさんのmixed_gaugeというgemです。

詳しくはクックパッド開発者ブログのシンプルで移行しやすいデータベースシャーディングの記事を参照していただいたほうが早いでしょう。

mixed_gauge

mixed_gaugeは、

  • 水平分割対応
  • レプリケーション対応
  • rails5対応
  • モンキーパッチがほとんどない
  • コードベースが小さい

と機能も多く、バージョンアップなども容易であるように思えたのでこれを使いたかったのですが、「シャードへの振り分けをこちらでコントロールしたい」という要件だったのでそのまま採用といきませんでした。

activerecord-sharding

もう一つ候補に上がったのが、activerecord-shardingです。

こちらは採番テーブルによるシャーディングが採用されています。

CEDEC2016のモンスターストライクを支える負荷分散手法でも紹介されており、モンストのバックエンドでも使われているため信頼性は高いでしょう。

mixed_gaugeと比べた時のメリット/デメリットとしては、

  • レプリケーション機能がない
  • 振り分け方式はプラグイン形式になっており採番テーブル以外でも使えるが、対応が中途半端(モンキーパッチが必要)

といった感じですね。

ただこちらも mixed_gauge と同じくシャードへの振り分けをこちらでコントロールしたいという要件を満たせなかったため、採用にはならずでした。

activerecord-shard_for

いろいろ調べはしましたが、結局要件を満たせるgemが見つからなかったので自前で作成することにしました。

今回作成した activerecord-shard_for は(「作成した」と言うのがおこがましいぐらいに)、前者2つのgemのコードベースを 丸パクリ 流用して作成しました。

2つのgemの良いところをピックアップして、

  • 水平分割対応
  • レプリケーション対応
  • 振り分け方式をプラグイン形式に
  • 分割方法をキー指定と範囲指定と両方を可能に

というgemになっております。

また、octopusのような using シンタックスでシャードを明示的に指定して呼ぶことも可能です。

最も特徴的なところは「振り分けをプラグイン方式にした」ところで、要件に合わせてシャードへの振り分けルールだけを実装することが出来るようになっています。

詳しくはwikiを見てもらうのが良いですが、簡単に例を挙げるとこんな感じです。

# database.yml
production_user_001:
  adapter: mysql2
  username: user_writable
  host: db-user-001
production_user_002:
  adapter: mysql2
  username: user_writable
  host: db-user-002
production_user_003:
  adapter: mysql2
  username: user_writable
  host: db-user-003
production_user_004:
  adapter: mysql2
  username: user_writable
  host: db-user-004
# router plugin
class SimpleModuloRouter < ActiveRecord::ShardFor::ConnectionRouter
  # keyには、def_distkeyに設定したカラムの値が渡される
  def route(key)
    key.to_i % connection_count
  end
end

# initializer
ActiveRecord::ShardFor.configure do |config|
  # SimpleModuloRouterを登録する
  config.register_connection_router(:modulo, SimpleModuloRouter)

  # シャーディングクラスタの設定
  config.define_cluster(:user) do |cluster|
    # unique identifier, connection name
    cluster.register(0, :production_user_001)
    cluster.register(1, :production_user_002)
    cluster.register(2, :production_user_003)
    cluster.register(3, :production_user_004)
  end
end

# AR model
class User < ActiveRecord::Base
  include ActiveRecord::ShardFor::Model
  use_cluster :user, :modulo # 登録したシャーディングクラスタとSimpleModuloRouterを使う宣言

  # idをルーティングキーとして設定
  # Routerのrouteメソッドにレコードのidが渡される
  def_distkey :id

  def self.generate_unique_id
    # Implement to generate unique id
  end

  before_put do |attributes|
    attributes[:id] = generate_unique_id unless attributes[:id]
  end
end

この例だと、 SimpleModuloRouter#route の結果が0なら :production_user_001 に、 1なら :production_user_002 に振り分けられるようになります。

Routerクラスの route メソッドをclusterに登録したshardのkeyに振り分けるようにするだけなので、要件にあわせて柔軟にシャードへの振り分けルールが実装可能になっているはずです。

DBを水平分割することがないのが一番ですが、水平分割する必要が出てきた際は選択肢の一つにどうでしょうか。

28
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
22