RailsでMysqlスレーブ群をロードバランサ経由で使用できる、FreshConnectionを作り始めました

Tsukasa OISHI

追記)
FreshConnectionの最新情報はこちら
2016/3現在、Rails4.0以降に対応しています。

以前、RailsでMysqlのスレーブサーバーたちを、LVS+keepalivedのようなロードバランサー経由で使用したいという記事を書きました( ActiveRecordのコネクションプーリングをやめてみる)。その続きになります。
前回はアクションごとにコネクションを切断するだけでしたが、今回は実際にスレーブ群を使用する、FreshConnectionというライブラリを作り始めました。
現在のところ、Rails2.3.14でのみテストしています。今日は時間がなかったので実際にロードバランサーを使った環境でテストしていませんが、次はそれと合わせてRails3対応も考えていこうと思います。

コードは以下で公開しています。
https://github.com/tsukasaoishi/fresh_connection

以下のような環境を前提にして話をすすめていきます。

RailsApp --------- Mysql(Master) 192.168.11.1
             |
             |                     +------ Mysql(Slave1)
             |                     |
             +---- LVS ------------+
                 192.168.11.10     |
                                   +------ Mysql(Slave2)

1.Railsでのセットアップ
まず、GemfileにFreshConnectionを指定します。

gem "fresh_connection", "=0.0.1"

次にconfig/database.ymlでスレーブの設定をします。

production:
  adapter: mysql
  encoding: utf8
  reconnect: true
  database: kaeru
  pool: 5
  username: master
  password: master
  host: 192.168.11.1
  socket: /var/run/mysqld/mysqld.sock

  slave:
    username: slave
    password: slave
    host: 192.168.11.10
    max_connection: 5

production環境のスレーブは上のように書きます。基本的には通常の設定と同じ項目が指定できます。設定していないものは、マスターの設定が使われます。
ひとつだけ新しい項目があって、それがmax_connectionになります。これは、スレーブへのコネクションの最大数となります。

さらに、config/environment.rbでrackの入れ替えを行います。

...
Rails::Initializer.run do |config|
  ...
end

require 'fresh_connection'
ActionController::Dispatcher.middleware.swap ActiveRecord::ConnectionAdapters::ConnectionManagement, FreshConnection::Rack::ConnectionManagement

これで準備を完了です。

2.使い方
Selectなど、読み込み系のSQLはスレーブに投げられます。

  Article.find(1)

マスターを使いたいときは、 :readonly => false を指定します。

  Article.find(1, :readonly => false)

また、トランザクション内のクエリはすべてマスターに投げられます。

  Article.transaction do
    Article.find(1)
  end

3.使ってみる
以下のようなアクションでテストしてみます。

class ArticlesController < ApplicationController
  def test
    Article.find(105)
    Article.find(106, :readonly => false)
    Article.transaction do
      Article.find(107)
    end

    10.times do |num|
      Article.find(100 + num)
    end

    render :text => "ok"
  end
end

アクセスしたときのRailsログを見てみます。

  [MASTER] SQL (0.1ms)   SET NAMES 'utf8'
  [MASTER] SQL (0.1ms)   SET SQL_AUTO_IS_NULL=0

Processing ArticlesController#test (for 127.0.0.1 at 2011-10-16 21:23:42) [GET]
  [MASTER] Article Columns (0.6ms)   SHOW FIELDS FROM `articles`
  SQL (0.1ms)   SET NAMES 'utf8'
  SQL (0.0ms)   SET SQL_AUTO_IS_NULL=0
  Article Load (0.1ms)   SELECT * FROM `articles` WHERE (`articles`.`id` = 105) 
  [MASTER] Article Load (0.1ms)   SELECT * FROM `articles` WHERE (`articles`.`id` = 106) 
  [MASTER] SQL (0.0ms)   BEGIN
  [MASTER] Article Load (0.1ms)   SELECT * FROM `articles` WHERE (`articles`.`id` = 107) 
  [MASTER] SQL (0.0ms)   COMMIT
  SQL (0.0ms)   SET NAMES 'utf8'
  SQL (0.0ms)   SET SQL_AUTO_IS_NULL=0
  Article Load (0.1ms)   SELECT * FROM `articles` WHERE (`articles`.`id` = 100) 
  SQL (0.0ms)   SET NAMES 'utf8'
  SQL (0.0ms)   SET SQL_AUTO_IS_NULL=0
  Article Load (0.1ms)   SELECT * FROM `articles` WHERE (`articles`.`id` = 101) 
  SQL (0.0ms)   SET NAMES 'utf8'
  SQL (0.0ms)   SET SQL_AUTO_IS_NULL=0
  Article Load (0.1ms)   SELECT * FROM `articles` WHERE (`articles`.`id` = 102) 
  SQL (0.0ms)   SET NAMES 'utf8'
  SQL (0.0ms)   SET SQL_AUTO_IS_NULL=0
  Article Load (0.1ms)   SELECT * FROM `articles` WHERE (`articles`.`id` = 103) 
  Article Load (0.0ms)   SELECT * FROM `articles` WHERE (`articles`.`id` = 104) 
  CACHE (0.0ms)   SELECT * FROM `articles` WHERE (`articles`.`id` = 105) 
  CACHE (0.0ms)   SELECT * FROM `articles` WHERE (`articles`.`id` = 106) 
  CACHE (0.0ms)   SELECT * FROM `articles` WHERE (`articles`.`id` = 107) 
  Article Load (0.0ms)   SELECT * FROM `articles` WHERE (`articles`.`id` = 108) 
  Article Load (0.0ms)   SELECT * FROM `articles` WHERE (`articles`.`id` = 109) 
Completed in 13ms (View: 0, DB: 1) | 200 OK [http://localhost/articles/test]

FreshConnectionは、マスターへのアクセスのときはSQLログに [MASTER] を付与するので、どのクエリがマスターに、どれがスレーブに投げられたのかわかるようになっています。
期待したように処理しているようです。

MySqlのクエリログは以下のようになっています。

111016 21:28:20    60 Connect   master@localhost on kaeru
                   60 Query     SET NAMES 'utf8'
                   60 Query     SET SQL_AUTO_IS_NULL=0
                   60 Statistics
                   60 Query     SHOW FIELDS FROM `articles`
                   61 Connect   slave@localhost on kaeru
                   61 Query     SET NAMES 'utf8'
                   61 Query     SET SQL_AUTO_IS_NULL=0
                   61 Query     SELECT * FROM `articles` WHERE (`articles`.`id` = 105)
                   60 Query     SELECT * FROM `articles` WHERE (`articles`.`id` = 106)
                   60 Query     BEGIN
                   60 Query     SELECT * FROM `articles` WHERE (`articles`.`id` = 107)
                   60 Query     COMMIT
                   62 Connect   slave@localhost on kaeru
                   62 Query     SET NAMES 'utf8'
                   62 Query     SET SQL_AUTO_IS_NULL=0
                   62 Query     SELECT * FROM `articles` WHERE (`articles`.`id` = 100)
                   63 Connect   slave@localhost on kaeru
                   63 Query     SET NAMES 'utf8'
                   63 Query     SET SQL_AUTO_IS_NULL=0
                   63 Query     SELECT * FROM `articles` WHERE (`articles`.`id` = 101)
                   64 Connect   slave@localhost on kaeru
                   64 Query     SET NAMES 'utf8'
                   64 Query     SET SQL_AUTO_IS_NULL=0
                   64 Query     SELECT * FROM `articles` WHERE (`articles`.`id` = 102)
                   65 Connect   slave@localhost on kaeru
                   65 Query     SET NAMES 'utf8'
                   65 Query     SET SQL_AUTO_IS_NULL=0
                   65 Query     SELECT * FROM `articles` WHERE (`articles`.`id` = 103)
                   61 Query     SELECT * FROM `articles` WHERE (`articles`.`id` = 104)
                   65 Query     SELECT * FROM `articles` WHERE (`articles`.`id` = 108)
                   61 Query     SELECT * FROM `articles` WHERE (`articles`.`id` = 109)
                   60 Quit
                   61 Quit
                   64 Quit
                   62 Quit
                   65 Quit
                   63 Quit

テストではロードバランサー環境を用意する時間がなかったので、ひとつのMySqlサーバ上で実行しています。
しかし。コネクションがマスターを含めて6本存在していることがわかります。また、期待したコネクションに対してクエリが投げられていることがわかります。

4.今後の予定
・実際にロードバランサー環境でのテスト
Rails3対応
・エラーが発生したときのフェイルセーフ機能
・スレーブへアクセスしたくないモデルの指定機能