Heroku では、アクセス時のエクスペリエンスを最適化するために、Rack::Cache
経由で CDN を使用してアセットの配信を高速化することを推奨しています。すでに CDN を使用している場合、Rack::Cache
を追加してもアセットの配信は高速化されません。
Cedar スタック上でアセットを効率的に供給するために、Ruby on Rails アプリケーションで Rack::Cache を使用することが推奨されています。Rack::Cache を適切に使用すれば、応答時間が短縮され、負荷が軽減されます。これは、アプリケーションを通じて静的アセットを提供する場合に重要です。
この記事では、Rack::Cache を使用したアセットのキャッシングの概念を要約します。また、Rails 3.1 以降のアプリケーションとアセットパイプラインの適切な設定について説明します。このガイドの内容は Rails 4 アプリケーションにも完全に適用されます。
Rack を理解する
Rack は、Ruby をサポートする Web サーバーと、Ruby フレームワークの間を取り持つ最小限のインターフェースです。その目的は共通のインターフェースとして機能することであり、これによって Web サーバーでは、Rack を実装するだけで、同じく Rack をサポートする任意の Ruby Web フレームワークをサポートできるようになり、その逆も同様です。
この設計から生まれる優れた機能は、いわゆる “ミドルウェア” です。ミドルウェアは単に、両側で Rack インターフェースを実装するアプリケーションです。つまり、Rack インターフェースを通じて Web サーバーからのリクエストを消費し、何らかの処理を行った後、Rack インターフェースを通じて別のアプリケーションにリクエストを渡します。実際に、Rack インターフェースを使用すれば、Web サーバーとアプリケーションの間に、透過的なプロキシのようなサービスをいくつでも配置できます。 受信 HTTP リクエストの場合、各ミドルウェアは何らかのアクションを実行してから、"ラック" 内の次のミドルウェアにリクエストを渡します。 最終的に、リクエストは正しくフォーマットされてアプリケーションに到達します。
Rack は軽量性と柔軟性を備えるように記述されています。ミドルウェアは、リクエストに直接応答できる場合、Rails アプリケーションとやり取りする必要はありません。これにより、リクエストの戻りが高速化され、Rails アプリケーションの全体的な負荷が軽減されます。
Rails 3 以降や Sinatra を含む多くの Ruby Web フレームワークは Rack を基盤としています。
Rack::Cache
Rack ミドルウェアの Rack::Cache によって、アプリケーションでの HTTP キャッシングが実現します。また、アプリケーションにおいて、メインの Rails アプリケーションからの処理を必要とせずにストレージバックエンドからアセットを供給できるようになります。
Rack::Cache ストレージ
Rack::Cache には、メタストアとエンティティストアの 2 種類のストレージ領域があります。 MetaStore は、HTTP リクエストおよび応答のヘッダーなど、個々のキャッシュエントリに関する高レベルの情報を保持します。リクエストが受信されると、キャッシングのコアロジックでは、このメタ情報を使用して、リクエストを満足できる期限内のキャッシュエントリが存在するかどうかを判断します。EntityStore は、実際の応答本体の内容が保存される場所です。応答がキャッシュに入力されると、応答本体の内容の SHA1 ダイジェストが計算され、キーとして使用されます。
Rack::Cache では、MetaStore と EntityStore は区別されるため、それぞれに使用するストレージエンジンをユーザーが個別にカスタマイズできます。MetaStore はアクセス頻度が非常に高い反面、メモリはほとんど必要としません。一方で、EntityStore はアクセス頻度が低く、必要なメモリは多くなります。
Rack::Cache には、file
、heap
、memcache
の 3 種類のストレージエンジンが付属します。file
エンジンでのデータ保存は低速ですが、メモリ効率に優れています。heap
を使用すると、プロセスのメモリが使用されて高速化しますが、際限なく肥大するとパフォーマンスに影響が出る可能性があります。memcache
の使用は最速のオプションですが、大きなオブジェクトの保存には適していません。
エンティティストアとメタストアの詳細は、Rack キャッシュストレージに関する記事を参照してください。
MetaStore と memcache
ストレージエンジンを使用すると、共有メタデータへの非常に高速なアクセスが可能になります。一方で、EntityStore の file
エンジンを使用すれば、オブジェクトのサイズが大きくなり、アプリケーションパフォーマンスプロファイルが効率的で予測可能になるため、Heroku ではこちらを推奨しています。
Memcached のローカルインストール
他の OS でのローカルインストール手順については、MemCachier アドオンの記事をご覧ください。
アプリケーションをローカルで実行して Rack::Cache の設定をテストするには、memcached がインストールされている必要があります。Mac OSX では、homebrew などのツールを使用してインストールできます。
$ brew install memcached
インストールが終了すると、memcached を手動で起動する方法と、システム起動時に自動起動する方法について homebrew から指示があります。
Rails キャッシュストアと Rack::Cache
Rails には、Rack::Cache とは別に、独自の組み込みのキャッシングシステムが備わっています。 このシステムは一般に、Rack::Cache とは異なる目的に使用されます。
Rails キャッシングシステムは、コントローラーアクションとページフラグメントをキャッシュするためのものですが、Rails コードは呼び出します。 一方、Rack::Cache は、スタイルシート、JavaScript、画像などの完全に静的なアセットのキャッシングを管理します。キャッシュヒットによって Rails コードが呼び出されることはありません。 Rack::Cache は Varnish、Nginx、Apache などの CDN または HTTP キャッシュを代替しますが、Rails キャッシングシステムはこれらから完全に独立しています。
Rails キャッシングシステムの詳細は、こちらをご覧ください。 Memcache も有力な選択肢であり、このシステムでの使用が十分にサポートされています。
Rails キャッシュストアの設定
この手順では、memcached を使用するように Rails キャッシングシステムを設定します。Rails キャッシングシステムは Rack::Cache から独立しているため、これは厳密に必須というわけではありません。しかし、キャッシングシステムを統合することが推奨されています。
Rack::Cache gem と、推奨の memcached クライアントである Dalli をインストールする必要があります。Gemfile
で、次の内容を追加します。
gem 'rack-cache'
gem 'dalli'
オプションですが、より高速な kgio IO システムをインストールすることもお勧めします。
gem 'kgio'
bundle install
を実行して Dalli をアプリケーションの依存関係として確立した後、そのキャッシュストアに Dalli クライアントを使用するよう、config/application.rb
で Rails に指示します。
config.cache_store = :mem_cache_store
ローカルの Rails コンソールセッションを開始し、単純なキーと値を取得および設定することによって、設定を確認します。
$ rails c
> mc = Dalli::Client.new
> mc.set('foo', 'bar')
> mc.get('foo')
'bar'
memcached を使用するようにアプリケーションを設定したら、次は Rack::Cache を設定します。
Rack::Cache の設定
config/environments/production.rb
環境ファイルに変更を加えて、Rails の組み込み Rack::Cache 統合に適したストレージバックエンドを指定します。
指定しない場合、Dalli::Client.new
は memcache サーバーの場所を自動的に
MEMCACHE_SERVERS
環境変数から取得します。存在しない場合、
デフォルトで localhost とデフォルトポート (11211) になります。
client = Dalli::Client.new((ENV["MEMCACHIER_SERVERS"] || "").split(","),
:username => ENV["MEMCACHIER_USERNAME"],
:password => ENV["MEMCACHIER_PASSWORD"],
:failover => true,
:socket_timeout => 1.5,
:socket_failure_delay => 0.2,
:value_max_bytes => 10485760)
config.action_dispatch.rack_cache = {
:metastore => client,
:entitystore => client
}
config.static_cache_control = "public, max-age=2592000"
設定オプションの完全なリストはこちらで確認できますが、上記のもので十分です。
:value_max_bytes
オプションは 10 MB に設定します。このようにしないと、1 MB より大きいアセットに対して Rack::Cache が HTTP 5xx エラー応答を返します。その原因は、memcached ではデフォルトで 1 MB 以下の値しか許可されず、1 MB より大きいキーと値のペアを set
しようとすると Dalli で例外がスローされるためです。:value_max_bytes
を
10 MB に増やすと、Dalli でこのエラーがスローされなくなる代わりに、memcache サーバーは 1 MB を超えるアセットについてミスを返すようになります。したがって、非常に大きいアセットは Rack::Cache によって供給されない (もっとも、これらの種類のアセットにとっては特にメリットがない) ものの、これによって問題が起きることはありません。
複数の dyno の処理
EntityStorage には file
ストレージエンジンを使用することをお勧めしますが、複数の dyno を使用する場合、Heroku ではこれは不可能です。問題は、MetaStore に memcache
を使用する場合、どのファイルが現在キャッシュされていないか、またはキャッシュされているかに関するすべてのメタデータがすべての dyno 間で共有されることです。ただし、file
ストアはそうではなく、dyno ローカルに限定されます。したがって、ファイルをキャッシュする最初の dyno はそのローカルディスクにファイルを保存しますが、キャッシュにファイルが含まれていることを他のすべての dyno に通知します。他の dyno では、それぞれのキャッシュに実際のファイルが存在しないため、キャッシュからファイルが見つかることもありません。
ここでは、EntityStore にも memcached を使用するアプローチを採用します。もう 1 つの選択肢は、dyno 固有のプレフィックスを MetaStore のキーに使用するように各 dyno を設定し、各 dyno の Rack::Cache の独立性を保つというものです。
あるいは、memcached を完全に排除して、MetaStore には heap
ストアを、EntityStore には file
ストアをそれぞれ使用することもできます。
静的アセットの供給
GitHub のリファレンスアプリケーションの production.rb
設定を参照してください。
アプリケーションで静的アセットを適切に供給、無効化、更新できるようにするには、config/environments/production.rb
でいくつかの設定を更新する必要があります。Rails にアセットを供給させる (したがって、Rack::Cache によって管理されるようにする) には、serve_static_assets
設定を
使用します。
config.serve_static_assets = true
加えて、Cache-Control ヘッダーを設定することによって、アイテムがキャッシュにとどまる時間の長さを指定します。Cache-Control ヘッダーがない場合、静的ファイルは Rack::Cache によって保存されません。
config.static_cache_control = "public, max-age=2592000"
これらの設定は、静的要素をかなり長時間保存するよう Rack::Cache に指示します。
Cache-Control
ヘッダーと HTTP キャッシング は、通常、動的コンテンツにも適用できます。
変更が加えられたファイルを正しく無効化するために、Rails では、各ファイルのハッシュダイジェストを保持し、計算されたファイル名の一部としてそれを保存します。これがファイルのフィンガープリントの役割を果たすので、ファイルが変更されたときの検出が可能になります。
config.assets.digest
設定でこのアプローチを有効にします。
config.assets.digest = true
本番環境でキャッシュが有効になっていることを確認する必要もあります。
config.action_controller.perform_caching = true
MemCachier アドオンのプロビジョニング
Rack::Cache の MetaStore として memcache を使用するので、MemCachier アドオン を Heroku 上のアプリケーションに追加する必要があります。
$ heroku addons:create memcachier:dev
----> Adding memcachier on memcachier-direct... done, v24 (free)
MemCachier が設定する環境変数のプレフィックスは MEMCACHIER
です
(MEMCACHE
ではありません)。ただしこれは、memcachier
gem によって自動的に修正されます。これを gemfile に含めます。
gem "memcachier"
本番環境でのキャッシング
アプリケーションを Heroku にデプロイし、heroku logs
コマンドを使用してキャッシュ出力を表示します。
$ git push heroku master
$ heroku logs --ps web -t
ハードリフレッシュを使用すると、ブラウザのキャッシュがクリアされて
アセットの強制的なリクエストに役立ちます。ほとんどのブラウザでは、Shift-R
ショートカットでハードリフレッシュを実行します。
本番環境のログストリームに cache
エントリが出現します。miss, store
トークンは、項目がキャッシュに見つからなかったが次のリクエストのために保存されたことを示します。
cache: [GET /assets/application-95bd4fe1de99c1cd91ec8e6f348a44bd.css] miss, store
cache: [GET /assets/application-95fca227f3857c8ac9e7ba4ffed80386.js] miss, store
cache: [GET /assets/rails-782b548cc1ba7f898cdad2d9eb8420d2.png] miss, store
fresh
は、項目がキャッシュに見つかってそこから取得されたことを示します。
cache: [GET /assets/application-95bd4fe1de99c1cd91ec8e6f348a44bd.css] fresh
cache: [GET /assets/application-95fca227f3857c8ac9e7ba4ffed80386.js] fresh
cache: [GET /assets/rails-782b548cc1ba7f898cdad2d9eb8420d2.png] fresh
これで完了です。Rails 3.1 以降のアプリケーションは、memcached を使用して静的アセットをキャッシュするように設定されたので、その役割から解放された dyno で動的なアプリケーションリクエストを実行できるようになります。
デバッグ
設定が正しくない場合、store
または fresh
の代わりに miss
がログに記録されることがあります。
cache: [GET /assets/application-95bd4fe1de99c1cd91ec8e6f348a44bd.css] miss
cache: [GET /assets/application-95fca227f3857c8ac9e7ba4ffed80386.js] miss
cache: [GET /assets/rails-782b548cc1ba7f898cdad2d9eb8420d2.png] miss
その場合は、curl
を使用してアセット応答ヘッダーを検査し、Cache-Control ヘッダーが存在していることを確認してください。
$ curl -I 'http://memcachier-examples-rack.herokuapp.com/assets/shipit-72351bb81da0eca408d9bd8342f1b972.jpg'
HTTP/1.1 200 OK
Age: 632
Cache-Control: public, max-age=2592000
Content-length: 70522
Etag: "72351bb81da0eca408d9bd8342f1b972"
Last-Modified: Sun, 25 Mar 2012 01:51:21 GMT
X-Rack-Cache: fresh
応答ヘッダーに Cache-Control
が含まれ、その値が config.static_cache_control
設定の具体的な値 (public, max-age=2592000
) である必要があります。X-Rack-Cache
ヘッダーがアセットの状態 (fresh/store/miss) を示していることも確認してください。. Also confirm that you are seeing the
予期しない結果になった場合は、本番環境の設定を確認してください。
ファイルバージョンの不整合
ファイルに変更を加えてもサーバーから古いファイルが供給され続ける場合は、デプロイする前に、ファイルを Git リポジトリにコミットしたことを確認してください。
heroku run bash
を使用して public/assets
ディレクトリの内容を一覧表示すると、コンパイルされたコードにそのファイルが存在するかどうかを
確認できます。ハッシュ化されたアセットファイル名がこのディレクトリに含まれているはずです。
$ heroku run bash
Running bash attached to terminal... up, run.1
$ ls public/assets
application-95bd4fe1de99c1cd91ec8e6f348a44bd.css application.css manifest.yml
application-95bd4fe1de99c1cd91ec8e6f348a44bd.css.gz application.css.gz rails-782b548cc1ba7f898cdad2d9eb8420d2.png
application-95fca227f3857c8ac9e7ba4ffed80386.js application.js rails.png
application-95fca227f3857c8ac9e7ba4ffed80386.js.gz application.js.gz
Rails の manifest.yml
にそのファイルの記述があることも確認してください。
$ cat public/assets/manifest.yml
rails.png: rails-782b548cc1ba7f898cdad2d9eb8420d2.png
application.js: application-95fca227f3857c8ac9e7ba4ffed80386.js
application.css: application-95bd4fe1de99c1cd91ec8e6f348a44bd.css
探しているファイルが見つからない場合は、bundle exec rake assets:precompile RAILS_ENV=production
をローカルで実行して、当該のファイルがご自身の public/assets
ディレクトリにあることを確認してください。