Skip to content

building a rails 3 application with the memcache addon

iwhurtafly edited this page Oct 28, 2012 · 6 revisions

webアプリケーションへキャッシュを追加することにより、劇的にパフォーマンスを改善することが出来ます。 複雑なDBへの問い合わせ結果や、高度な計算処理、外部リソースへの時間の要するコール、これらの結果はシンプルなキー値へアーカイブすることが可能です。キー値へは高速なO(1)探索を経由してアクセス可能です。

Rails 3.1でStatic asset caching + Rack::Cacheを使う場合の概要は、[こちらの記事]にあります。

このチュートリアルでは、シンプルなRails 3.2アプリケーションをクリエートし、Herokuへデプロイ、そして高度なクエリーの結果をキャッシュするためにMemcache Add-onを使用することで学んで行きます。

サンプルコードは、GitHubに上がっています。[demo Rails application](https://github.com/mattmanning/memcache-example)

アプリケーションのクリエート

スケルトンの作成のため、railsコマンドを使います:

:::term
$ rails new memcache-example
$ cd memcache-example/

Gemfile内の下記個所を変更して下さい:

:::ruby
gem 'sqlite3'

:::ruby
group :development do
  gem 'sqlite3'
end

group :production do
  gem 'pg'
end

へ変更して下さい。

こうすることで、本番環境のアプリが、Postgresのデータベースを使用することを保証します。

次に以下のコマンドを実行して下さい。:

:::term
$ bundle install --without production

このコマンドは、明記されたgemsのみをインストールするために使います。そしてGemfile.lockというファイルが作成されます。 --without productionオプションは、pgのgemがローカルにインストールされることを防ぎます。

このプロジェクトをGitのリポジトリに作成し、変更をコミットして下さい。:

:::term
$ git init
$ git add .
$ git commit -m "first commit"

Herokuへのデプロイ

Heroku上に新しいアプリケーションを作成するために、herokuコマンドを使用して下さい:

:::term
$ heroku create --stack cedar

その後で、Herokuへデプロイして下さい。:

:::term
$ git push heroku master

Memcacheアドオンをインストールし、キャッシュを設定して下さい。

Memcacheこの記事にある通り、 Memcacheアドオン以外に、dalliというgemもインストールが必要です。:

ターミナルで以下を実行します。:

:::term
$ heroku addons:add memcache:5mb

dalli(memcacheクライアントのライブラリ)をインクルードするために、Gemfileを変更して下さい。:

:::ruby
gem 'dalli'

dalliにより提供されるキャッシュの保管を利用するために、デフォルトで使用されるRailsのキャッシュを設定して下さい。 config/environments/production.rbにあるファイルに、下記を含めます。

:::ruby
config.cache_store = :dalli_store

このサンプルアプリがどのように動作するかを簡単に検証するために、元々備わっているキャッシュの機能を一時的にオフに して下さい。下記の箇所をコメント化します。:

:::ruby
# config.action_controller.perform_caching = true

機能の追加

名前とemailアドレスだけのシンプルなテーブルへ、データの格納と参照を行うために、 Railsのscaffold generatorを使用して下さい。:

:::term
$ rails g scaffold contact name:string email:string
$ rake db:migrate

rootのルートへcontacts#indexをセットするために、config/routes.rbを下記のように編集して下さい。

:::ruby
root :to => 'contacts#index'

それから、public/index.htmlを削除して下さい。

変更をコミットし、Herokuへプッシュして下さい。下記のコマンドを使用し、リモートのデータベースをマイグレートして下さい。:

:::term
$ heroku run rake db:migrate

contactsのリストを確認するため、heroku openコマンドを使用すれば、Heroku上に作成したアプリを確認することが出来るでしょう。 "New Contact"のリンクを押し、いくつかのレコードを作成して下さい。

キャッシュの追加

現在のContactsController内のコードは、このような感じかと思います。:

:::ruby
def index
    @contacts = Contact.all

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @contacts }
    end
end

/contactsがリクエストされる度に、indexメソッドが実行され、contactsテーブル内の全てのレコードをフェッチする クエリーが走ります。

テーブルが小規模で、リクエストされる量も少ない場合、あまり問題にはなりませんが、 データ量が増え、ユーザー数も増えた場合、アプリケーションのパーフォーマンスへ与えるインパクトは大きくなります。 Contact.allの結果をキャッシュしてみましょう。こうすることで、同一のページを訪問する度に、データベースへの 問い合わせが走ることはありません。

`Rails.cache.fetch`メソッドは、キー値の引数とブロックを設定することが出来ます。もしキー値が存在すれば、 対応する値が返されます。もしキー値が存在しなければ、ブロックが実行され、与えられたキー値に値が格納され、返されます。

app/models/contact.rbに下記のメソッドを追加して下さい。:

:::ruby
def self.all_cached
  Rails.cache.fetch('Contact.all') { all }
end

app/controllers/contacts_controller.rb内の

:::ruby
@contacts = Contact.all

を下記に変更して下さい。

:::ruby
@contacts = Contact.all_cached

さらに、indexのページに統計情報を表示させてみましょう。app/controllers/contacts_controller.rb内の indexメソッドへ、下記の行を追加して下さい。:

:::ruby
@stats = Rails.cache.stats.first.last

それから、app/views/contacts/index.html.erb内の一番最終行に、下記のマークアップを追加して下さい。:

:::html
<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へプッシュして下さい。/contactsのページをリフレッシュすると、"Cache misses: 1"と 表示されるでしょう。これは、'Contact.all'キー値でフェッチしようと試みたものの、値が存在しなかったことを意味しています。 再度、ページのリフレッシュを行って下さい。そうすると、"Cache hits: 1"と表示されるでしょう。 今回は、先ほどのリクエストにより値が格納されたため、'Contact.all'のキー値が存在することとなります。

Herokuのコンソールからキャッシュをクリアしてみると、再びこの効果を確認することが出来ます。:

:::term
$ heroku run console
>> Rails.cache.clear

キャッシュの失効

Contact.allがキャッシュされるようになりましたが、テーブルの値が変更された時、何が起こるでしょうか? 新たにcontactのレコードを追加し、リストのページに戻ってみて下さい。新たに追加したはずのcontactが表示されないでしょう。 これは、Contact.allがキャッシュされるので、古いテーブルの値が格納され、表示されていることが原因となります。 テーブルの値が変更された時に、キャッシュを失効させる方法が必要となります。これは、Contactモデルに フィルターを追加することで対応可能となります。

下記のコードをapp/models/contact.rbに追加して下さい。:

:::ruby
after_save    :expire_contact_all_cache
after_destroy :expire_contact_all_cache

def expire_contact_all_cache
  Rails.cache.delete('Contact.all')
end

これらの修正をコミットし、Herokuへプッシュして下さい。テーブルへcontactを格納(追加または更新)するか、contactを 削除する毎に、Contact.allのキャッシュのキー値が削除されます。テーブルの値を変更し、/contactsのページへ戻る度に、 "Cache misses"のカウントが1ずつ増加しているのが確認出来るでしょう。

ビルトイン済みのRails Action Cachingについて

上記の例では、キャッシュのフェッチと失効について、詳細な説明を行っています。便利なことに、Railsは、このような機能の ほとんどをビルトインで提供してくれます。例えば、もしshowアクションの結果をキャッシュしたいのであれば、 app/controllers/contacts_controller.rb内に下記の行を追加して下さい。:

:::ruby
caches_action :show

適切にキャッシュを失効させるために、contacts_controller.rb内のupdatedestroyメソッドの両方へ 下記の行を追加して下さい。

:::ruby
expire_action :action => :show

Action Cachingは、ページキャッシュと同じような形で、オブジェクトとビューをキャッシュしますが、 リクエストがアプリケーションスタックにヒットするので、認証のようなものにbeforeフィルターを適用することが出来ます。

さらに理解を深めるために、Rails Guide on Cachingを 読んで下さい。Railsにビルトインされているアクション、フラグメント等へのキャッシュ機能に関し、素晴らしい情報が記載されています。

コードについて

このチュートリアルで作成されたアプリケーションの全ソースは、GitHub上から、自由にダウンロード可能となっています。

Clone this wiki locally