Ruby 言語でよく引き合いに出されるのは、その柔軟性です。Dick Sites が言うように、Ruby を使えば「プログラムを作成するためのプログラムを作成」することができます。Ruby on Rails はコアとなる Ruby 言語を拡張するものの、その拡張性を可能にしているのは Ruby そのものです。Ruby 言語の柔軟性を利用する Ruby on Rails では、ボイラープレート・コードをあまり使わずに、また追加でコードを作成することもそれほどなく、極めて構造化されたプログラムを簡単に作成することができます。つまり一般的な振る舞いをするプログラムを作成する場合には、そのほとんどで追加の作業が必要ありません。このように手をかけずに作成できるプログラムの振る舞いが常に完璧であるとは限らないものの、数々の優れたアーキテクチャーを簡単な作業で実現することができます。
例えば、Ruby on Rails はモデル・ビュー・コントローラー (MVC) パターンをベースにしていることから、大半の Rails アプリケーションは 3 つの構成部分にはっきりと分かれています。このうち、アプリケーションのデータを管理するために必要な振る舞いが含まれるのはモデルの部分です。一般的に、Ruby on Rails アプリケーションではモデルとデータベース・テーブルの間に 1 対 1 の関係があります。モデルのデータベース操作を管理するのは、Ruby on Rails がデフォルトで使用する ActiveRecord
というオブジェクト・リレーション・マッピング (ORM) です。つまり、平均的な Ruby on Rails プログラムに SQL コードがあったとしても、その量は極めて限られています。ビューの部分には、ユーザーに送信される出力を作成するコードが含まれます。このコードは通常、HTML、JavaScript などで構成されます。そしてコントローラーが、ユーザーからの入力を正しいモデルの呼び出しに変換し、適切なビューを使用してレスポンスをレンダリングします。
Rails の支持者たちは、Rails が使いやすくなっている理由としてこの MVC パラダイムを (Ruby と Rails 両方の利点とともに) 挙げ、Rails ではより少人数のプログラマーでより多くの機能をより短時間に生み出すことができると主張します。これは、ソフトウェア開発費あたりのビジネス価値が増大することに他なりません。このことから、Ruby on Rails 開発がさらに大きな人気を博したというわけです。
けれども、初期開発費だけがすべてではありません。他にも保守にかかる費用、アプリケーションを実行するためのハードウェアの費用など、継続的に必要となってくる費用があります。Ruby on Rails 開発者は多くの場合、テストやその他のアジャイル開発手法を使って保守にかかる費用を抑えていますが、膨大な量のデータを使用する Rails アプリケーションを効率的に実行することに関しては見逃されがちです。Rails はデータベースへのアクセスを容易にするものの、必ずしも効率的にアクセスしているというわけではありません。
Rails アプリケーションは、いくつかの根本的な理由から実行に時間がかかる場合があります。理由の 1 つは単純なもので、Rails では開発者が迅速に開発できるようにすることを前提としているからです。通常はこれらの前提は正しく、有効に働きますが、パフォーマンス面でメリットがあるとは限りません。場合によっては、リソースを非効率的に使用する結果になることもあります。これは特に、データベース・リソースに関して言えることです。
例えばデフォルトでは、ActiveRecord
は SELECT *
に相当する SQL 文を使用して、すべてのフィールドをクエリーで選択します。大きなサイズの VARCHAR
や BLOB
フィールドが含まれている場合は尚更のこと、列の数がたくさんあるとしたら、この振る舞いはメモリー使用量とパフォーマンスという点で大きな問題になってしまいます。
もう 1 つの重大な問題は、この記事で詳しく検討する N+1 問題です。N+1 問題は基本的に、1 つの大きなクエリーではなく、小さなクエリーが数多く実行されるという結果をもたらします。ActiveRecord
には、例えば親レコードの各セットに対して子レコードが要求されていることを知る手段はありません。そのため、親レコードのそれぞれに対して 1 つの子レコードのクエリーを生成します。クエリーごとのオーバーヘッドにより、この振る舞いは極めて重大なパフォーマンス問題を引き起こす可能性があります。
さらに、Ruby on Rails 開発者の開発スタイルと開発スタンスに密接に関連する問題もあります。ActiveRecord
は多くのタスクを極めて簡単に行えるようにするため、Rails 開発者は「反 SQL」のスタンスを取りがちです。このようなスタンスの Rails 開発者は、SQL のほうが理にかなっているとしても、SQL を使いたがりません。けれども、大量の ActiveRecord
オブジェクトを作成して操作するとなると、必然的に実行速度は低下します。場合によってはオブジェクトをまったくインスタンス化しない SQL クエリーをそのまま作成したほうが、実行速度が遥かに向上する結果となります。
Ruby on Rails は多くの場合、開発チームの規模を縮小するために使用されます。そして、本番環境でのアプリケーションのデプロイメントおよび保守に必要なシステム管理タスクの一部は、Ruby on Rails 開発者が行うのが通例です。この 2 つの理由から、Ruby on Rails 開発者が本番環境について十分に理解していないと、問題が発生する可能性があります。その一例は、オペレーティング・システムやデータベースが正しく設定されないことです。MySQL の my.cnf が最適な設定になっていないにも関わらず、このデフォルト設定が Ruby on Rails のデプロイメントでそのまま使われていることがよくあります。さらに、パフォーマンスを長期的に向上させるための監視ツールやベンチマーク・ツールが十分に揃えられていないことも珍しくありません。もちろん、Ruby on Rails 開発者を非難しているのではなく、単に、専門知識を持っていないと起こり得る問題だと言っているのです。場合によっては、Rails 開発者が開発環境および本番環境の両方でのエキスパートであることもあります。
最後に挙げる問題は、Ruby on Rails はプログラマーにローカル環境で開発を行うことを推奨している点です。ローカル環境での開発にはさまざまなメリットがあります。例えば、開発の遅延が少なくなり、ディストリビューション数の増大につながります。けれどもこれは同時に、ワークステーションの規模が小さいことから、限られたデータ・セットでしか作業できないことを意味します。開発者の開発方法と、コードが実際にデプロイされる場所との違いは、大きな問題になり得ます。負荷の少ないローカル・サーバーで小さなデータ・サイズの処理を長時間行ってパフォーマンスに優れているとしても、負荷の大きなサーバーで大きなデータ・サイズの処理を行えば、途端にアプリケーションに大きなパフォーマンス問題があることが発覚するだけです。
もちろん上記の他にも、Rails アプリケーションのパフォーマンス問題には多くの原因があります。Rails アプリケーションの潜在的なパフォーマンス問題を見つける最善の方法は、正確で繰り返し測定できる診断ツールに目を向けることです。
パフォーマンス問題を検出するための最高のツールの 1 つとなるのは、Rails 開発ログです。各開発マシンの log/development.log ファイルにある このログには、リクエストに応答するまでの合計所要時間、データベースで費やされた時間のパーセンテージ、ビューを生成するのに費やされた時間のパーセンテージなど、さまざまなメトリックの総計があります。このログは、development-log-analyzer
などのツールを使用して自動的に分析することができます。
本番環境では、mysql_slow_log
を調べることによって貴重な情報を見つけることができます。このログについての詳しい説明はこの記事ではしませんが、「参考文献」セクションに詳細を調べられるリンクを記載してあります。
特に強力で有用なツールとして挙げられるのは、query_reviewer
プラグインです (「参考文献」を参照)。このプラグインは、ページで実行されているクエリーの数、ページの生成に費やされた時間を示すだけでなく、ActiveRecord
が生成する SQL コードを自動的に分析して潜在的な問題を見つけてくれます。例えば、MySQL インデックスを使用していないクエリーなどです。したがって、重要な列にインデックスを付け忘れたためにパフォーマンス問題が発生しているとしても、このことを簡単に突き止めることができます (MySQL インデックスについての詳細は、「参考文献」を参照してください)。このプラグインはこうしたすべての情報を、ポップアップ表示される <div>
に含めて表示しますが、この <div> は開発モードでのみ表示されます。
最後に、Firebug、yslow
、Ping、tracert
などのツールを使用して、パフォーマンス問題の原因がネットワークにあるのか、それともアセットのロード問題にあるのかを突き止めることも忘れないでください。
次のセクションでは、Rails に特有のパフォーマンス問題とそのソリューションに取り組みます。
N+1 クエリー問題は、Rails アプリケーションに伴う最大の問題の 1 つです。例えば、リスト 1 のコードが生成するクエリーの数がわかりますか?このコードは、一時的に作成した posts テーブル全体を単純にループ処理して、投稿のカテゴリーと本文を表示するコードです。
リスト 1. 最適化される前の Post.all コード
<%@posts = Post.all(@posts).each do |p|%> <h1><%=p.category.name%></h1> <p><%=p.body%></p> <%end%> |
答え: このコードは 1 つのクエリーを生成するのに加え、@posts
に含まれる行ごとに 1 つのクエリーを生成します。クエリーごとのオーバーヘッドにより、このコードは極めて重大な問題となる可能性があります。問題の原因は、p.category.name
の呼び出しです。この呼び出しは @posts
配列全体ではなく、この特定の投稿オブジェクトにしか適用されません。幸い、この問題は Eager Loading という手段によって解決することができます。
Eager Loading とは、Rails が自動的に、指定された子オブジェクトのオブジェクトをロードするために必要なクエリーを行うという意味です。Rails はその際、JOIN
SQL 文や、複数のクエリーが実行されるストラテジーを使用しますが、使用する予定のすべての子オブジェクトを指定すれば、ループの繰り返し処理ごとにクエリーが追加で生成されるという N+1 の状況に陥ることはありません。N+1 問題を回避するためにリスト 1 のコードで Eager Loading を使用した場合、リスト 2 に記載するコードになります。
リスト 2. Eager Loading による最適化後の Post.all コード
<%@posts = Post.find(:all, :include=>[:category] @posts.each do |p|%> <h1><%=p.category.name%></h1> <p><%=p.body%></p> <%end%> |
上記のコードは、posts テーブルにいくつの行があったとしても、最大で 2 つのクエリーしか生成しません。
当然のことながら、すべてがすべて、このように単純なわけではありません。さらに複雑な N+1 クエリー問題に対処するには、さらに大きな労力を要します。それだけの労力をかける価値はあるのでしょうか。そこで、簡単なテストを行ってみます。
リスト 3 のスクリプトを使用して、クエリーの実行にどれだけ時間がかかるか (あるいはかからないか) を調べることができます。リスト 3 は、スタンドアロンのスクリプトで ActiveRecord
を使用してデータベース接続を確立し、テーブルを定義し、データをロードする例です。このスクリプトを実行した後、Ruby の組み込みベンチマーク・ライブラリーを使って、どの手法が速度に優れているのか、そして速度にどれだけの差があるのかを確認することができます。
リスト 3. Eager Loading のベンチマーク・スクリプト
require 'rubygems' require 'faker' require 'active_record' require 'benchmark' # This call creates a connection to our database. ActiveRecord::Base.establish_connection( :adapter => "mysql", :host => "127.0.0.1", :username => "root", # Note that while this is the default setting for MySQL, :password => "", # a properly secured system will have a different MySQL # username and password, and if so, you'll need to # change these settings. :database => "test") # First, set up our database... class Category < ActiveRecord::Base end unless Category.table_exists? ActiveRecord::Schema.define do create_table :categories do |t| t.column :name, :string end end end Category.create(:name=>'Sara Campbell\'s Stuff') Category.create(:name=>'Jake Moran\'s Possessions') Category.create(:name=>'Josh\'s Items') number_of_categories = Category.count class Item < ActiveRecord::Base belongs_to :category end # If the table doesn't exist, we'll create it. unless Item.table_exists? ActiveRecord::Schema.define do create_table :items do |t| t.column :name, :string t.column :category_id, :integer end end end puts "Loading data..." item_count = Item.count item_table_size = 10000 if item_count < item_table_size (item_table_size - item_count).times do Item.create!(:name=>Faker.name, :category_id=>(1+rand(number_of_categories.to_i))) end end puts "Running tests..." Benchmark.bm do |x| [100,1000,10000].each do |size| x.report "size:#{size}, with n+1 problem" do @items=Item.find(:all, :limit=>size) @items.each do |i| i.category end end x.report "size:#{size}, with :include" do @items=Item.find(:all, :include=>:category, :limit=>size) @items.each do |i| i.category end end end end |
このスクリプトは、:include
節を使用した Eager Loading を使った場合と、使わなかった場合とで、100、1,000、および 10,000 のオブジェクトのループ処理速度をそれぞれテストします。このスクリプトを実行するには、スクリプトの先頭近くにあるデータベース接続パラメーターを、使用しているローカル環境に応じたパラメーターに置き換えなければならない場合があります。また、test という名前の MySQL データベースを作成することも必要です。さらに、ActiveRecord
および fakerr
gem も必要になるので、gem install activerecord faker
を実行して取得してください。
私のマシンでこのスクリプトを実行したところ、リスト 4 の結果となりました。
リスト 4. Eager Loading のベンチマーク・スクリプト出力
-- create_table(:categories) -> 0.1327s -- create_table(:items) -> 0.1215s Loading data... Running tests... user system total real size:100, with n+1 problem 0.030000 0.000000 0.030000 ( 0.045996) size:100, with :include 0.010000 0.000000 0.010000 ( 0.009164) size:1000, with n+1 problem 0.260000 0.040000 0.300000 ( 0.346721) size:1000, with :include 0.060000 0.010000 0.070000 ( 0.076739) size:10000, with n+1 problem 3.110000 0.380000 3.490000 ( 3.935518) size:10000, with :include 0.470000 0.080000 0.550000 ( 0.573861) |
すべてのパターンのなかで最も高速だったのは、:include
を使用したテストです。具体的には、テストしたそれぞれのオブジェクト数で 5.02 倍、4.52 倍、6.86 倍速くなっています。正確な結果は具体的な状況に依存することは言うまでもありませんが、Eager Loading が顕著なパフォーマンス向上をもたらすことは確かです。
ネストされた関係、つまり関係の関係を参照する必要がある場合を考えてみてください。リスト 5 は、ネストされた関係を参照する一般的な例です。このコードではすべての投稿をループ処理して、投稿した人のアイコンを表示します。ここで、Author
は Image
に対して belongs_to
の関係を持ちます。
リスト 5. ネストされた Eager Loading の使用事例
@posts = Post.all @posts.each do |p| <h1><%=p.category.name%></h1> <%=image_tag p.author.image.public_filename %> <p><%=p.body%> <%end%> |
このコードには前と同じように N+1 問題がありますが、関係の関係を使用しているため、問題を修正するための構文はすぐにはわかりません。それでは、ネストされた関係にはどのように Eager Loading を適用するのでしょうか。
これに対する正しい答えは、:include
節にハッシュ構文を使用することです。リスト 6 に、ハッシュを使用して、ネストされた Eager Loading を行う例を示します。
リスト 6. ネストされた Eager Loading のソリューション
@posts = Post.find(:all, :include=>{ :category=>[], :author=>{ :image=>[]}} ) @posts.each do |p| <h1><%=p.category.name%></h1> <%=image_tag p.author.image.public_filename %> <p><%=p.body%> <%end%> |
上記のように、ハッシュおよび配列リテラルをネストすることができます。この場合のハッシュと配列の違いは、ハッシュではサブ項目をネストできる一方、配列にはそれが不可能であるという点だけです。この点を除けば、どちらも同じです。
N+1 問題のインスタンスのなかには、簡単に認識できないものもあります。例えば、リスト 7 はいくつのクエリーを生成するでしょうか。
リスト 7. 間接的な Eager Loading の使用事例
<%@user = User.find(5) @user.posts.each do |p|%> <%=render :partial=>'posts/summary', :locals=>:post=>p %> <%end%> |
クエリーの数を判断するには当然、posts/summary
の部分に関する知識が必要です。リスト 8 に、この部分を記載します。
リスト 8. 間接的な Eager Loading の部分: posts/_summary.html.erb
<h1><%=post.user.name%></h1> |
残念ながら、リスト 7 とリスト 8 は、ActiveRecord
がすでにメモリー内にある User
オブジェクトから自動的に post
オブジェクトを生成したにも関わらず、post
の行ごとに追加でクエリーを作成して、ユーザーの名前を検索します。要するに、このままの状態では、Rails は子レコードをそれぞれの親に関連付けません。
この問題を解決するには、自己参照 Eager Loading を使用します。基本的に、Rails は親レコードによって生成された子レコードをリロードします。そのため、親と子が全く関係していないかのように、親レコードの Eager Loading を行う必要があるというわけです。この場合のコードは、リスト 9 のようになります。
リスト 9. 間接的な Eager Loading のソリューション
<%@user = User.find(5, :include=>{:posts=>[:user]}) ...snip... |
直観には反しているものの、この手法は前の手法とほとんど同じように機能します。けれどもこの手法を使うと、階層が複雑な場合をはじめ、過剰にネストしてしまいがちです。リスト 9 に記載したような単純な使用事例であれば問題ありませんが、過剰なネストは問題の原因になることがあります。場合によっては、Ruby オブジェクトを必要以上にロードすることによって、N+1 問題に対処した場合よりも時間がかかるようになるという事態も考えられます。これは特に、すべてのオブジェクトでツリー全体がトラバースされない場合に当てはまります。そのような場合には、N+1 問題に対する別のソリューションを使ったほうが適切かもしれません。
そうしたソリューションの 1 つが、キャッシング手法を使用したソリューションです。Rails V2.1 には単純なキャッシュ・アクセスが組み込まれています。Rails.cache.read
や Rails.cache.write
、および関連メソッドを使用すれば、独自の単純なキャッシュ・メカニズムを簡単に作成できるため、バックエンドを単純なメモリー・バック・エンド、ファイル・ベースのバックエンド、または memcached サーバーにすることが可能です。Rails に組み込まれているキャッシング・サポートについての詳細は、「参考文献」セクションを参照してください。ただし、独自のキャッシング・ソリューションを作成するまでのことはありません。事前ビルドされた Rails プラグインとして、Nick Kallen のcache money
プラグインなどを使用することができます。ライトスルー・キャッシュを行うこのプラグインは、Twitter で使用されているコードをベースとしています。詳細については「参考文献」を参照してください。
当然のことながら、クエリーの数とは関係のない Rails 問題もあります。
直面する可能性がある問題の 1 つは、データベースで行うべき処理を Ruby で行うことによって発生します。これは、Ruby がいかに強力であるかの証しです。人々が大きな動機もなしにデータベース・コードの一部を進んで C 言語で再実装するのは想像し難いことですが、Rails を使って同じような計算を ActiveRecord
オブジェクトのグループで計算するのはわけありません。けれども残念ながら、Ruby はデータベース・コードよりも遅いのが常です。したがって、リスト 10 に記載するような純粋な Ruby 手法を使って計算することは避けてください。
リスト 10. 誤ったグループ計算の実行方法
all_ages = Person.find(:all).group_by(&:age).keys.uniq oldest_age = Person.find(:all).max |
Rails では代わりに、一連のグループ関数と集約関数を用意しています。これらの関数は、リスト 11 に記載するように使用します。
リスト 11. 正しいグループ計算の実行方法
all_ages = Person.find(:all, :group=>[:age]) oldest_age = Person.calcuate(:max, :age) |
ActiveRecord::Base#find
には、SQL を真似るために使えるオプションがいくつもあります。詳細については、Rails のドキュメントを見るとわかります。注目すべき点は、calculate
メソッドは、データベースがサポートする有効なあらゆる集約関数 (:min
、:sum
、:avg
など) と一緒に使えることです。その上、calculate は :conditions
などの多数の引数を取ることもできます。Rails のドキュメントで詳細を調べてください。
けれども SQL で実行できることはすべて Rails でも実行できるとは限りません。組み込み SQL で必要を満たせない場合には、カスタム SQL を使用してください。
人と、その人の職業、年齢、そして過去 1 年間に関係した事故の件数をリストアップしたテーブルがあるとします。この情報を取得するには、リスト 12 に記載するカスタムの SQL 文を使用することができます。
リスト 12.
ActiveRecord
を使用したカスタム SQL の例sql = "SELECT profession, AVG(age) as average_age, AVG(accident_count) FROM persons GROUP BY profession" Person.find_by_sql(sql).each do |row| puts "#{row.profession}, " << "avg. age: #{row.average_age}, " << "avg. accidents: #{row.average_accident_count}" end |
このスクリプトによって、リスト 13 のような結果が出力されます。
リスト 13.
ActiveRecord
を使用したカスタム SQL の出力Programmer, avg. age: 18.010, avg. accidents: 9 System Administrator, avg. age: 22.720, avg. accidents: 8 |
もちろんこれは単純な例ですが、この例をどんなに複雑な SQL 文にでも拡張可能であることは想像できるはずです。また、例えば ALTER TABLE
文といった別のタイプの SQL 文でも、ActiveRecord::Base.connection.execute
メソッドを使用して実行することができます (リスト 14 を参照) 。
リスト 14. ActiveRecord を使用した検索以外のカスタム SQL
ActiveRecord::Base.connection.execute "ALTER TABLE some_table CHANGE COLUMN..." |
列の追加や削除などのスキーマ操作のほとんどは、Rails の組み込みメソッドを使用して行うことができます。けれども必要な場合には、任意の SQL コードを実行することも可能です。
あらゆるフレームワークと同じく、Ruby on Rails は然るべき配慮と注意を怠ると、パフォーマンス問題に苛まされる可能性があります。幸い、パフォーマンス問題を監視して修正するための適切な手法は比較的単純で、簡単に習得することができます。複雑な問題でも、ある程度の忍耐、そしてパフォーマンス問題の原因についての知識があれば、解決できるはずです。
学ぶために
- RubyonRails.org にアクセスして Ruby on Rails の詳細を学んでください。
- MySQL のマニュアルの低速クエリー・ログのセクションで、MySQL での実行速度の遅いクエリーのログについての詳細を調べてください。このログでは、実行時間のしきい値を超えるクエリーを追跡しています。
- MySQL のインデックスについての詳細は、MySQL のマニュアルのインデックスに関するセクションを参照してください。
- 「Rails 2.1: now with better integrated caching」というタイトルの hewebfellas ブログ投稿では、Rails V2.1 に統合されたキャッシング API の使い方を説明しています。
- developerWorks podcasts ではソフトウェア開発者のための興味深いインタビューや議論を聞くことができます。
- developerWorks の Technical events and webcasts で最新情報を入手してください。
- Twitter で developerWorks をフォローしてください。
- 世界中で近日中に予定されている IBM オープンソース開発者を対象とした会議、見本市、ウェブキャストやその他のイベントをチェックしてください。
- オープンソース技術を使用して開発し、IBM の製品と併用するときに役立つ広範囲のハウツー情報、ツール、およびプロジェクト・アップデートについては、developerWorks Open source ゾーンを参照してください。
- My developerWorks コミュニティーは、多種多様なトピックを網羅した全般的コミュニティーの成功例です。
- 無料の developerWorks On demand demos で、IBM およびオープンソースの技術と製品機能を調べて試してみてください。
製品や技術を入手するために
- Rails 開発ログ・アナライザーを調べてください。開発ログから情報を抽出するのに役立つツールです。
- N+1 問題の検出に役立つ query_reviewer プラグインを入手してください。
- cache-money プラグインを必ず入手してください。このプラグインは、Rails アプリケーションでのキャッシングに最適な方法となります。
- IBM ソフトウェアの試用版を使用して、次のオープンソース開発プロジェクトを革新してください。ダウンロード、あるいは DVD で入手できます。
- IBM 製品の評価版をダウンロードするか、あるいは IBM SOA Sandbox のオンライン試用版で、DB2®、Lotus®、Rational®、Tivoli®、および WebSphere® などが提供するアプリケーション開発ツールやミドルウェア製品を試してみてください。
議論するために
- developerWorks blogs から developerWorks コミュニティーに加わってください。