RailsアプリケーションにおけるHTTPキャッシュの仕組みと実践的な活用方法
キャッシングを一般的に説明すると、データを保存しておき、後で素早く取り出せるようにすることです。計算済みのデータを再計算せずに済むように保持する場合もあれば、ローカルにデータを保管して再取得の手間を避けることを指す場合もあります。皆さんのコンピュータは常にこれを行っています。OSは頻繁にアクセスされるデータをRAMに維持しようとするため、ハードディスクやSSDから再度読み込む必要がなくなるからです。
同様に、ブラウザもすでにダウンロードしたリソースの再利用を試みます。新しいWebサイトを初めて訪れたときの経験がある方なら、ご存じでしょう。初回の読み込みに時間がかかるのは、ブラウザが必要なすべて——画像、JavaScript、スタイルシートなど——を取得しなければならないからです。興味深いことに、CNNのトップページを新規ダウンロードすると、そのデータ量は1993年頃のオリジナル版「DOOM」ゲームよりも大きくなります。執筆時点でCNNは約3MB(圧縮前は約15MB)のデータをダウンロードします(広告ブロック有効時の数値)。一方、オリジナル版「DOOM」のインストーラーは約2.2MBでした。
ブラウザがこのデータをキャッシュするには、サーバーとの連携が必要です。ブラウザは何をどれくらいの期間キャッシュしてよいのかを知る必要があります。そうでなければ、サーバーに新しいバージョンが存在するにもかかわらず、古いコンテンツを表示してしまう可能性があります。本記事では、このクライアント・サーバー間のキャッシュ調整がどのように行われるのか、そしてRailsがそれを制御するためにどんな機能を提供しているのかを見ていきます。
焦点はRuby on Railsの扱いですが、実際のメカニズム自体はHTTPの仕様の一部です。つまり、ここで扱うキャッシングはインターネットの基盤に組み込まれており、現代のWebサイトやフレームワーク開発の礎となっています。Railsのようなフレームワーク、シングルページアプリケーション(SPA)、さらには静的サイトまで、これらのメカニズムを活用してパフォーマンスを向上させることができます。
HTTPリクエストとレスポンス
リクエスト・レスポンスのライフサイクルについては、少なくとも概要レベルではご存じでしょう。Webサイト上のリンクをクリックすると、ブラウザがサーバーにコンテンツをリクエストし、サーバーがそのコンテンツを返します(ここでは多くの複雑さを意図的に省略しています)。
このやり取りで実際に送信されているデータを少し詳しく見てみましょう。各HTTPメッセージにはヘッダーとボディがあります(HTMLタグの<head>や<body>とは別物なので注意してください)。リクエストヘッダーは、アクセスしようとしているパスと使用するHTTPメソッド(GET/PUT/PATCH/POSTなど)をサーバーに伝えます。必要であれば、ブラウザの開発者ツールやcurlなどのコマンドラインツールを使ってヘッダーを確認できます。
# curl -v honeybadger.io
...
> GET / HTTP/1.1
> Host: honeybadger.io
> User-Agent: curl/7.64.1
> Accept: */*
出力の最初の部分がリクエストヘッダーです。honeybadger.ioに対してGETを発行しています。続いて、サーバーが返した内容(レスポンスヘッダー)が表示されます。
>
< HTTP/1.1 301 Moved Permanently
< Cache-Control: public, max-age=0, must-revalidate
< Content-Length: 39
< Content-Security-Policy: frame-ancestors 'none'
...
< Content-Type: text/plain
レスポンスにはHTTPステータスコードが含まれます(成功なら200、未検出なら404など)。この例では永続的なリダイレクト(301)になっています。curlがhttpのURLに接続しようとしたため、安全なhttpsのURLへリダイレクトされたのです。
レスポンスヘッダーにはコンテンツタイプも含まれています。ここではtext/plainですが、その他によく使われるものとしてtext/html、text/css、text/javascript、application/jsonなどがあります。
レスポンスボディはヘッダーの後に続きます。今回の場合、301リダイレクトにはボディが不要なため空になっています。curl -v https://www.honeybadger.ioで再度試せば、ここにトップページのコンテンツが表示されます。ブラウザでページのソースを表示したときと同じ内容です。
自分で試してみたい方向けに、2つのヒントを紹介します。
- curlでレスポンスヘッダーだけを表示する(リクエストヘッダーやレスポンスボディを非表示にする)には、
-Iオプションを使います。例:curl -I localhost:3000。 - Railsはデフォルトでは開発環境でキャッシュを行いません。
rails dev:cacheを先に実行する必要があるかもしれません。
Cache-Control HTTPヘッダー
キャッシングに関して最も重要なヘッダーはCache-Controlです。これは、Railsサーバーからのレスポンスをどのマシンがキャッシュできるか、そしてそのキャッシュデータがいつ期限切れになるかを決めるのに役立ちます。Cache-Controlヘッダーには複数のフィールドがあり、そのほとんどは任意指定です。ここでは特に関連性の高い項目をいくつか紹介します。詳細については、w3.orgの公式HTTP仕様をご確認ください。
以下は、デフォルト設定のままの基本的なRailsレスポンスヘッダーのサンプルです。
< Content-Type: text/html; charset=utf-8
< Etag: W/"b41ce6c6d4bde17fd61a09e36b1e52ad"
< Cache-Control: max-age=0, private, must-revalidate
max-age
max-ageフィールドは、レスポンスが有効である秒数を表す整数です。デフォルトでは、Railsのビューに対するレスポンスは0に設定されています(つまり、レスポンスは即座に失効し、ブラウザは常に新しいバージョンを取得すべきということです)。
public / private
ヘッダーにpublicまたはprivateを含めると、どのサーバーがレスポンスをキャッシュできるかが決まります。privateが含まれている場合、キャッシュできるのはリクエスト元のクライアント(ブラウザなど)だけであり、途中経由する他のサーバー——CDNやプロキシなど——はキャッシュできません。publicが含まれている場合は、こうした中間サーバーもレスポンスをキャッシュできます。Railsはデフォルトでprivateを設定します。
must-revalidate
Railsはデフォルトでmust-revalidateフィールドも設定します。これは、クライアントがキャッシュしたバージョンを使用する前に、サーバーに問い合わせてまだ有効であることを確認しなければならないという意味です。キャッシュバージョンの有効性を判定するために、クライアントとサーバーはETagを使用します。
ETagとは
ETagは、サーバーがクライアントにレスポンスを送る際に付加できる任意のHTTPヘッダーです。通常はレスポンス本体のチェックサムのようなものです。クライアント(ブラウザ)が同じリソースを再リクエストする際、以前のレスポンスをキャッシュしていれば、受け取ったETagをIf-None-Match HTTPヘッダーに含めて送ります。サーバー側でバージョンが変わっていなければ、304(Not Modified)ステータスコードと空のボディで応答できます。これはサーバー上のバージョンが変更されていないため、クライアントはキャッシュ済みのバージョンを使ってよいという意味です。
ETagには強い(strong)と弱い(weak)の2種類があります(弱いETagはW/プレフィックスで示されます)。動作は同じですが、強いETagはサーバー上のリソースとローカルキャッシュ内のコピーが100%バイト単位で同一であることを意味します。弱いETagの場合は、両者がバイト単位で完全に一致していなくても、キャッシュバージョンを使用できることを示します。よくある例がRailsのcsrf_meta_tagsヘルパーです。これは頻繁に変わるトークンを生成するため、静的ページであってもCSRF(クロスサイトリクエストフォージェリ)トークンのせいで、更新のたびにバイト単位では同一になりません。Railsはデフォルトで弱いETagを使用します。
RailsにおけるETag
Railsはビューに対してETagを自動的に処理します。送信ヘッダーにETagを含めるとともに、ミドルウェアで受信したETagヘッダーをチェックし、適切な場合には304(Not Modified)を返します。ただし注意点があります。Railsはビューを動的に生成するため、そのビューのETagを算出する前にすべてのレンダリング作業を実行しなければなりません。つまり、ETagが一致しても節約できるのはネットワーク経由でデータを送信する時間と帯域幅だけです。ビューキャッシュのように、キャッシュ済みバージョンがあればレンダリング工程を丸ごとスキップできるわけではありません。とはいえ、Railsには生成されるETagを調整する方法がいくつか用意されています。
stale?
絶えず変化するCSRFトークンによってETagが変わってしまう問題を回避する方法の1つが、ActionControllerのstale?ヘルパーです。これによりETag(強弱どちらでも)を直接設定できます。また、ActiveRecordモデルなどのオブジェクトを渡すだけでも構いません。その場合、オブジェクトのupdated_atタイムスタンプに基づいてETagが計算され、コレクションを渡した場合は最大のupdated_atが使われます。
class UsersController < ApplicationController
def index
@users = User.includes(:posts).all
render :index if stale?(@users)
end
end
curlでページにアクセスすると、結果を確認できます。
# curl -I localhost:3000 -- 初回アクセス
ETag: W/"af9ae8f2d66b9b6c4d0513f185638f1a"
# curl -I localhost:3000 -- 再読み込み(CSRFトークンによる変化)
ETag: W/"f06158417f290334f47ea2124e08d89d"
-- stale? をコントローラーに追加
# curl -I localhost:3000 -- 再読み込み
ETag: W/"04b9b99835c359f36551720d8e3ca6fe" -- @users から生成されるようになった
# curl -I localhost:3000 -- 再読み込み
ETag: W/"04b9b99835c359f36551720d8e3ca6fe" -- 変化なし
これで、クライアントに完全なペイロードを再ダウンロードさせるタイミングをより細かく制御できるようになりました。それでも、キャッシュが有効かどうかを判断するために毎回サーバーへの問い合わせが必要です。そもそもこのチェック自体を省きたい場合はどうすればよいでしょうか? そこで登場するのがヘッダーのmax-ageフィールドです。
expires_in と http_cache_forever
RailsはActionControllerにmax-ageフィールドを調整するためのヘルパーメソッドを2つ提供しています。expires_inとhttp_cache_foreverです。名前から想像できる通りの動作をします。
class UsersController < ApplicationController
def index
@users = User.includes(:posts).all
expires_in 10.minutes
end
end
# curl -I localhost:3000
Cache-Control: max-age=600, private
Railsはmax-ageを600(10分=600秒)に設定し、must-revalidateフィールドを削除しました。名前付き引数public: trueを渡せば、privateの部分も変更できます。
http_cache_foreverは、max-ageを100年に設定するexpires_inのラッパーのようなもので、ブロックを受け取ります。
class UsersController < ApplicationController
def index
@users = User.includes(:posts).all
http_cache_forever(public: true) do
render :index
end
end
end
# curl -I localhost:3000
Cache-Control: max-age=3155695200, public
このような超長期キャッシュこそ、Railsのアセットに「フィンガープリント」が付加される理由です。フィンガープリントはファイル内容のハッシュで、packs/js/application-4028feaf5babc1c1617b.jsのようなファイル名を生成します。末尾のフィンガープリントは、ファイルの内容とファイル名を事実上結び付けています。内容が変われば、ファイル名も変わります。つまり、ブラウザはこのファイルを永久に安全にキャッシュできるのです。たとえ小さな変更があったとしてもフィンガープリントが変わるため、ブラウザにとってはそれはダウンロードが必要な全く別のファイルになるからです。
影響範囲について
ここまでいくつかのキャッシュオプションを紹介してきましたが、私からのアドバイスは少し意外に聞こえるかもしれません。本記事で紹介したメソッドの使用は極力避けてください。 ETagやHTTPキャッシュについて知識を持っておくことは価値があり、Railsは特定の問題に対処するための具体的なツールも提供しています。しかし重要な注意点があります。これらのキャッシュはすべてアプリケーションの外側で行われるため、ほぼ制御不能だということです。このシリーズの前編で取り上げたビューキャッシュや低レベルキャッシュをRailsで使っている場合、無効化の問題に直面しても対処手段があります。モデルをtouchしたり、更新済みのコードをデプロイしたり、最悪の場合はコンソールから直接Rails.cacheを操作したりすることも可能です。しかし、HTTPキャッシュにはそれができません。個人的には、本番環境でRails.cache.clearを実行する方が、ユーザーがブラウザキャッシュを削除するまでサイトが壊れたままになる状況に直面するよりもはるかにマシです(カスタマーサポートチームにも感謝されるでしょう)。
まとめ
Railsにおけるキャッシングに関するシリーズは以上で終わりです。役立つ情報になっていれば幸いです。キャッシングに関する私のアドバイスは常に次の通りです。必要最小限に抑えつつ、必要な分だけ行うこと。 パフォーマンスの問題に遭遇したら、まず頻繁に呼び出されるメソッドを探しましょう。メモ化できるかもしれません。値をリクエスト間で保持する必要があるなら、多用されているそのメソッドに低レベルキャッシュを適用できないか検討してください。あるいは特定のメソッドが原因ではなく、大量にネストされたパーシャルの処理が遅延を引き起こしているだけかもしれません。その場合はビューレベルのキャッシュが有効です。Railsはこうした問題それぞれに狙いを定められる「鋭いナイフ」を提供してくれています。あとは、いつどれを使うべきかを見極めるだけです。
-
Rubyでコマンドラインアプリケーション(CLI)を作る方法を徹底解説
RubyはWebアプリケーション開発のイメージが強く、それ以外の用途があることを忘れられがちです。そこで本記事では、Rubyを使ってコマンドラインアプリケーション(CLI)を構築する方法を詳しく解説します。 まず、皆さんがよく目にするコマンドラインアプリケーションの例を挙げてみましょう。 psql(PostgreSQLの対話型ターミナル) rails bundler gem git CLIアプリケーションの作り方はさまざまですが、この記事では代表的な3つの方法に焦点を当てて紹介します。 この記事で学べること ARGV配列の使い方 OptParseライブラリの使い方 Thor gemの使い
-
Ruby on Railsとは?初心者にもわかる仕組み・魅力・学び方を徹底解説
Ruby on Railsとは? Ruby on Rails(略称:RoR)は、世界で最も人気のあるオープンソースのWebアプリケーションフレームワークです。プログラミング言語「Ruby」をベースに構築されており、シンプルなサイトから大規模で複雑なサービスまで、幅広いWebアプリケーションの開発を支援します。 そもそもフレームワークとは? フレームワークとは、ソフトウェア開発の際に土台となる構造を提供してくれるコードやツール、ユーティリティの集合体です。あらかじめ用意された構造に沿ってコードを書くことで、プログラムが整理され、保守性も高まります。正しく使いこなせるようになれば、開発作業は格段に