iQON Rails4.0への移行に関して① ~MySQLのgemを修正~

みなさん初めまして! 体を引き締めるため、強い漢になるために最近キックボクシングをはじめたボブです。 4月1日からvasilyでバックエンドエンジニアとして働きはじめ、今回Techブログを初めて書かさせていただきます。よろしくお願い致します。 今回は自分が今担当している、iQONのRails4.0移行について書きたいと思います。その中でも今回は「mysqlのgemの設定」に焦点を当てて記載していきたいと思います。

はじめに

VASILYでは約2年ほど前に、PHPで構成されていたiQONをRuby on Railsで書き直しました。 しかし一度実装して以来、プロダクトの新規システム実装や運用に時間をとられてしまい、Railsやミドルウェアのバージョンをあげるなどの作業を行うことができていませんでした。 待望のRails4.0が正式リリースされたこのタイミングにあわせて、Railsならびにミドルウェアやgemのバージョンをあげることにしたので、その作業の中で自分が苦労した点や、参考になりそうな点をこれから何回かに分けてまとめていきたいと思います。

Rails4.0化するメリット

まず、Rails4.0化するメリットとしては大きく分けて以下の3つの理由があると考えています。

速度改善

Rails4.0に移行することで、ユーザーがサービスにアクセスした際のレスポンスの速度が大きく改善されます。 あくまで一つのエントリポイントに対してのアクセスに関してですが、具体的には以下のような速度改善に成功しています。

[bob@iqon_rails iqon_api]$ ./bin/rails runner batch/test/response/response_time.rb # 既存のインスタンスのベンチマーク
user system total real 0.000000 0.010000 0.010000 ( 0.270074) # Rails4.0インスタンスのベンチマーク
user system total real 0.000000 0.000000 0.000000 ( 0.045121) 

また開発を行う面でも、サーバーやrailsコンソールを立ち上げる速度が大きく改善されていることが肌感でも感じることができます。

より効率的な書き方で実装できる

Rails4.0では非効率だった既存のメソッドの削除やより最新の思想に基づいた新しい仕組みなどが導入されています。 Rails4.0に準じた書き方に修正することで今までより、さらに効率的な実装を進めることができます。

ミドルウェアやgem、ロジックの見直し、バグの発見等がある

なかなか時間をとることができないgemやミドルウェアのアップデートも、Rails4.0化にあわせてアップデートするきっかけになります。またiQONのように新機能をどんどん追加していくような開発スタイルですと、スピード感を重視し、あまり効率的でないロジックを書いてしまっている場合がありますが、そちらを修正するきっかけにもなります。またその際に今まで発見されていなかったバグなどを発見することもできます。実際に今回の移行作業の中で、開発環境でのみ起きる既存のバグを見つけ改修することができました。

ActiveRecordの変更点

Rails4.0では以下が主にActiveRecord周りで改修された点となります。

  • set_table_nameメソッドの改修
  • finderメソッドの改修
  • ActiveRecord::SessionStoreの廃止
  • ActiveRecord Observersの廃止

このようにActiveRecordに関していくつかの改修が入っているため、現状のほとんどのgemが既存のままでは動かないという現象に悩まされました。これら廃止されたいくつかの機能はgemをインストールすることによって使用することが可能ですが、それでは本来のRails4.0の意図とずれてしまうため、今回はgemをforkして改修することで対応することとしました。

調査した/試したgemたち

弊社が運営するサービス、iQONではMySQLのマスター/スレーブ構成を実現するために、multi_dbというgemを使っています。 今回multi_db以外のgemもふまえて以下のgemを調査、仮導入してみました。

  • multi_db
  • octopus
  • db_charmer
  • seamless_database_pool

Rails4.0で使用されているgemの依存関係やActiveRecordに大きく改修が入っていることなどから、既存でリリースされているgemではうまく最初から動くものはありませんでした。 結論としては、gemの改修にかかるコスト、gemの思想とiQONの実装状況をふまえた上で、既存で使っているmulti_dbをforkしてきて一部修正することでmaster/slave構成を実装しました。

保留したgemとそのエラー

orcpusに関して

ar-octopus-0.5.0を使用してアクセスした場合、以下のようなエラーが発生しました。

/vendor/bundle/ruby/2.0.0/gems/activesupport-4.0.0/lib/active_support/core_ext/module/aliasing.rb:32:in `alias_method': undefined method `set_table_name' for class `Class' (NameError) from /home/bob/iqon_api/vendor/bundle/ruby/2.0.0/gems/activesupport-4.0.0/lib/active_support/core_ext/module/aliasing.rb:32:in `alias_method_chain'

set_table_nameでgrepした結果

./README.mkdn: set_table_name("yummy")
./spec/octopus/model_spec.rb: describe "when using set_table_name" do ./spec/octopus/model_spec.rb: Keyboard.should_not_receive(:reset_table_name) ./spec/support/database_models.rb: set_table_name "yummy" ./spec/support/database_models.rb: set_table_name { "yummy" } ./lib/octopus/model.rb: if self != ActiveRecord::Base && self.respond_to?(:reset_table_name) && !self.custom_octopus_table_name ./lib/octopus/model.rb: self.reset_table_name() ./lib/octopus/model.rb: alias_method_chain(:set_table_name, :octopus) ./lib/octopus/model.rb: def set_table_name_with_octopus(value = nil, &block) ./lib/octopus/model.rb: set_table_name_without_octopus(value, &block) ./lib/octopus/model.rb: def octopus_set_table_name(value = nil) ./lib/octopus/model.rb: ActiveSupport::Deprecation.warn "Calling `octopus_set_table_name` is deprecated and will be removed in Octopus 1.0.", caller ./lib/octopus/model.rb: set_table_name(value) 

set_table_nameを修正

set_table_nameはrails4.0から使用できなくなったため、修正してもう一度実行すると

 /home/bob/iqon_api/vendor/bundle/ruby/2.0.0/gems/activesupport-4.0.0/lib/active_support/core_ext/module/aliasing.rb:32:in `alias_method': undefined method `announce' for class `Class' (NameError) from /home/bob/iqon_api/vendor/bundle/ruby/2.0.0/gems/activesupport-4.0.0/lib/active_support/core_ext/module/aliasing.rb:32:in `alias_method_chain'

エラーを調べてみると、Rails3.1でも同じエラーがでていたようでした。こちらは問題の根が深そうなのでいったんここで保留しました。

db_charmerに関して

こちらはrails4.0で使用するgem同士の依存関係によりエラーが発生してしまいました。

gem 'db-charmer', '1.8.4', :require => 'db_charmer' をGemfileに記載した場合

Bundler could not find compatible versions for gem "activesupport": In Gemfile: db-charmer (= 1.8.4) ruby depends on activesupport (<= 3.2.13) ruby rails (= 4.0.0) ruby depends on activesupport (4.0.0) Bundler could not find compatible versions for gem "railties": In Gemfile: rails (= 4.0.0) ruby depends on railties (= 4.0.0) ruby sass-rails (~> 4.0.0) ruby depends on railties (4.0.0.rc2)

最新のdb-charmerをインストールしようとすると、gem同士の依存関係のエラーにより、インストールすることができませんでした。

バージョンの指定を外し、gem 'db-charmer', :require => 'db_charmer' をGemfileに記載した場合

db-charmer-1.6.13がインストールされました。そこで実際にActiveRecordでアクセスしてみると、下記のエラーが発生しました。

/home/bob/iqon_api/vendor/bundle/ruby/2.0.0/gems/activesupport-4.0.0/lib/active_support/core_ext/module/aliasing.rb:32:in `alias_method': undefined method `format_log_entry' for class `ActiveRecord::ConnectionAdapters::AbstractAdapter' (NameError)

エラーがでているformat_log_entryについて各db-charmerでgrepをかけてみると、

1.6.13

./lib/db_charmer/abstract_adapter_extensions.rb: base.alias_method_chain :format_log_entry, :connection_name

1.8.4

./lib/db_charmer/rails2/abstract_adapter/log_formatting.rb: base.alias_method_chain :format_log_entry, :connection_name format_log_entry

メソッドはrails2以前のActiveRecordでのみ使用されていたメソッドです。最新版の1.8.4ではrails2使用時にformat_log_entryを呼び出していますが、1.6.13ではrailsのバージョンに関係なく呼び出されてしまうため、エラーが発生してしまっています。 この段階でdb_charmerを保留することとしました。

seamless_database_poolに関して

こちらに関しては調査した情報だと、controllerで接続先を設定するように書かれていたので、その段階で使わないことにしました。

multi_dbの改修内容

multi_dbを通常通りGemfileに入れて呼び出した場合、以下のようなエラーが出てしまいました。

/vendor/bundle/ruby/2.0.0/gems/multi_db-0.3.1/lib/multi_db/connection_proxy.rb:57:in `setup!': uninitialized constant ActiveRecord::Observer (NameError)

自分たちのプロダクトgemに求めていた機能は、「master/slaveをきちんと切り分けてアクセスできる」部分だけだったので、以下のソースをコメントアウトして対応しました。

lib/multi_db/connection_proxy.rb

 slaves = init_slaves raise "No slaves databases defined for environment: #{self.environment}" if slaves.empty? master.send :include, MultiDb::ActiveRecordExtensions - ActiveRecord::Observer.send :include, MultiDb::ObserverExtensions + #ActiveRecord::Observer.send :include, MultiDb::ObserverExtensions master.connection_proxy = new(master, slaves, scheduler) master.logger.info("** multi_db with master and #{slaves.length} slave#{"s" if slaves.length > 1} loaded.") end

この修正のみで正常に動かすことができました。

テスト用スクリプトの実装

ここで実際にmulti_dbが、書き込みはmaster、読み込みはslaveへアクセスしているかのテストを書きたいと思います。テストをしたいと思います。

multi_db_test.rb

1 =begin 2 * MySQLに関するテスト 3 * テスト内容 4 ** modelを呼び出し、読み込みと書き込みを行う 5 ** slave, masterにきちんとアクセスが分かれているかは、mysqlのログで確認 6 =end 7 8 data = Hoge.where(:id => 1).first 9 p data.params1 # => 0 10 item.params1 = 1 11 item.save! 12 data_new = Hoge.where(:id => 1).first 13 p item.params1 # => 1

MySQLのクエリログを落とすようにMySQL上で設定し、実際に各サーバーに飛んできているクエリを確認してみます。

mysql> use <db名>; 
mysql> SET GLOBAL general_log = 'ON'; 
mysql> SET GLOBAL general_log_file = '/var/log/mysql.log';

上記の設定をした状態で、テスト用のバッチを動かすと、masterとslaveには以下のようなログが吐き出されます。

masterのログ

130715 15:34:05 198 Connect hoge@xxx.xxx.x.xxx on hoge 198 Query SET @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483, @@SESSION.sql_mode = 'STRICT_ALL_TABLES' 198 Query SHOW TABLES LIKE 'hoge' 198 Query SHOW CREATE TABLE `hoge` 198 Query SHOW FULL FIELDS FROM `hoge` 198 Query BEGIN 198 Query UPDATE `hoge` SET `update_time` = '2013-07-13 00:29:02' WHERE `hoge`.`params1` = 1 198 Query COMMIT 198 Quit

slaveのログ

130715 15:34:05 67 Connect hoge@xxx.xxx.x.xxx on hoge 67 Query SET @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483, @@SESSION.sql_mode = 'STRICT_ALL_TABLES' 67 Query SHOW TABLES LIKE 'hoge' 67 Query SHOW FULL FIELDS FROM `hoge` 67 Query SELECT `hoge`.* FROM `hoge` WHERE `hoge`.`id` = 1 ORDER BY `hoge`.`id` ASC LIMIT 1 2 Query UPDATE `hoge` SET `update_time` = '2013-07-13 00:29:02' WHERE `hoge`.`id` = 1 67 Query SELECT `hoge`.* FROM `hoge` WHERE `hoge`.`id` = 1 ORDER BY `hoge`.`id` ASC LIMIT 1 67 Quit

きちんとアクセスが振り分けられていることが確認できました。

今後の予定

現状としては、現在使用しているミドルウェアに接続できることの確認が終了し、いくつかのエントリポイントに関してのアクセスが正常に挙動することの確認ができ、テストを書きながら既存のソースを移行するフェーズとなっています。今後は移行に関してもっと全体的な進め方の方法などについて記載していきたいと考えています。

最後に

今回Rails4.0化におけるMySQLのmaster/slave構成に対する対処法について記載しましたが、この対処法がベストだとはおもっていません。もしもっといい方法で実現されている、おすすめの方法を知っている方がいましたらぜひ教えてください。いやVASILYに入って一緒にチャレンジしていきましょう! VASILYでは現在、一緒に働いてくれるエンジニアを大募集しています! 募集要項 連絡先:info@vasily.jp