Railsで開発するときにはキャッシュを予め想定に入れておく
via En attendant Jérémie on Flickr – Photo Sharing!
今回、まともにキャッシュを使おうと思い始めたのですが、ある程度組み上げた段階でキャッシュを組み入れようとすると色々問題が起こることが分かりました。まず知識としては、Railsのキャッシュは3パターンあります。
- Page
- Action
- Fragment
があります。Pageは一番強力で、一度生成後はApacheやlighttpdなどのWebサーバレベルでレスポンスを返してくれます。アプリケーションサーバを通さない分、高速です。が、逆に言えば生成した後はRails側で制御できない状態になるので、表示を細かに変更することはできません。
次にAction Cacheがあります。これはPageアクション同様に生成されるページ全体をキャッシングしますが、Railsのアクションは実行するという点が異なります。アクションを通るので、DBにアクセスして情報を取得することが可能というメリットはありますが、DBへのアクセスがボトルネックになっている場合は使えません。
最後にFragmentキャッシュです。これはViewの中で細かく利用できます。言わば部品を定義していくことで、一部分だけの表示をキャッシングしていくことができます。細かく制御できる利点はありますが、裏を返せば多数使うと管理が大変になります。
では次に問題点や解決策を挙げてみたいと思います。
まず、レンダリングの高速化を考えてできるだけPageキャッシングを使っていきたいと思います。が、ログイン後に出す「○○さんいらっしゃい」という系統のメッセージを出す場合は問題です。これの解決策としては幾つかあるのですが、今回はJavaScriptとCookieで解決しました。
ログイン時にCookieに対してログイン名を渡します。
cookies[:login] = name
これでCookieレベルでログイン名が渡せるようになります。次にビュー側では、
<div id="logged_in"> <%= link_to _("My page"), :controller => :memo, :action => :mypage %>
(<%= link_to _("Settings"), :controller => :account, :action => :settings %>)
<%= link_to _("Log out"), :controller => :account, :action => :logout %>
</div>
<div id="logged_out">
<%= link_to _("Login"), :controller => :account, :action => :login %>
</div>
という形で、ログイン時とログアウト時の両方を書いておきます。その状態でJavaScriptを使います。ここではクッキーの制御が簡単にできるCookieManagerを使いました。
Cookie Manager | Javascript Code | All Things Webby
http://insin.woaf.net/code/javascript/cookiemanager.html
function handle_cached_user() {
var manager = new CookieManager({shelfLife:30});
var logged_in = $('logged_in');
var logged_out = $('logged_out');
if(manager.getCookie('login') == null) {
logged_in.style.display = 'none';
logged_out.style.display = 'inline';
} else {
logged_out.style.display = 'none';
logged_in.style.display = 'inline';
}
message = manager.getCookie('message');
if (message != null) {
$("notice").innerHTML = decodeURIComponent(message).replace(/\+/g, " ");
manager.setCookie("message", "");
}
}
という風にして、loginというクッキーが存在するならログイン状態に、なければ未ログイン状態としてHTML側の表示を制御しています。この場合の問題点としては、JavaScriptレベルでの制御なので、それを基準にサーバ側の処理は行ってはいけないということがあります。サーバ側は別なセッションなりできちんとアクセス制限を行う必要があります。
が、この方法を使えば、パブリックな情報は殆どPageキャッシュレベルにすることが可能です。
Pageキャッシングの問題として、ページネーションを使った場合の処理があります。これはURLの?以下の文字はキャッシングを別にしてくれないということが問題です。そのため、pageをroute.rbに組み入れておくと、1.htmlのようなページネーションに対応したHTMLファイルを生成してくれるようになります。
map.connect ‘memo/index/:id/:page’, :controller => ‘memo’, :action => ‘index’, :requirements => { :page => /\d+/}, :page => nil
Actionキャッシングは今回は使いませんでした。できるだけDBに接続しないような方法を考えた場合、Actionキャッシングでは中途半端でした。
Fragmentキャッシングは様々な場所で利用しています。例えば、今回はデータベースには簡単なデータのみを保存し、文字情報はAmazon S3に保存しています。これはMogileFSなどの分散化ファイルシステムを使った場合でも同様ですが、HTTPベースでのアクセスになるので速度的に不利です。そのため、必要なデータはキャッシングし、できるだけ接続しないようにしながらストレージを活用していくのが重要です。
当初はモデルのafter_findを使って、データベースからデータを取得した際に、それに関連するデータをAmazon S3から取得する形にしていたのですが、Fragmentキャッシュを利用するにあたって、Amazon S3に極力接続しない形に移行しました。
また、ログインしている否か、マイページなのか否かによって細かく表示を制御している場合、Fragmentキャッシュは相当増える可能性があります。そして増えすぎるとその管理が煩雑になります。そこで解決手段としては、そうした表示上の制御をできるだけ除いてしまうということがあります。ログイン時にしか使えない機能も表示してしまうことで、それがクリックされた際にはログインのページを表示するといった方法や、エラーメッセージを出すという手法をとるようにします。また、それらをJavaScriptで制御するという方法も可能ではありますが、JavaScriptにあまり頼りすぎるのも問題があります。
次の問題として、エラーメッセージがあります。例えばflash[:notice]を使ってエラーメッセージを出した場合、エラーメッセージが表示された状態でキャッシュが作成され、固定化されてしまいます。これだと、別なユーザが見た場合や、再読み込みした場合でも同じエラーメッセージが出続けます。これはCookieレベルでエラーメッセージを出すようにすると解決します。cookie[:message] = “hogehoge”でメッセージを与えておき、表示側でCookieにメッセージがあればそれを表示し、Cookieを消すという動作です。
これらの方法を後から行うと、パブリックなアクションとマイページ系のアクションを別にしたり、レイアウトも別にしたりと修正が相当入ります。元々想定しながら作っておくのが一番です。
PageキャッシングとFragmentキャッシングを活用することで、速度的には相当改善されるようになりました。CookieやJavaScriptたよりになるデメリットもありますが、単なる表示制御だけで、アクセス権限の制御はサーバ側できちんと行っている限りはそれほど問題にはならないかと思います。Railsは他のフレームワークに比べると速度面のデメリットが大きいと思いますので、キャッシングをうまく使って速度向上を行ってみてください。

7月 29th, 2008 at 11:56 AM
>Action Cache
>アクションを通るので、DBにアクセスして情報を取得することが可能というメリットはありますが、DBへのアクセスがボトルネックになっている場合は使えません。
これは違います。(少なくともRails2では・・)
確かにコントローラに処理は移りますが、実行されるのはフィルタのみでアクション自体は実行されません。
アクションキャッシュはフィルタとして実装されていて、このフィルタはキャッシュが存在する場合にはその内容を描画してfalseを返します。
つまり、アクション内でのDBへのアクセスがボトルネックとなっている場合、アクションキャッシュはむしろ有効です。(フィルタでDBアクセスを行ってて、それがボトルネックであるなら確かに意味ないですが、普通そんな重い操作をフィルタでは行いませんよね?)