開発者の間で、Rails の評価は明白です。Rails は流行語であり、また論争の種でもあります。つまり非常に高い生産性を評価する声がある一方で、議論も起きています。誰に尋ねるかによって、Rails は超生産的であるとされるか、あるいはおもちゃであるとされたり、また市場で適切な評価を得ているとされるか、あるいは過大評価されているとも言われます。また多くの新しい技術と同じく、Rails は実証されておらず、スケーラビリティーに制限があるという評判もあります。C 言語や Java™ 言語とは違って Rails はインタープリター型であり、そのためインタープリター型に特有のパフォーマンス上の弱点があります。
現実には、インターネット上の大規模サイトの多くはインタープリター型言語を使っています。そうしたサイトは、Ruby と同じ方式、つまりクラスター化され、何も共有しないアーキテクチャーを採用しています。そしてキャッシュも行っています。大部分のサイトでは、可能な限り最大のパフォーマンスを得るために、効果的なキャッシング方法を必要としています。そして Rails 開発者も、そうした方式に従い始めています。
まず、ChangingThePresent.org の何ページかを紹介させてください。このサイトの中で、キャッシュが必要となりそうな、いくつかの場所を示すことにします。次に、私達がそれぞれの場所に対して何を選択したか、そして、そうしたページを実装するために使用したコード、つまり方式に注目します。特に、私達が下記のすべてに対して、どう対応しているかを説明します。
- 静的なページ全体
- ほとんど変化しない動的なページ全体
- 動的なページの一部 (動的なページ・フラグメント)
- アプリケーション・データ
静的なページを考えてみましょう。ほとんどすべてのサイトには静的なページがあります。例えば図 1 は、私達の使用許諾条件を示しています。このページに行くためには、register
をクリックし、次に user agreement
をクリックします。ChangingThePresent の場合、私達はこのページからすべての動的コンテンツを削除したので、このページを Apache にキャッシュさせることができました。私達の Apache の構成ルールから、このコンテンツが Rails サーバーでサービスされることはありません。私はこの Rails のキャッシングをまったく考えていません。
図 1. 利用規約
次に、完全に動的なページを考えてみましょう。理論的には、ChangingThePresent は、動的に作成されても稀にしか変化しないページを持つことができます。ほとんどすべてのページはユーザーがログインしているかどうかを表示するため、私達はこのタイプのキャッシングには関心がありません。
次に、ページ・フラグメントのキャッシングを考えます。私達のホームページ (図 2) は、以前は完全に静的でした。現在では、いくつかの要素が動的です。このページは毎日、一連の寄付項目を表示します (寄付項目は、ランダムに選択されるものと私達の管理者が選択するものの両方があります)。「A Few of our Special Gifts for Earth Day」というタイトルのセクションの寄付項目に注目してください。また、右端にある「login」というリンクにも注目してください。このリンクは、ユーザーがログインしているかどうかに依存します。ページ全体をキャッシュすることはできません。このページは 1 日に 1 度だけ変更されます。
図 2. ホームページ
最後に、このアプリケーションを考えてみます。15 年前にすべてのネット・サーフィンを終えたのでもない限り、興味深いサイトの大部分は動的です。最近のアプリケーションにはレイヤーがあり、通常はレイヤー間にキャッシュを追加することで、アプリケーションを効率的にすることができます。ChangingThePresent は、少しばかりデータベース・レイヤーでキャッシングを行っています。次に、こうしたキャッシングのタイプをそれぞれ掘り下げ、私達が ChangingThePresent でどんなことをしているかを説明しましょう。
静的なデータのキャッシング方法については、画像を除けば、あまり言うことがありません。私達のサイトは寄付のためのポータルです。これはつまり、ユーザーの感情面に訴える必要があるということです。これは画像が、そしてさらにはビデオが必要なことを意味しています。しかし私達の Web サーバーである Mongrel は、静的データをあまり適切にサービスすることができません。そのため私達は、Apache を使って画像コンテンツをサービスしています。
私達は最も頻繁に使われる画像をキャッシュし、私達の顧客がそうした画像を手軽に見られるように、画像アクセラレーター、Panther Express に移行しつつあります。この方法を使うために、images.changingThePresent.org
というサブドメインを持つことにします。Panther Express は、そのローカル・キャッシュで任意の画像を直接サービスし、そしてリクエストを私達に対して送信します。Panther サービスは私達がいつ画像を変更するのかわからないため、私達は HTTP ヘッダーを使って画像を失効させています。下記を見てください。
キャッシュを失効させる HTTP ヘッダー
HTTP/1.1 200 OK Cache-Control: max-age=86400, must-revalidate Expires: Tues, 17 Apr 2007 11:43:51 GMT Last-Modified: Mon, 16 Apr 2007 11:43:51 GMT |
これらが HTML ヘッダーではないことに注意してください。こうした HTTP ヘッダーは Web ページのコンテンツとは独立に作成されます。Web サーバーは、こうした HTTP ヘッダーを作成してくれます。Web サーバーの構成は、Rails に関する記事シリーズとしては関心の対象ではないため、今度は Rails フレームワークでコントロールされるキャッシュの内容に移ることにします (Web サーバーの構成について詳しくは、「参考文献」のリンクを参照してください)。
稀にしか変化しない動的なページがある場合には、ページ・レベルのキャッシングをする必要があります。ブログや公開の掲示板は、そうした種類のアプリケーションの例です。ページ・キャッシングを行うと、Rails は動的な HTML ページを作成でき、それを公開のディレクトリーに保存できるため、アプリケーション・サーバーはそれを他の静的ページと同じようにサービスすることができます。
Rails は、ページがキャッシュされた場合、画像を含めないため、ページ・キャッシングは Rails では最も高速な種類のキャッシング動作です。最も基本的なレベルのページ・キャッシングであれば、実は Rails では非常に容易に行うことができます。ページ・キャッシングとフラグメント・キャッシングはどちらも、コントローラー・レベルで行われます。Rails に対して下記を指示する必要があります。
- どのページをキャッシュしたいか
- ページのコンテンツが変化した場合、キャッシュにあるページをどのように失効させるか
ページ・キャッシングを有効にするには、コントローラー・クラス内の caches_page
ディレクティブを使います。例えば privacy_policy
ページと user_agreement
ページを about_us_controller
でキャッシュするためには、下記のコードを入力します。
リスト 2. ページ・キャッシングを有効にする
class AboutController < ApplicationController caches_page :privacy_policy, :user_agreement end |
ページを失効させるためには expire_page
ディレクティブを使います。Rails が new_pages
アクションを呼び出した時に上記のページを失効させる場合には、私は下記のコードを使います。
リスト 3. ページを失効させる
class AboutController < ApplicationController caches_page :privacy_policy, :user_agreement def new_pages expire_page :action => :privacy_policy expire_page :action => :user_agreement end end |
ここで、いくつかの小さな問題、例えば URL に注目する必要があります。URL を URL パラメーターに依存させることはできません。例えば、gifts/water?page=1 の代わりに gifts/water/1 を使う必要があります。routes.rb では、そうした URL を容易に使うことができます。例えば私達のページは、どのタブが選択されているかを示すタブ・パラメーターを持つことがよくあります。このタブを URL の一部にするために、私達は下記のルーティング・ルールを使っています。
リスト 4. タブのためのルーティング・ルール
map.connect 'member/:id/:tab', :controller => 'profiles', :action => 'show' |
同じことを、ページ・パラメーターを持つリストや、URL パラメーターに依存する他のページにも行う必要があります。また、セキュリティーも考慮する必要があります。
ページがキャッシュの中にある場合、Rails フレームワークは無関係なため、サーバーはセキュリティーの面倒を見てくれません。Web サーバーはキャッシュの中の任意のページを構わず描画し、ユーザーがそうしたページを見る権限を持っているかどうかを気にしません。そのため、そのページを見られる人が誰かを気にする場合には、ページ・キャッシングを使うべきではありません。
単純で静的なページを単にキャッシュするだけであれば、必要なことは以上です。ある程度コンテンツが単純である限り、難しいことはありません。
しかし、もっと複雑なコンテンツをキャッシュしようとすると、トレードオフを迫られます。大幅に動的なページをキャッシュする場合には、失効ロジックが複雑になります。複雑な失効ポリシーを処理するためには、カスタムのスイーパー (sweeper) を作成して構成する必要があります。こうしたクラスは、ある種のコントローラー・アクションが起動されると、選択された要素をキャッシュから削除します。
大部分のカスタム・スイーパーは、何らかのモデル・オブジェクトを観察し、そして変化に基づいて、1 つ以上のキャッシュ・ページを失効させるためのロジックを起動します。リスト 5 は典型的なキャッシュ・スイーパーを示しています。スイーパーの中では、アクティブ・レコード・イベント (例えば after_save
など) を定義することができます。そのイベントが起動すると、スイーパーが起動し、そしてキャッシュの中にある、選択されたページを無効にします。この例は expire_page
メソッドに基づく無効化を示しています。多くの本格的なアプリケーションでは、こうした方法ではなく、Ruby の素晴らしいファイル・システム・ユーティリティーを直接使って、キャッシュされたページを明示的に削除しています。
リスト 5. 典型的なオブザーバー
class CauseController < ApplicationController cache_sweeper :cause_sweeper ... class CauseSweeper < ActionController::Caching::Sweeper observe Cause def after_save(record) expire_page(:controller => 'causes', :action => 'show', :id => record.id) cause.nonprofits.each do |nonprofit| expire_page(:controller => 'nonprofits', :action => 'show', :id => nonprofit.id) end end end |
おそらく皆さんは、ページ・キャッシングの欠点、つまり複雑さに気が付き始めたのではないでしょうか。ページ・レベルのキャッシングを適切に行うことはできますが、複雑になるという特有の問題があるため、アプリケーションはテストしにくくなり、またシステム中でバグの可能性が高まります。また、もしページがユーザーごとに異なる場合には、あるいは認証されたページをキャッシュしたい場合には、ページ・キャッシング以上のものを検討する必要があります。ChangingThePresent の場合は両方の状況に対応する必要があります。なぜなら、ユーザーがログインしているかによって基本的なレイアウト上のリンクを変更しているからです。私達の大部分のページでは、ページ・レベルのキャッシングなど、考えることすらできません。ページ・レベルのキャッシングに関して学ぶためには、いくつかの素晴らしい記事へのリンクを「参考文献」にあげましたので、それらを参照してください。次に、ページ全体のキャッシングの別形式である、アクション・キャッシングについて説明します
ここまでのところで、ページ・キャッシングの基本的な強みと、基本的な弱みの両方を学びました。大部分のページ取得では、Rails が画像を含めることはありません。強みはスピードであり、弱みは柔軟性です。アプリケーションの状態 (例えば認証など) に基づいてページ全体のキャッシングを行う必要がある場合には、ページ・キャッシングではなくアクション・キャッシングをすることができます。
アクション・キャッシングは、ページ・キャッシングと同じように動作しますが、フローは少し異なります。Rails はアクションを描画する前に、実際にコントローラーを呼び出します。もしそのアクションによって描画されたページが既にキャッシュの中にある場合には、Rails はそのページを再度描画するのではなく、キャッシュの中にある、そのページを描画します。今度は画像の中に Rails があるため、ページ・キャッシングよりもやや遅くなりますが、有利な点もあります。ほとんどすべての Rails 認証スキーマは、コントローラーの before フィルターを使います。アクション・キャッシングでは、コントローラーによる認証と任意のフィルターを利用することができます。
構文的には、アクション・キャッシングはページ・キャッシングとまったく同じように動作しますが、使用するディレクティブは異なります。リスト 6 は caches_action ディレクティブの使い方を示しています。
リスト 6. アクション・キャッシングを有効にする
class AboutController < ApplicationController caches_action :secret_page, :secret_list end |
キャッシュの失効、そしてスイーパーも、同じように動作します。私達はアクション・キャッシングを使っていませんが、その理由の多くはページ・キャッシングを使わない理由と同じです。しかしフラグメント・キャッシングは私達にとって、はるかに重要です。
部分キャッシングを使うと、ページの一部 (多くの場合はレイアウトのためのコンテンツ) をキャッシュすることができます。またフラグメント・キャッシングを使うと、Web ページ上に直接置かれた rhtml ディレクティブでブロックを囲むことによって、キャッシュすべきフラグメントを特定することができます (リスト 7)。ChangingThePresent.org では、フロント・ページと他の何ページかをフラグメント・キャッシングを使ってキャッシュしています。これらのページは、どれもデータベースを頻繁にアクセスし、また私達のサイトで最も人気のあるページです。
リスト 7. キャッシュ・フラグメントを特定する
<% cache 'gifts_index' do %> <h3> Here, you can make the world a better place with a single gift. Donation gifts are also a wonderful way to honor friends and family. Just imagine what we can achieve together. </h3> <h2 class="lightBlue"><%= @event_title %></h2> <div id="homefeatureitems"> <% for gift in @event_gifts %> <%= render :partial => 'gifts/listable', :locals => { :gift => gift } %> <% end %> </div> ... <% end %> |
リスト 7 では、キャッシュする対象のフラグメントを cache
ヘルパーが特定します。最初のパラメーターは、キャッシュ・フラグメントを特定する固有の名前です。2 番目のパラメーターは、どの RHTML フラグメントをキャッシュするかを正確に特定するコード・ブロック (最初の do
と最後の end
の間のコード) を含んでいます。
私達のサイトにはホームページが 1 つしかないため、ページに名前を付けるのは簡単です。他の場所では、あるページの URL を決定するメソッドを使って、キャッシュ・フラグメントを固有識別しています。例えば世界平和あるいは貧困の緩和など、大義のためのコードをキャッシュする場合、私達はリスト 8 のコードを使います。このコードは、その大義に対するパーマネント URL (パーマリンクとも呼ばれます) を発見します。
リスト 8. URL によってキャッシュ・フラグメントを特定する
<% cache @cause.permalink(params[:id]) do %> |
通常、個々のページをキャッシュする場合、それらのページをスイーパーで失効させる必要があります。しかし場合によると、単純に時間に基づいてオブジェクトを失効させる方法を使った方が容易で簡潔なことがあります。Rails はデフォルトではそうした機構を提供していませんが、timed_fragment_cache
というプラグインを使うと、ちょっとしたトリックを行えます。このプラグインを使うと、キャッシュされたコンテンツの中で、あるいはページの動的データを提供するコントローラー・コードの中で、タイムアウトを指定することができます。例えばリスト 9 は、大義のリストを持つページに対する動的データを作成するコードを示しています。when_fragment_expired
メソッドは、関連付けられたキャッシュ・フラグメントが失効する時にのみ実行されます。このメソッドは、タイムアウトの長さを指定するパラメーターと、どのコンテンツが失効した時にそのコンテンツを再構築するかを指定するコード・ブロックを使います。rhtml
ページの中で、キャッシュ・メソッドと共にタイムアウトを指定する選択もあったのですが、私達にとってはコントローラー・ベースのメソッドの方が好ましい選択でした。
リスト 9. 時間に基づくキャッシュの失効
def index when_fragment_expired 'causes_list', 15.minutes.from_now do @causes = Cause.find_all_ordered end end |
有効期限付きの方法を使うと、(少しばかり古いデータで我慢できるのなら) キャッシング方法を大幅に単純化することができます。キャッシュされる要素それぞれに対して、キャッシュしたいコンテンツと、動的コンテンツを作成する任意のコントローラー・アクション、そしてタイムアウトを指定すればよいだけです。ページ・キャッシングの場合と同じく、もし必要であれば、expire_fragment :controller => controller, :action => action, :id => id
.というメソッドを使って明示的にコンテンツを失効させることもできます。このメソッドは、キャッシュされたアクションやページの失効とまったく同じように動作します。次に、バック・エンドの構成方法を説明しましょう。
ここまで、Ruby on Rails のためのページとフラグメントのキャッシング・モデルを説明してきました。API については説明したので、今度はキャッシュされたデータがどこに行くのかを定義しましょう。Rails はデフォルトで、キャッシュされたページをファイル・システムの中に置きます。キャッシュされたページもアクションも、公開のディレクトリーの中に入ります。キャッシュされたフラグメントのストレージ場所は、構成することができます。メモリー・ストアを使うこともでき、あるいは (指定したディレクトリーの) ファイル・システムやデータベース、あるいは memcached と呼ばれるサービスを使うこともできます。ChangingThePresent.org では memcached
を使っています。
memcached は、ネットワークを介して到達できる巨大なハッシュ・マップと考えることができます。メモリー・ベースで行うキャッシングは高速であり、そしてネットワーク・ベースのキャッシュはスケーラブルです。プラグイン・サポートを利用することで、Rails は memcached を使ってフラグメントと ActiveRecord モデルをキャッシュすることができます。memcached を使うためには、memcached をインストールし (詳細は「参考文献」を参照)、そしてそれを environment.rb (あるいは production.rb など環境構成ファイルの 1 つ) の中で構成します。
リスト 10. キャッシングの構成
config.action_controller.perform_caching = true memcache_options = { :c_threshold => 10_000, :compression => false, :debug => false, :readonly => false, :urlencode => false, :ttl => 300, :namespace => 'igprod', :disabled => false } CACHE = MemCache.new memcache_options |
リスト 10 は、典型的な構成を示しています。1 行目の config.action_controller.perform_caching = true
は、キャッシングをオンにします。次の行は、キャッシングのオプションを準備します。さまざまなオプションを利用することで、さらに多くのデバッグ・データを取得でき、キャッシュを無効にでき、そしてキャッシュの名前空間を定義できることに注意してください。構成オプションの詳細については、「参考文献」セクションにあげた memcached のサイトを参照してください。
私達が使用しているキャッシング形式の最後は、モデル・ベースのキャッシングです。私達は、CachedModel と呼ばれる、キャッシング・プラグインをカスタマイズしたものを使っています。モデル・キャッシングは限定された形式のデータベース・キャッシュであり、容易に、モデル単位で有効化することができます。
モデルでキャッシング・ソリューションを利用するためには、ActiveRecord を継承する代わりに単純に CachedModel
クラスを継承します (リスト 11)。CachedModel
は ActiveRecord::Base
を継承します。ActiveRecord は、完全なオブジェクト・リレーショナル・マッピング・レイヤーではありません。このフレームワークは、SQL に大きく依存して複雑な機能を実行します。そしてユーザーは、必要に応じて容易に SQL の中まで入り込むことができます。ただし直接 SQL を使用すると、キャッシングに問題が起こりがちです。これはキャッシング・レイヤーが、1 つのデータベース行ではなく完全な結果セットを処理しなければならないためです。結果セットを完全に処理することは、できたとしても問題が多く、それを十分にサポートするアプリケーション・ロジックがない限り、ほとんど不可能です。こうした理由から、CachedModel
は厳密に 1 つのモデル・オブジェクトのキャッシングのみに焦点を絞っており、1 つの行を返すクエリーに対してのみ高速化を行います。
リスト 11. CachedModel を使う
Class Cause < CachedModel |
大部分の Rails アプリケーションは、(ユーザー・オブジェクトなど) いくつかの項目を繰り返しアクセスします。こうした状況では、モデル・キャッシングによって動作が高速化されます。ChangingThePresent の場合には、ようやく最近、モデル・ベースのキャッシングの活用を始めたところです。
Ruby は非常に生産的な言語ですが、インタープリター型であるため、パフォーマンスの観点からは理想的ではありません。大部分の主な Rails アプリケーションは、キャッシングを効果的に利用することで、そうした欠点をいくらか低減しています。ChangingThePresent.org の場合、私達は基本的にフラグメント・キャッシングを使っており、また基本的にタイム・ベースの方法を使うことで、コントローラーによるフラグメントのキャッシングを無効にしています。私達の場合にはログインしたユーザーに基づいて変化するページがあるのですが、この方法はうまく機能しています。
また私達は、memcached に基づく CachedModel
クラスを使った場合の影響も検討しています。まだ、そうしたキャッシングがデータベースのパフォーマンスに与える影響を検討し始めたばかりですが、初期の結果を見る限り有望です。次回の記事では、現実の世界の Rails の、もう 1 つの例として、私達がデータベースの最適化に使用しているいくつかのトリックを解説する予定です。
学ぶために
- 『Java から Ruby へ ―マネージャのための実践移行ガイド』(2006 年、Pragmatic Bookshelf 刊) は、この記事の著者による本です。Java プログラミングから Ruby on Rails に切り替える意味があるのは、いつ、どのような場合か、そしてその方法について解説しています。
-
Changing The Present は非営利のマーケットであり、1 エーカーの熱帯雨林、目の見えない人のための視力、あるいは癌研究者の 1 時間などから成る寄付を行うことができます。この記事シリーズは、このサイトを基に解説しています。
- 「Rolling with Ruby on Rails」と「Learn all about Ruby on Rails」を読んで、インストールの手順を含めて Ruby and Rails について学んでください。
-
Active Record は Ruby on Rails フレームワークのためのパーシスタンス・フレームワークです。
- Robert Evans による Rails Caching は、Rails でのキャッシング・モデルの概要を説明しており、ページ・キャッシングやアクション・キャッシング、そしてフラグメント・キャッシングなどを網羅しています。
-
Ruby on Rails Caching Tutorial は Gregg Pollack がキャッシングについて解説している素晴らしいチュートリアルです。順を追いながらページ・キャッシングを説明しており、またキャッシュのスイープ方法についても適切に概要を説明しています。
議論するために
- オープンソースの Ruby on Rails Web フレームワークをダウンロードしてください。
-
Mongrel は実働の強さを持つアプリケーション・サーバーであり、最高レベルの Rails サイトの多くで実行されています。私達は ChangingThePresent.org で Mongrel を使っています。
-
Apache Web サーバーは、多くの Rails サイトで (キャッシュされたコンテンツを含む) 静的コンテンツをサービスするために使用されている Web サーバーです。
-
Panther Express は、ChangingThePresent.org が画像コンテンツをキャッシュするために使おうとしている画像アクセラレーターです。
-
Timed-cache expiration plug-in は Richard Livsey によるプラグインであり、キャッシュされたフラグメントを制限時間によって失効処理することができます。ChangingThePresent.org は、このプラグインを使って私達のホームページや他の主なキャッシュ・フラグメントをキャッシュしています。
-
Memcached はネットワーク・サービスであり、分散されたオブジェクトのキャッシュを提供します。Memcached は ChangingThePresent のキャッシュ・サービスのバック・エンドとして動作しています。
-
CachedModel: は memcached に基づくキャッシュ・サービスであり、ActiveRecord オブジェクトの裏付けとして動作します。CachedModel は、結果が 1 行のデータベース・クエリーのみをアクセラレートします。
Bruce Tate氏は、独立コンサルタント兼執筆者です。彼は12年間IBMに勤め、Javaのproof-of-conceptチームなどの様々な仕事に携わりました。Tate氏は、IBMで、Austinのあるスタートアップのためにソリューション開発組織を運営していましたが、その後IBMを退職し、J2Life, LLC という独自の事業を立ち上げました。彼は、Bitter Javaを含め、Javaアンチパターンに関する3冊の書籍を執筆しました。彼の連絡先は、bruce.tate@j2life.comです。