ドメイン名を使ってEC2を運用していたら、ELBのスケールアウトで苦労した話
2014年5月14日 13時00分 修正 タイトルが誤解を招くものだったので、「なぜ URL に www を付けるのか。または、サブドメインなしでは CNAME が使えない件」から変更致しました。併せて、画像に Public IP と Private IP の明記を行いました。
皆さん、こんにちは。MUGENUP の osada です。
今回は、スケールアウト時にELB(Amazon Elastic Load Balancer)
を使うときの注意点についての記事です。
といっても、インフラ・エンジニア
には自明のことと思いますので、読者の対象は
インフラ・エンジニアではないけど、インフラもやる
というベンチャーならではのエンジニア向けです。
要旨
ELB
にはEIP
などのAレコード
を関連付けることが出来ず、ドメインとサーバーを結びつけるには提供されるCNAMEを使う必要がありますサブドメイン無しのドメイン名 (zone apex)
には、CNAMEレコード
を貼ることはできません。ELB
を使ったスケールアウトを考えているなら、zone apex
ではなく、サブドメイン
で運用すると簡単ですzone apx
を使って運用する場合、スケールアウトには、route 53
が必要そうです。
弊社 のインフラは AWS です。
まず弊社のインフラをお話すると、AWS(Amazon Web Service)を使用しており、CTO の伊藤がメイン・オペレーションを担当しています
(彼は TCP/IP や WebSocket の RFP などを嬉々として語る通信系のツワモノです)。
しかし、それでは CTO が 単一障害点(Single Point of Failure, SPOF)
となってしまうため、
社員全員が AWS へのアクセス権を持ち、一通りオペレーションができる状態となっています。
スケールアウトの頓挫
さて、先日、ご好評いただいている弊社のゲーム攻略サイトみなゲー(β版)が アクセス増のため、スケールアウトが必要となりました。
サーバ1台だったものを、ロードバランサーを使って、2台構成にする
という、とても単純な作業です。
……と、その時はまだ思っていたのです。
元々は、サーバ1台に対して、IP を使って DNS を貼っていました。
そこで、ELBに従来のIPを設定し、ELBから、新しい2台のサーバに振り分けることを計画しました。
ところが、AWS の ロードバランサーである、ELB (Elastic Load Balancing) には、 IP を振ることができなかったのです。
ELB が IP を持たない理由
SPOF(単一障害点)
の話を上述しましたが、これはロードバランサーにおいても同じです。
もしロードバランサー1台が壊れてしまったら、どのサーバにもアクセスできなくなってしまいます。
ELBはそれを避けるため、複数台のロードバランサーを1つとして扱っているようです。
そのため、ELB 自体には、DNS が振られるのであって、IP を振ることはできません。
IP を振る とは? [DNS の設定]
DNS とは、ドメイン名(mina-game.com
など)を扱うサービスです。
ドメイン名の扱い方には複数ありますが、
本稿では、IPアドレスと結びつけるAレコード
と、別名を付けるCNAMEレコード
を取り上げます
レコード名 | 役割 | 例 |
---|---|---|
A (Adress) レコード |
IPアドレスと結びつける | mina-game.com => 123.456.78.90 |
CNAME レコード |
ドメイン名に別名を付ける | mina-game.com => www.mina-game.com |
今まで、mina-game.com
にアクセスすると、123.456.78.90
のサーバにアクセスできていたのは、
Aレコード
が付いていたからです。
ところが、ELB
には IP を付けられない ため、Aレコードを貼ることができません。
よって、この場合、 CNAME を貼る ことが正解となります。
……そう、その時は、それで解決すると思っていたのです。
CNAME には、サブドメイン(ホストドメイン)が必須
ところが、ドメイン名mina-game.com
に対して、CNAME を振ることは出来ない のです。
少々長いですが、AWS から引用します。
ドメイン名を登録する際は、ドメイン名自体だけでなく、一連のサブドメイン名全体も予約することになります。例えば、example.com をカスタムドメイン名として登録する場合、foo.bar.example.com や foo.myLB.example.com などのサブドメイン名を作成することができます。この一連のドメインとサブドメイン名は、ゾーンと呼ばれます。予約した example.com などのドメイン名は、ゾーンの階層の最上部にあるため、Zone Apex と呼ばれます。 DNS ルールでは Zone Apex(example.com など)での CNAME レコードの作成が許可されていません。例えば、example.com というドメイン名を所有している場合、CNAME レコードはサブドメイン名である foo.example.com には使用できますが、Zone Apex である example.com には使用できません。 http://docs.aws.amazon.com/ja_jp/ElasticLoadBalancing/latest/DeveloperGuide/using-domain-names-with-elb.html
mina-game.com
は ゾーンの階層の最上部にある、Zone Apex
であり、それには CNAME
を使用する事ができないのです。
もし、www.mina-game.com
などの サブドメイン を指定してあったなら、
cname
の差し替えで期待どおりの処理が終わっていたことでしょう。
しかし、今回は zone apex
で運用していたため、ELB
を使ったスケールアウトをすることはできませんでした。
Route 53 を使用する
zone apex
で運用を始めてしまった以上、ELB
でのスケールアウトは無理なのでしょうか?
AWSのヘルプの続きに、こう書いてあります。
Zone Apex とロードバランサーの DNS 名を関連付ける場合は、次のオプションを使用します。 Option 2: DNS サービス Amazon Route 53 はホストゾーンのドメインに関する情報を格納するため、Amazon Route 53 を使用してドメインを作成します。
あ、Route 53
使えば良いんですね。
まるでこのために作られたのではないかと思えるくらい、 完璧なお膳立てで、AWS 製の DNS Amazon Route 53 を案内されてしまいました。
というわけで、zone apex
を使って AWS ELB
でスケールアウトするには、route 53
を使おう、という結論に落ち着きました.
元々は使用していなかったサービスが一つ追加で必要になってしまいましたが、無事目的を達成出来ました。
まとめ
ELB
にはEIP
などのAレコード
を関連付けることが出来ず、ドメインとサーバーを結びつけるには提供されるCNAMEを使う必要がありますサブドメイン無しのドメイン名 (zone apex)
には、CNAMEレコード
を貼ることはできません。ELB
を使ったスケールアウトを考えているなら、zone apex
ではなく、サブドメイン
で運用すると簡単ですzone apx
を使って運用する場合、スケールアウトには、route 53
が必要そうです。
というわけで、インフラ初心者が送る
スケールアウトで困った経験
の共有でございました。
参考
もっと詳しい解説
【Rails3.2】【バッドノウハウ】includes 使用時に where で ドットを使うと想定外になるかも知れない件、または、なぜRails4 から references が必要になったのか
皆さん。お久しぶりです。MUGENUP の osada です。
某死にすぎるゲーム
の1周目をクリアしたので、戻ってまいりました!
目標を達成するために大事なことは観察力
である、ということを思い知らされるゲームでした。
さてそんな今回は、Rails 3.2
の includes
に関するバッドノウハウの共有です。
弊社 okuda が、トライ・アンド・エラーの末に見つけた、バグの解説になります。
Rails 4 系
をお使いの方には不要ですので、何かの話のネタになれば幸いです。
includes は LEFT OUTER JOIN と eager loading の 2 種類がある
Rails
では、includes
は 2種類の使い方ができます。
1. eager loading
2. LEFT OUTER JOIN
通常の使い方だと、(1) の eager loading
となり、
where
句の中で、includes
した テーブルを参照すると、
(2) の LEFT OUTER JOIN
となる、という認識でした。
class User < ActiveRecord::Base attr_accessible :email, :name has_many :user_items end describe User do let!(:user) { create :user, name: "user0", email: "user0@example.com" } it "eager loading" do subject = User.where("`name` LIKE ?", "%user0%").explain expect(subject).to include "SELECT `users`.* FROM `users` WHERE (`name` LIKE '%user0%')" # => User Load (0.9ms) SELECT `users`.* FROM `users` WHERE (`name` LIKE '%user0%') # => UserItem Load (1.3ms) SELECT `user_items`.* FROM `user_items` WHERE `user_items`.`user_id` IN (252) end it "outer join by string" do subject = User.includes(:user_items).where("user_items.number IS NULL").explain expect(subject).to include "LEFT OUTER JOIN" # => SQL (2.0ms) SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`email` AS t0_r2, `users`.`created_at` AS t0_r3, `users`.`updated_at` AS t0_r4, `user_items`.`id` AS t1_r0, `user_items`.`user_id` AS t1_r1, `user_items`.`item_id` AS t1_r2, `user_items`.`number` AS t1_r3, `user_items`.`created_at` AS t1_r4, `user_items`.`updated_at` AS t1_r5 FROM `users` LEFT OUTER JOIN `user_items` ON `user_items`.`user_id` = `users`.`id` WHERE `user_items`.`number` IS NULL end end
一体何が問題なのか?
上でLEFT OUTER JOIN
になる条件を
where
句の中で、includes
した テーブルを参照する と書いたのですが、それは誤りでした。
端的に言うと、 where 句の中で、ドットが含まれていれば、LEFT OUTER JOIN とみなす 、ということだったのです。
何が問題になるのでしょうか?
皆さん。where 句に email を使ったことはありませんか?
it "like email" do subject = User.includes(:user_items).where("`email` LIKE ?", "%user0@example.com%").explain expect(subject).to include "LEFT OUTER JOIN" expect(subject).to include "SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`email` AS t0_r2, `users`.`created_at` AS t0_r3, `users`.`updated_at` AS t0_r4, `user_items`.`id` AS t1_r0, `user_items`.`user_id` AS t1_r1, `user_items`.`item_id` AS t1_r2, `user_items`.`number` AS t1_r3, `user_items`.`created_at` AS t1_r4, `user_items`.`updated_at` AS t1_r5 FROM `users` LEFT OUTER JOIN `user_items` ON `user_items`.`user_id` = `users`.`id` WHERE (`email` LIKE '%user0@example.com%')" end
user_items
を join
するつもりがないのに、join
されています。
email にはほとんど必ずドット
が 含まれます。
email 以外では eager loading
なのに、email で where をした途端に、いきなりLEFT OUTER JOIN
になってしまうのです。
無論それは、email
に限りません。name
でドットが含まれていれば同じことです。
it "like name with dot" do subject = User.includes(:user_items).where("`name` LIKE ?", "%user0.name%").explain expect(subject).to include "LEFT OUTER JOIN" end
さらには、string
で構築しているから、という理由でもありません。
arel_table で構築しても、結果は同じです。
it "like name with dot on arel" do subject = User.includes(:user_items).where(User.arel_table[:name].matches("%user0.name%")).explain expect(subject).to include "LEFT OUTER JOIN" end
なぜこんなことになってしまうのでしょうか?
それは、eager loading
と LEFT OUTER JOIN
を区別するものが、下記のメソッドだからです。
references_eager_loaded_tables?
で、どちらなのかを決めますが、
その決定権を握るのがtables_in_string
です。
def references_eager_loaded_tables? joined_tables = arel.join_sources.map do |join| if join.is_a?(Arel::Nodes::StringJoin) tables_in_string(join.left) else [join.left.table_name, join.left.table_alias] end end joined_tables += [table.name, table.table_alias] # always convert table names to downcase as in Oracle quoted table names are in uppercase joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq (tables_in_string(to_sql) - joined_tables).any? end def tables_in_string(string) return [] if string.blank? # always convert table names to downcase as in Oracle quoted table names are in uppercase # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_'] end
string.scan
の中で、sql の中に、ドットが含まれているかどうか
、のみで判断していることが見て取れると思います。
末尾の行を pry した結果が下記です。
[1] pry(#<ActiveRecord::Relation>)> to_sql => "SELECT `users`.* FROM `users` WHERE (`users`.`name` LIKE '%user0.name%')" [2] pry(#<ActiveRecord::Relation>)> tables_in_string(to_sql) => ["users", "user0"] [3] pry(#<ActiveRecord::Relation>)> joined_tables => ["users"] [4] pry(#<ActiveRecord::Relation>)> (tables_in_string(to_sql) - joined_tables).any? => true
確かに、ドット
で判断していることが分かります。
まとめ
以上が、Rails3.2
で、includes
に起きる、バッドノウハウです。
知っていればどうということはありませんが、意図せず想定外の結果を産むこともありますので、十分お気をつけ下さい。
むしろ、この機会に、最近リリースされたRails4.1.0
に乗り換えるのも良いのではないでしょうか。
(おまけ) Rails 4.1 では、LEFT OUTER JOIN の使用に、references が必要になった。
さて、そんなわけで、Rails 4から、
LEFT OUTER JOINしたい時は明示的に、
references` を書くことになりました。
User.includes(:user_items).where("user_items.number IS NULL") # => Mysql2::Error: Unknown column 'user_items.number' in 'where clause': SELECT `users`.* FROM `users` WHERE (user_items.number IS NULL) User.includes(:user_items).where("user_items.number IS NULL").references(:user_items) # => SQL (0.4ms) SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`email` AS t0_r2, `users`.`created_at` AS t0_r3, `users`.`updated_at` AS t0_r4, `user_items`.`id` AS t1_r0, `user_items`.`user_id` AS t1_r1, `user_items`.`item_id` AS t1_r2, `user_items`.`number` AS t1_r3, `user_items`.`created_at` AS t1_r4, `user_items`.`updated_at` AS t1_r5 FROM `users` LEFT OUTER JOIN `user_items` ON `user_items`.`user_id` = `users`.`id` WHERE (user_items.number IS NULL)
上述のメソッドは、下記のように書き換わりました
def references_eager_loaded_tables? joined_tables = arel.join_sources.map do |join| if join.is_a?(Arel::Nodes::StringJoin) tables_in_string(join.left) else [join.left.table_name, join.left.table_alias] end end joined_tables += [table.name, table.table_alias] # always convert table names to downcase as in Oracle quoted table names are in uppercase joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq (references_values - joined_tables).any? end
変更されているのは、末尾の (references_values - joined_tables).any?
です。
3.2系のときは、(tables_in_string(to_sql) - joined_tables).any?
でした。
これらの問題については、こちらで検討され、修正がされたようです。 includes eager loading is realized with LEFT OUTER JOIN strategy when querying with value contains 'dot' · Issue #7177 · rails/rails
Deprecated
になった、ということでしたが、今回試したところ、エラーになりました。
確認したところ、Rails 4.1
では、使用不可になったようです。
Remove implicit join references that were deprecated in 4.0.
(おまけ 2) Arel を使おう
事例を説明するために、where の中に文字列を書きましたが、sexy ではありません。 綺麗に書きましょう。
it "like name call outer join" do subject = User.includes(:user_items).where(user_items: { number: nil } ).explain expect(subject).to include "LEFT OUTER JOIN" end
(おまけ 3)
使用したテストを置いておきます。
- ruby_test/rails_test/rails3.2/test01/spec/models/user_spec.rb at master · osdakira/ruby_test
- ruby_test/rails_test/rails4.1.0/test01/spec/models/user_spec.rb at master · osdakira/ruby_test
備考1
SQLの確認に、to_sql
ではなくexplain
を使っているのは、Rails 3
系のto_sql
は、eager loading
を考慮しないためです。
Rails 4
系では修正されています。
MUGENUP勉強会を開催しました!
こんにちは、MUGENUPの伊藤です。
今回は先日2/27日に開催した勉強会をまとめてみました!
当日は外部の方を6人お招きし、全部で12人での開催となりました。
開催までの流れ
MUGENUPの開発部では隔週木曜日に勉強会を開催しています。 元々は少人数でそれぞれの人が好きなことを発表するスタイルで、発表中にツッコミとかしながら和気あいあいとやっていました。
また、年末に外部の人を呼んでやったところ好評で、その後定期的に参加してくれるようになった人もいます。
そんなこんなで「本格的に外部の人を呼んでやってみたい!」と思い、元々MUGENUPと御縁のあったエンジニアの方や、知り合いづてなどで何人かに声をかけてみました。
すると皆さん快諾して頂き、晴れてこの勉強会開催にこぎつけました。
会場
今回はMUGENUPの入っているビルにある「シアタールーム」というところで開催しています。 名前の通り映画鑑賞や、カラオケなんかもできちゃう部屋なんですがソファでくつろげたり照明をいじれたりで結構雰囲気を出せるような部屋になっています。
当日の内容
当日は19:30 ~ 21:30の2時間で予定していました。 普段は一人あたり5~10分の発表で、発表中にツッコミを入れたりするので一人あたりだいたい15分くらいでのんびりやっていました。
しかし、段々と参加者が増えていき、「あれ、これそもそも発表時間オーバーするかも」などと思っていたのですが、 最終的には発表者が10名程だったことからなんとかギリギリ全員が発表できました。
さて、気になる当日の発表内容ですが、
vagrant, chef, knife soloを使ってmunin-nodeを複数サーバーに展開し監視をするまで
酔っぱらいレベルを測定できるアプリNomBay
Semantic Versioningについて
psd のファイル構造、または photoshop plugin を作る的な何か
楽しくなるPerl開発環境
RubyのSymbolについて
電子マネーに関する何か
binding.pryに隠れている黒魔術について
といった内容の発表がありました。
各人が好きなことを発表し合った結果、内容がかなりばらつき 普段なら接することはない技術をお互いが知ることができてかなり刺激的な勉強会になりました。
ちなみに私もperlは使ったことがなかったので、その後インストールして色々いじってみました。
社内では「また外部の人を呼んで勉強会をやろう!」という風に盛り上がったので、 また近いうちに開催予定です。
ご興味の有る方は伊藤までご連絡ください!
宣伝
MUGENUP では、知りたがりなエンジニアを募集中です。
無限流開発、ご一緒しませんか?
エントリーはこちら http://mugenup.com/recruit/information#web_application
Rails 4.1 の spring で paralell_tests を使う方法
みなさん、こんにちは! 2週間ぶりのご無沙汰、MUGENUP の osada です。
ruby 2.1.0
、Rails 4.1
で開発した、みなゲー編集部が正式リリースとなりました!
よろしくお願いします。
さて、そんなRails 4.1
の新機能として、プレローダーspring
が標準装備となりました。
本日はspring
とparalell_tests
を併用する方法についてのお話です。
要旨は下記となります。
PARALLEL_TESTS_EXECUTABLE
を設定して、paralell_tests
にspring
を使わせないbin/rspec
を書き換えて、始めのプロセスだけspring
を使うFailuresLogger
を使って、失敗したテストを再実行するRuntimeLogger
を使って、テストのグループを平均化する
では、よろしくお願いします。
PARALLEL_TESTS_EXECUTABLE
を設定して、paralell_tests
にspring
を使わせない
Rails 4.1
のテストで、rspec
では成功するのに、paralell_tests
では多くが失敗する、という現象に遭いました。
spring
が原因と考えられます。
通常、rake
やrspec
コマンドを使うとき、コード(code
)を読み込んで、メモリ上にRails
のapp
インスタンスを生成し、使用します。処理が終わると、app
は解放されます。
この処理はかなり重い処理なので、毎回作るのではなく、一度作ったapp
を使いまわそう、というのがspring
の動きです。spring
で作られたapp
は処理が終わっても維持され、再度使用されます。
このspring
はbin_stub
として提供され、bin/
というディレクトリに
rails
, rspec
, rake
のコマンドが用意されます。例えば、
be bin/rake db:create
のように、通常のコマンドの代わりに、bin/
下のコマンドを呼び出すことで、spring
が使用されます。
一方、paralell_tests
というのは、rake
などの処理を複数のプロセスに分けて同時に処理する方法です。
例えば、parallel:spec
を実行すると、下記の4つのコマンドが実行されます。
※
be
はbundle exec
のalias
です
$ be rake parallel:spec TEST_ENV_NUMBER=;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;rspec TEST_ENV_NUMBER=3;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;rspec TEST_ENV_NUMBER=4;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;rspec TEST_ENV_NUMBER=2;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;rspec
しかし、これにspring
を使用すると、4つのプロセスがあるのに、実際に稼働するapp
は1つになってしまいます。
よって、テストが失敗してしまうのです。
原因はparalell_tests
がbin/rspec
を使用するという点にあります。これをどうにかしましょう。
$ be rake parallel:spec TEST_ENV_NUMBER=;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;bin/rspec ……
結論からいうと、PARALLEL_TESTS_EXECUTABLE
を設定することで、bin/rspec
の使用を回避できます。
PARALLEL_TESTS_EXECUTABLE="bundle exec rspec" be rake parallel:spec TEST_ENV_NUMBER=;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;bundle exec rspec ……
この理由は、parallel_tests
は、実行するファイルを下記のように設定しているからです。
def executable ENV['PARALLEL_TESTS_EXECUTABLE'] || determine_executable end
またrspec
のdetermine_executable
は下記であり、[bin/rspec
, script/spec
, bundle exec rspec
, spec
, rspec
] のどれかを起動する事がわかります。
def determine_executable cmd = case when File.exists?("bin/rspec") "bin/rspec" when File.file?("script/spec") "script/spec" when ParallelTests.bundler_enabled? cmd = (run("bundle show rspec-core") =~ %r{Could not find gem.*} ? "spec" : "rspec") "bundle exec #{cmd}" else %w[spec rspec].detect{|cmd| system "#{cmd} --version > /dev/null 2>&1" } end cmd or raise("Can't find executables rspec or spec") end
bin/rspec
を削除しても良いのですが、それではspring
が使えなくなるので、
PARALLEL_TESTS_EXECUTABLE
を設定する方が適切でしょう。
以上、本項の結論は、
parallel_tests
に、spring
を使わせないためには、PARALLEL_TESTS_EXECUTABLE
を設定する となります。
bin/rspec
を書き換えて、始めのプロセスだけspring
を使う
しかしせっかくのspring
です。
4つの内1つだけでspring
を使い、
残り3つを別のプロセスで動かせば良いのではないでしょうか?
相手はbin_stub
ですから、修正はとても簡単です。
bin/rspec
のこれを、
if !Process.respond_to?(:fork) || Gem::Specification.find_all_by_name("spring").empty? exec "bundle", "exec", "rspec", *ARGV else ARGV.unshift "rspec" load Gem.bin_path("spring", "spring") end
こうしましょう!
if !Process.respond_to?(:fork) || Gem::Specification.find_all_by_name("spring").empty? exec "bundle", "exec", "rspec", *ARGV else if ENV["TEST_ENV_NUMBER"].nil? || ENV["TEST_ENV_NUMBER"].empty? ARGV.unshift "rspec" load Gem.bin_path("spring", "spring") else exec "bundle", "exec", "rspec", *ARGV end end
ENV["TEST_ENV_NUMBER"].nil?
は通常のbin/rspec
起動時です。
ENV["TEST_ENV_NUMBER"].empty?
は、parallel_tests
の一番始めのプロセスの時です。
つまり、
bin/rspec
として起動したとき、または、parallel_tests
の一番始めのプロセスの時に、spring
を使用する。
というbin_stub
になりました。
さて、肝心のスピードアップですが、feature
テストなどが多いため、私のプロジェクトでは違いが現れませんでした。
参考までとはなりますが、環境の影響が少ないspec/model
に適用した結果を載せておきます。
[feature/parallel stash]~/projects/mugenup/workstation: time RAILS_ENV=test PARALLEL_TESTS_EXECUTABLE="bundle exec rspec" bundle exec rake parallel:spec[model] …… 444 examples, 0 failures Took 216.680576 seconds real 3m47.661s user 3m20.585s sys 0m25.249s
[feature/parallel stash]~/projects/mugenup/workstation: time RAILS_ENV=test bundle exec bin/rake parallel:spec[model] …… 444 examples, 0 failures Took 212.844729 seconds real 3m37.395s user 0m1.378s sys 0m0.285s
real
を見ると10 sec
ほど速くなっているように見えますが、誤差の範囲かもしれません。
もし「速くなったよ!」という方がいらっしゃいましたら、ご一報ください!
FailuresLogger
を使って、失敗したテストを再実行する
feature
テストは実際のブラウザの挙動を再現するため、
タイムアウトなどの理由により、
タイミングによって失敗することもあります。
parallel_tests
を使うと、さらに失敗しやすくなります。
たまたま失敗したのか、本当に失敗しているのかを確認するため、
失敗したテストのみを再度実行するのですが、
その時に役立つのが、FailuresLogger
です。
.rspec_parallel
に paralell_tests
のみのオプションを設定できます。
# .rspec_parallel --format progress --format ParallelTests::RSpec::FailuresLogger --out tmp/failing_specs.log
これには失敗したテストがログに残ります。標準出力と同じものです。
ここから、再度テストを実行しましょう。
$ grep rspec tmp/failing_specs.log | awk '{print $2}' | xargs bundle exec rspec /Users/osada/projects/mugenup/workstation/vendor/bundle/gems/rspec-core-2.14.7/lib/rspec/core/configuration.rb:896:in `load': cannot load such file -- /Users/osada/projects/mugenup/workstation/spec/views/projects/edit.html.erb_spec.rb:34 (LoadError)
あら、失敗してしまいました。なぜでしょうか?
実際にファイルを開いてみたところ、ascii color code
が含まれていました。
たしかに、これでは、rspec
が通りませんね。
といっても--color
を消してしまうのも、嬉しくありません。
そこで、正規表現を使って、抜き出すことにしました。
- rspec から始まり、「ドット、スラッシュ、コロン、英数字とアンダースコア」で構成される文字列を抽出する
$ ruby -ne 'puts $1 if /(?<=rspec )([\.\/:\w]+)/' tmp/failing_specs.log | xargs bundle exec rspec
これで、失敗したテストのみ、再実行することができます。
rake task
にするべきか?とも思ったのですが、さらに実行を遅らせてしまいそうなので、
alias
の登録だけにしておきました。
こうゆうとき、どういう技を使えばいいか、わからないの
ruby
でワンライナーを書くのは、少し大変ですね。
RuntimeLogger
を使って、テストのグループを平均化する
さて、paralell_tests
はデフォルトでは、ファイルサイズ順に均等化します。
ファイルサイズの大きい順に並べて、4つのグループに順番に追加していくわけです。
RuntimeLogger
を使うことで、テストにかかった時間で、均等化することができます。
--format progress --format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log
tmp/parallel_runtime_rspec.log
には、テストファイル名と、その実行時間が記録されています。
[feature/parallel stash]~/projects/mugenup/workstation: tail tmp/parallel_runtime_rspec.log …… ./spec/views/sub_projects/show.html.erb_spec.rb:2.261982 ./spec/views/projects/show.html.erb_spec.rb:2.806851
テストのグループ分けのとき、with_runtime_info
メソッドが呼ばれます。
runtime_log
つまりtmp/parallel_runtime_rspec
があるとき、かつ、
今回のtests
ファイル群が、このログの中に含まれていると考えられるときに、
このログを使ってグループ分けします。
puts "Using recorded test runtime"
が出力されれば、成功です。
def with_runtime_info(tests) lines = File.read(runtime_log).split("\n") rescue [] # use recorded test runtime if we got enough data if lines.size * 1.5 > tests.size puts "Using recorded test runtime" times = Hash.new(1) lines.each do |line| test, time = line.split(":") next unless test and time times[File.expand_path(test)] = time.to_f end tests.sort.map{|test| [test, times[File.expand_path(test)]] } else # use file sizes tests.sort.map{|test| [test, File.stat(test).size] } end end
実際に、使用したときと、使用していないときを比較してみます。
[feature/parallel stash]~/projects/mugenup/workstation: RAILS_ENV=test bundle exec bin/rake parallel:spec[model] Finished in 2 minutes 19.6 seconds 78 examples, 0 failures Finished in 3 minutes 19.1 seconds 124 examples, 0 failures Finished in 3 minutes 10.7 seconds 116 examples, 0 failures Finished in 3 minutes 13.1 seconds 126 examples, 0 failures
使用していないときの処理時間は2:19.6 〜 3:19.1
であり、60
秒以上のズレがあります。
一方、runtime_log
を使用したときは下記です。
Using recorded test runtime
が表示されていることが確認できます。
[feature/parallel stash]~/projects/mugenup/workstation: RAILS_ENV=test bundle exec bin/rake parallel:spec[model] Using recorded test runtime Finished in 3 minutes 7.8 seconds 102 examples, 0 failures Finished in 2 minutes 58.3 seconds 108 examples, 0 failures Finished in 3 minutes 10.2 seconds 123 examples, 0 failures Finished in 3 minutes 11.8 seconds 111 examples, 0 failures
2:58.3 〜 3:11.8
であり、13
秒ほどのズレでした。
残念ながら、こちらも大きなスピードアップは感じられませんでしたが、 速くなったよ、という方はご一報いただけると嬉しいです。
まとめ
parallel_tests
とspring
の使い方、いかがだったでしょうか?
- PARALLEL_TESTS_EXECUTABLE="bundle exec rspec" を設定して、
paralell_tests
にspring
を使わせないbin/rspec
を書き換えて、始めのプロセスだけspring
を使うFailuresLogger
を使って、失敗したテストを再実行するRuntimeLogger
を使って、テストのグループを平均化する
弊社でもテストが遅いということが、問題になっており、日々改善に勤しんでおります。
こんなやり方あるよ!という情報をお持ちの方、いらっしゃいましたら、 ご教授いただけますと幸いです。
よろしくお願いします!
宣伝
MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly
【探検】Railsカラム更新のメソッド1
初めに
皆さん、初めまして。そして、明けましておめでとうございます。株式会社MUGENUP 開発部の奥田です。 今回、初めて技術ブログを書くことになりました。よろしくお願い致します。
何回かに分けてRailsのカラム更新メソッドについて書いていき、今回はupdate_attribute
とupdate_attributes
について記述します。2つのメソッドは私の環境だとactiverecord-3.2.16/lib/active_record/persistence.rb
にありました。以下の表は簡単なまとめです。
メソッド名 | validation | callback | 更新カラム数 |
---|---|---|---|
update_attribute |
なし | あり | 1つだけ |
update_attributes |
あり | あり | 複数 |
それではそれぞれのメソッドのソースコードを探検してみましょう〜!!
ActiveRecord::Persistence#update_attribute
# Updates a single attribute and saves the record. # This is especially useful for boolean flags on existing records. Also note that # # * Validation is skipped. # * Callbacks are invoked. # * updated_at/updated_on column is updated if that column is available. # * Updates all the attributes that are dirty in this object. # def update_attribute(name, value) name = name.to_s raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) send("#{name}=", value) save(:validate => false) end
save(:validate => false)
となっているので、validationなしということがわかります。Callbackを起こすは英語でinvokeと表現するのですね〜。updated_at
またはupdated_on
は更新するとわざわざ記述しているので、更新しないメソッドもあるのかもしれません。驚いたのはsend("#{name}=", value)
の部分で、sendメソッドはカラム名=
をメソッドと認識することです。sendメソッドを深堀りするのも面白そうだと思いました。
ActiveRecord::Persistence#update_attributes
# Updates the attributes of the model from the passed-in hash and saves the # record, all wrapped in a transaction. If the object is invalid, the saving # will fail and false will be returned. # # When updating model attributes, mass-assignment security protection is respected. # If no +:as+ option is supplied then the +:default+ role will be used. # If you want to bypass the protection given by +attr_protected+ and # +attr_accessible+ then you can do so using the +:without_protection+ option. # def update_attributes(attributes, options = {}) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do self.assign_attributes(attributes, options) save end end
update_attributes
にはoptions
があるんですね〜。:without_protection
とoptions
に入れることでmass-assignmentのprotectionチェックなしにできることを初めて知りました。assign_attributes
の中も見たい気持ちはありますが、今後のネタとして取っておきます(笑)。コメントアウトで強調したいところを+
で囲っていますが、私の知らない記法でした。Markdown記法ではないようなので、要調査です。
番外編 ActiveRecord::Persistence#update_attributes!
#update_attributes
はsave
を呼んでいるのでカラム更新の失敗時に戻り値としてfalse
を返します。そして、#update_attribute
に!
をつけた#update_attributes!
の場合はカラム更新の失敗時に例外を返しますが、その中身はどうなっているのでしょう?save
が呼ばれて、!
がついてるということは・・・?
# Updates its receiver just like +update_attributes+ but calls <tt>save!</tt> instead # of +save+, so an exception is raised if the record is invalid. def update_attributes!(attributes, options = {}) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do self.assign_attributes(attributes, options) save! end end
予想通りですが、save!
が呼ばれていました〜。
最後に
簡単にではありましたが、Railsのカラム更新メソッドについて見てきました。この記事を読んで初めて知ったことがあれば幸いです。他にもカラム更新のメソッドはあるので、私の次回の記事でもカラム更新のメソッドを探検したいと思います。
宣伝
MUGENUP では、Rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly
【拡張】論理和できるenumを書いてみた【gem】
新年あけましておめでとうございます! MUGENUP の osada です。 2014 年は挑戦の年、ということで、MUGENUP でも新しい事業を初めています。
みんなで作るゲーマー向けの攻略サイト「みなゲー」。 その攻略記事を書いてくださる方を募集しています。 スマホゲームなら俺に任せろ!という豪の方、攻略記事を書いてみませんか?
開発部でも、新しい挑戦としてRuby 2.1
、Rails 4.1
で開発を初めています!
新機能 enum
Rails 4.1 に enum という新機能が実装されました。
Ruby on Rails 4.1 Release Notes — Ruby on Rails Guides
class Conversation < ActiveRecord::Base enum status: [ :active, :archived ] end conversation.archived! conversation.active? # => false conversation.status # => "archived" Conversation.archived # => Relation for all archived Conversations
status など、「特定のグループの中のどれか1つの値を持つ要素」を便利に書けるようになりました。
enum では プレイングマネージャー を表現できない
enum はグループの間でどれか一つの要素を表現します。各要素は排他関係です。:active
と :archived
が同時に存在することはありません。
そう聞くと、グループの複数の値が共存して欲しい。 そう思うことはないでしょうか?
例えば、野球で、監督兼選手は、プレイングマネージャーと呼ばれるそうです。
class User < ActiveRecord::Base enum role: [ :manager, :player ] end
しかしこのとき、enum ではプレイングマネージャーを表現できません。User
がなれるのは、manager(監督)か、player(選手)しかないからです。
論理和できる列挙型、bitwise_enum を作ってみた
bitwise_enum/lib/bitwise_enum.rb at master · osdakira/bitwise_enum
とうことで複数の要素を表現できる、bitwise_enum
を作ってみました。
通常のenum
は、要素を[0, 1, 2]
と表現していくため、
manager
は 0
、palyer
には 1
が入ります。
一方、bitwise_enum
では、manager
は1
、player
には2
が入ります。
プレイングマネージャーを表現したいときは3
にすればOK、ということですね。
これはビット(2進数)に直すと、1桁目がmanager
、2桁目がplayer
を表現していることになります。
このようにすることで、複数の要素を同時に表現することが可能です。
使い方
class User < ActiveRecord::Base bitwise_enum role: [ :manager, :player ] end
基本的な使い方はenum
と同様です。
user = User.new user.manager! user.manager? # => true user.role # => "['manager']" user = User.new user.player! user.player? # => true user.role # => "['player']"
違いとしては、要素が共存できるということです。
user.manager! user.manager? # => true user.player! user.player? # => true user.role # => "['manager', 'player']"
プレイングマネージャーが表現出来ました!
一方で、今までは要素は上書きしていましたが、
bitwise_enum
では、明示的に削除する必要があります。
そのためにnot_xx()
というメソッドがあります。
user.manager! user.manager? # => true user.not_manager! user.manager? # => false
また、全てをリセットするためのreset_xx()
というメソッドがあります。
user.manager! # => ['manager'] user.reset_role # => nil user.role = []
enum
の実装も同じなのですが、manager!
を呼んだ時点で、
update!
が動いているため、即座にSQLが動いてしまいます。
このままでは error ハンドリングするときに困るので、代入するときにもビット演算を行うようになっています。
user.role = :manager user.role # => ['manager'] user.role = :admin user.role # => ['manager', 'admin']
scope も、通常の値ではなく、ビット演算の値でSQLを発行します。
User.manager # => SELECT `users`.* FROM `users` WHERE (role & 1 = 1)
ただコレに関しでは実装部分が、arel
を使わない未熟なものです。
klass.scope value, -> { klass.where("#{name} & #{bit} = #{bit}") }
どなたかarel
の使い方について、情報を教えていただけないでしょうか!
以上が、プレイングマネージャーが実装できるbitwise_enum
の説明です。
gem にしてみた
せっかくなので gem にしてみました。
gem を作るのは簡単でしたが、テスト環境の構築に迷いました。rails に載せずに環境を作るのは難しいですね。
まとめ
- 列挙した要素の値を、重複して持つことができる
bitwise_enum
を実装しました。 - enum のコードにビット演算を追加して実現しています。
皆さんはenum
をどう使われているでしょうか?是非使い方を教えて下さい!
宣伝
MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly
【悲報】ActiveSupport::Concern の ClassMethod はモジュールメソッドになる件
皆さんこんにちは!「太鼓式マッサージ?面白そう!」と思ったら「タイ古式マッサージ」でした。MUGENUP の osada です。名前って難しいですよね。
ところで、ActiveSupport::Concern
の ClassMethod
で定義したクラス変数が、どこにあるか、ご存知ですか?
# app/models/concern/item_module.rb module ItemModule extend ActiveSupport::Concern module ClassMethods def my_module_method @@concern_class_variable ||= "concern_class_variable" end end end
# app/models/item.rb class Item < ActiveRecord::Base include ItemModule end
# spec/models/item_spec.rb describe Item do it "ClassMethods で定義すると参照できる" do expect(Item.my_module_method).to eq "concern_class_variable" end end
ここまではOKだと思います。 では、これを自身のクラス変数として参照できないことをご存知でしょうか?
it "ClassMethods で取り込んだクラスメソッドを実行しても自身のクラス変数としては定義されない" do expect{ Item.my_module_method }.not_to change{Item.class_variable_defined?(:@@concern_class_variable)}.from(false) end
参照はできるのに、定義されていない。それは一体どこにあるの?というのが今回のお話です。
クラス変数はどこに定義されているの?
ruby の場合、クラスメソッドはクラスに特異メソッドを追加する、という文脈で定義されます。
Ruby におけるクラスメソッドとはクラスの特異メソッドのことです。 したがって、何らかの方法でクラスオブジェクトにメソッドを定義すれば、そ れがクラスメソッドとなります。 http://docs.ruby-lang.org/ja/2.0.0/doc/spec=2fdef.html#class_method
よって、クラスメソッド内で定義されたクラス変数は、自身のクラス変数となります。
class Item < ActiveRecord::Base include ItemModule class << self def my_class_methods @@my_class_variable = "my_class_variable" end end end
it "クラスメソッドを実行するとクラス変数が定義される" do expect{ Item.my_class_methods }.to change{Item.class_variable_defined?(:@@my_class_variable)}.from(false).to(true) end
これは全く期待通りの動きです。先ほどと違うのは、自身のクラスメソッドとして定義しているという点ですね。 では、class定義ならOKで、module だからダメなんでしょうか?
module の included として定義してみる
ActiveSupport::Concern
では included
というメソッドで、クラス定義の中でコードを評価することができます。
module ItemModule extend ActiveSupport::Concern included do class << self def my_singleton_module_method @@singleton_include_module_method = "singleton_include_module_method" end end end end
この結果はクラス変数はクラスに紐付きました。期待通りです。
it "included で 拡張した特異クラスのクラスメソッドはクラス変数が定義される" do expect{ Item.my_singleton_module_method }.to change{Item.class_variable_defined?(:@@singleton_include_module_method)}.from(false).to(true) end
どうやら、module だからではないようですね。違いは、included
とClassMethod
なので、期待通りに動かないのはClassMethod
のせいのようです。
ClassMethod
とincluded
の違いはどこにあるのか?
ClassMethod
はどのように動くのでしょうか?
base.extend const_get("ClassMethods") if const_defined?("ClassMethods")
ClassMethod
という定義があったら、base
に対してextend
するという実装になっています。
一方includes
は、
base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block")
def included(base = nil, &block) if base.nil? @_included_block = block else super end end
base クラスのclass_eval
を呼び出しているため、自身のコンテキストで評価されます。
違いはClassMethod
はモジュールのextend
であり、included
はクラス定義内での評価というところです。
引数で指定したモジュールのインスタンスメソッドを self の特異 メソッドとして追加します。 instance method Object#extend
extend されたクラス変数はモジュール変数となる
extend
はモジュールのメソッドを、特異クラスのメソッドとして追加するだけです。よって、extend
で追加されたクラスメソッド内で定義されたクラス変数は、モジュール変数になってしまうのです!!!
it "ClassMethods で定義したクラスメソッドは、モジュール変数" do expect{ Item.my_module_method }.to change{ItemModule::ClassMethods.class_variable_defined?(:@@concern_class_variable)}.from(false).to(true) end
そしてさらに恐ろしいことに、モジュール変数はinclude
した全てで共有されます。マニュアルにしっかり書かれています。
モジュールで定義されたクラス変数(モジュール変数)は、そのモジュールをイ ンクルードしたクラス間でも共有されます。 http://docs.ruby-lang.org/ja/2.0.0/doc/spec=2fvariables.html#class
module Foo @@foo = 1 end class Bar include Foo p @@foo += 1 # => 2 end class Baz include Foo p @@foo += 1 # => 3 end
ClassMethod はモジュールメソッドだ
名前って難しいですよね。ClassMethod
だと思っていたら(そして実際にクラスメソッドなのですが)、その中で定義したクラス変数は、モジュール変数になってしまうのです。
これからはできるだけ ClassMethod
は使わないようにしよう、と心に決めた日でした。
[注意] モジュール変数、クラス変数、クラスメソッドの役割について
モジュール変数を定義するべきではない、というのはよく言われていることだと思います。そもそもモジュールは数学的な意味で関数であるべきだというのが私の持論です。副作用がないと言い換えても良いです。
なので、モジュール内でクラス変数を使うべきではないというご指摘は最もだと思います。クラス変数を使わない方が良いというのも納得できます。
本記事は、クラス変数やクラスメソッドの使用を推奨したものではありません。あくまで、知っておくと罠が回避できるかも、という記事ですので、ご了承のほどよろしくお願い致します。
参考
» ActiveSupport::Concern でハッピーなモジュールライフを送る TECHSCORE BLOG
宣伝
MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly