Table of Contents [expand]
Web アプリケーションにキャッシングを追加して、パフォーマンスを大きく改善することができます。複雑なデータベースクエリ、コストの高い計算、または時間のかかる外部リソース呼び出しの結果を、単純な key-value ストアにアーカイブできます。このストアには、高速な O(1) ルックアップを使用してアクセスできます。
Rails 3.1 以降での Rack::Cache を使用した静的アセットキャッシングについては この記事で説明しています
このチュートリアルでは、単純な Rails 5 アプリケーションを作成して Heroku にデプロイし、MemCachier アドオン を使用してコストの高いクエリをキャッシュする手順を解説します。
前提条件
- Ruby/Rails に関する基本的な知識
- Ruby 2.2 以降、Rubygems、Bundler、Rails 5 以降のローカルにインストールされたバージョン。 注: このガイドは Rails 3 および 4 にも対応しています。
- Git に関する基本的な知識
- Heroku ユーザーアカウント。 無料ですぐにサインアップできます。
アプリケーションの作成
rails
コマンドを使用してアプリのスケルトンを生成します。
$ rails new memcache-example
$ cd memcache-example/
まず、Gemfile で ruby バージョンを指定します。
ruby '2.5.0'
次に、データベースをセットアップします。Gemfile で、次の行を変更します。
gem 'sqlite3'
変更後は次のとおりです。
group :development do
gem 'sqlite3'
end
group :production do
gem 'pg', '~>0.21'
end
これにより、アプリは本番環境で必ず Postgres データベースを利用します。注: pg
へのバージョン制約の追加は今後不要になる可能性がありますが、現在の Rails バージョン 5.1.4
は現在の pg バージョン 1.0.0
と互換性がありません。
ここで、次のように実行して
$ bundle install --without production
Gemfile.lock
ファイルを更新します。--without production
オプションを指定すると、pg gem がローカルにインストールされなくなります。
変更をコミットします (注: 古いバージョンの Rails を使用している場合、先に git init
で Git リポジトリを作成することが必要な場合があります)。
$ git add .
$ git commit -m "Initial rails app."
Heroku にデプロイ
heroku
コマンドを使用して、新しい Heroku アプリをプロビジョニングします。
$ heroku create
次に、Heroku にデプロイします。
$ git push heroku master
MemCachier アドオンのインストールとキャッシングの設定
MemCachier の記事で述べたように、アドオンと dalli
gem をインストールする必要があります。オプションの memcachier
gem も推奨されます。
ターミナルで次のように実行します。
$ heroku addons:create memcachier:dev
Gemfile を変更して、memcache クライアントライブラリ dalli
と、セットアップに役立つ単純な gem である memcachier
を含めます。
gem 'dalli'
その後、次のように実行します。
$ bundle install --without production
これにより、追加された gem がインストールされ、Gemfile.lock
ファイルが更新されます。
ここで、config/environments/production.rb
を変更して次の内容を含めることにより、dalli
によって提供されるキャッシュストアを利用するようにデフォルトの Rails キャッシングを
設定します。
config.cache_store = :mem_cache_store,
(ENV["MEMCACHIER_SERVERS"] || "").split(","),
{:username => ENV["MEMCACHIER_USERNAME"],
:password => ENV["MEMCACHIER_PASSWORD"],
:failover => true,
:socket_timeout => 1.5,
:socket_failure_delay => 0.2,
:down_retry_delay => 60
}
この例がどのように動作するかを確認しやすくするために、組み込みのキャッシングを一時的に無効化します。
config.action_controller.perform_caching = false
機能の追加
Rails scaffold ジェネレーターを使用して、名前とメールアドレスの単純なディレクトリを保存および表示するためのインターフェースを作成します。
$ rails g scaffold contact name:string email:string
$ rake db:migrate
config/routes.rb
を編集して、contacts#index
を root ルートとして設定します。
root :to => 'contacts#index'
注: Rails 3 アプリで public/index.html
を削除できるようになりました。
変更をコミットし、Heroku にプッシュし、次のコマンドを使用してリモートデータベースを移行します。
$ git add .
$ git commit -m "Added first model."
$ git push heroku master
$ heroku run rake db:migrate
heroku open
を使用してアプリにアクセスし、連絡先のリストを表示できるようになったはずです。"New Contact" (新しい連絡先) リンクから、いくつかのレコードを作成します。
キャッシングの追加
ContactsController
のコードは次のようになります。
def index
@contacts = Contact.all
end
/contacts
がリクエストされるたびに、index
メソッドが実行され、連絡先テーブル内のすべてのレコードを取得するためのデータベースクエリが実行されます。
テーブルが小さくリクエストの分量が少ない場合、これはあまり問題になりませんが、データベースとユーザーの規模が拡大すると、このようなクエリがアプリのパフォーマンスに影響を及ぼす可能性があります。このページにアクセスするたびにデータベースクエリが実行されないよう、Contact.all
の結果をキャッシュしましょう。
Rails.cache.fetch
メソッドはキー引数とブロックを取ります。キーが
存在する場合、それに対応する値が返されます。キーが
存在しない場合、ブロックが実行され、提供されたキーで値が保存されてから
返されます。
app/models/contact.rb
で、次のメソッドを Contact クラスに追加します。
def self.all_cached
Rails.cache.fetch('Contact.all') { all.to_a }
end
app/controllers/contacts_controller.rb
で、次の部分を変更します。
@contacts = Contact.all
変更後は次のとおりです。
@contacts = Contact.all_cached
all
の代わりに all.to_a
をキャッシュすることに注意してください。これは、Rails 4 の Model.all
は遅延実行され、実際の連絡先をキャッシュするためには to_a
を使用して Contact.all
を 配列に変換する必要があるからです。
インデックスページにもいくつかの統計を表示してみましょう。app/controllers/contacts_controller.rb
で、次の行を index
メソッドに追加します。
@stats = Rails.cache.stats.first.last
次のマークアップを app/views/contacts/index.html.erb
の下部に追加します。
<h1>Cache Stats</h1>
<table>
<tr>
<th>Metric</th>
<th>Value</th>
</tr>
<tr>
<td>Cache hits:</td>
<td><%= @stats['get_hits'] %></td>
</tr>
<tr>
<td>Cache misses:</td>
<td><%= @stats['get_misses'] %></td>
</tr>
<tr>
<td>Cache flushes:</td>
<td><%= @stats['cmd_flush'] %></td>
</tr>
</table>
結果をコミットして Heroku にプッシュします。
$ git commit -am "Add caching."
$ git push heroku master
/contacts
ページを更新すると “Cache misses: 1” と表示されます。これは、'Contact.all'
キーの取得を試みたものの存在しなかったためです。 もう一度更新すると、今度は “Cache hits: 1” と表示されます。 前回のリクエスト中に 'Contact.all'
キーが保存されたため、今回はこのキーが存在していました。
Heroku コンソールでキャッシュをクリアすると、この効果をもう一度確認できます。
$ heroku run console
>> Rails.cache.clear
キャッシュの期限切れ処理
Contact.all
がキャッシュされるようになりましたが、そのテーブルが変更されるとどうなるでしょうか。新しい連絡先を追加し、リスト表示ページに戻ってみてください。
新しい連絡先が表示されないことがわかります。Contact.all
が
キャッシュされているので、古い値がまだ表示されています。何か変更があったときにキャッシュの値を期限切れにする方法が必要です。 これは、Contact
モデルでフィルターを使用して実現できます。
次のコードを app/models/contact.rb
の Contact クラスに追加します。
class Contact < ApplicationRecord
after_save :expire_contact_all_cache
after_destroy :expire_contact_all_cache
def expire_contact_all_cache
Rails.cache.delete('Contact.all')
end
#...
end
これらの変更をコミットして Heroku にプッシュします。
$ git commit -am "Expire cache."
$ git push heroku master
連絡先を保存 (作成または更新) または破棄するたびに Contact.all
キャッシュキーが削除されるようになったことがわかります。これらの変更のいずれかを行って /contacts
に戻るたびに “Cache misses” カウントが 1 ずつ増加するはずです。
組み込みの Rails キャッシング
上記の例では、キャッシュを明示的に取得して期限切れにする方法を説明しています。 便利なことに、Rails にはこの機能の多くが組み込まれています。 次のように
config.action_controller.perform_caching = true
config/environments/production.rb
で設定すると、Rails でフラグメント、アクション、ページのキャッシングを実行できます。
ここでは、これらのキャッシング手法を簡単に紹介するにとどめます。詳細および “ロシア人形方式” キャッシングなどのその他の手法については、 キャッシングに関する Rails ガイドを参照してください。
フラグメントキャッシング
Rails のページは、さまざまなコンポーネントで構成されるのが一般的です。これらのコンポーネントはフラグメントキャッシングでキャッシュできるため、ページがリクエストされるたびに再構築する必要はありません。
たとえば、今回の /contacts
ページは複数の連絡先コンポーネントで構成され、それぞれのコンポーネントで名前、メールアドレス、3 つのアクション (表示、編集、破棄) を表示しています。app/views/contacts/index.html.erb
で次の内容を @contacts.each
ループに追加することによって、これらのフラグメントをキャッシュできます。
# ...
<% @contacts.each do |contact| %>
<% cache contact do %>
# ...
<% end %>
<% end %>
# ...
アクションキャッシング
フラグメントに加えて、Rails ではページとアクションのキャッシングを使用してページ全体をキャッシュすることもできます。Rails スタックを完全にバイパスできるページキャッシングの方が効率的ですが、認証などの事前フィルターがあるページでは機能せず、ファイルストレージがない Heroku でのセットアップには少しコツが必要です。アクションキャッシングでは、ページキャッシングと同様にオブジェクトとビューを保存しますが、こちらは Rails スタックによって提供されます。
アクションキャッシングを使用するには、actionpack-action_caching gem を
Gemfile に追加して bundle install
を実行する必要があります。
gem 'actionpack-action_caching'
たとえば、show
アクションの結果をキャッシュするには、次の行を app/controllers/contacts_controller.rb
に追加します。
class ContactsController < ApplicationController
caches_action :show
# ...
end
適切な期限切れ処理のために、contacts_controller.rb
で次の行を update
メソッドと destroy
メソッドの両方に追加します。
def update
expire_action :action => :show
# ...
end
def destroy
expire_action :action => :show
# ...
end
アクションキャッシングを使用する場合でも、フラグメントキャッシングの重要性は変わらないことに注意してください。 フラグメントキャッシュにより、ページの有効期限が切れた場合でもページ全体をゼロから再構築する必要はなくキャッシュ済みのフラグメントを使用できることが保証されます。この手法は “ロシア人形方式” のキャッシングに似ています。
セッションのキャッシング
Memcache は非永続セッションに適した高速ストレージです (永続セッションにはデータベースを使用してください)。これは、各 dyno が独自の一時的なファイルシステムを備える Heroku で特に当てはまります。したがって Heroku では、ファイルシステムを使用してセッションを保存するデフォルトの動作が不整合につながる可能性があります。
キャッシュをセッションストレージに使用するには、次の内容で config/initializers/session_store.rb
ファイルを作成するか (Rails 5)、このファイルを編集して次の内容を含めます (Rails 3 および 4)。
# Be sure to restart your server when you modify this file.
Rails.application.config.session_store :cache_store, key: '_memcache-example_session'
参考情報と資料
このチュートリアルで構築したアプリケーションの完全なソースは GitHub から自由にダウンロードできます。