おおいしつかさ


旅行とバイクとドライブと料理と宇宙が好き。
Ubie Discoveryのプログラマ。
Share:  このエントリーをはてなブックマークに追加

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

追記)
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対応
・エラーが発生したときのフェイルセーフ機能
・スレーブへアクセスしたくないモデルの指定機能