Railsアプリのパフォーマンスをデータベースリファクタリングとキャッシングで向上する [和訳]
本記事は英語版ブログで公開された記事の翻訳版です。
パフォーマンスはどんなアプリケーション開発者にとっても大きな懸念です。問題なのは、人は往々にして、実際にパフォーマンスが下がりはじめ懸念すべき事態になるまで問題を放置しがちだという点です。パフォーマンス対策は前もって講じておくべきもので、ほころびが出はじめてから取りかかるのでは遅すぎます。たまのレビューで済ませるのではなく、プロセスの一環に組み込むことが必要です。この記事では、パフォーマンス向上のために開発者が自分でできるいくつかの対策について見ていきます。まずはデータベースです。
データベースのパフォーマンス
Railsを使う場合、ActiveRecordやDataMapperといったORMを使えばアプリケーションからのデータ取得を楽にできますが、それだけにデータベースとのやりとりを改善する最適化やリファクタリングの重要性をおろそかにしてしまいがちです。ORMにすべて任せっきりでは、アプリケーションのライフサイクルが回るにつれて色々な問題が出てくる可能性があります。機能を追加するのと並行して、ORM呼び出しのリファクタリングにも取り組んでいかなくてはなりません。
開発段階では見つけにくいのですが、リファクタリングを行わないRailsでは、いわゆる「N+1問題」が発生する場合があります。N+1とは、1つのオブジェクトを呼び出すと別のオブジェクトも呼び出されてしまい、2つ目のクエリが発生することを指しています。これが積み重なると、たとえば100件の結果を得るにはクエリを1回実行すれば済むところを、実際には結果1件あたり100回もクエリを実行している、などという事態が起こります。開発段階で使うような小さいデータセットでは、これに気づくのはなかなか困難です。そのため、本番環境のデータベースに移行して初めてデータベースのパフォーマンス問題が発覚する場合もあります。
N+1問題を回避する方法の1つは、eager loadingを使うことです。そのためにはクエリコードに.includeを使います。たとえば、次のようなクエリコードがあるとしましょう。
# app/views/customers/index.html.erb
<% @customers.each do |customer| %>
<%= content_tag :h1, customer.name %>
<%= content_tag :h2, customer.addresses.first.city %>
<% end %>
# これだと100件のcustomerが登録されたデータベースに対し101回のクエリを発行してしまう
eagar loadingを使うために.includeを追加すると、次のようになります。
# app/controller.customers_controller.rb
class CustomersController < ApplicationController
def index
@customers = Customer.includes(:addresses).all
end
…
…
end
# これなら同じ100件のcustomerに対してもクエリは2回で済む
最初からこのテクニックを使っておけば、パフォーマンスが徐々に低下する問題を防ぐのに役立つでしょう。後からリファクタリングでやろうとすると、問題がどこで起きているのか突きとめるためにSQLバックログをさかのぼることになり、場合によっては大変です。そのような場合に問題の特定と解決の手間を軽減してくれるNew Relicのようなツールも存在します。
もう1つ、データベースパフォーマンスに影響を及ぼし、ひいてはアプリケーション全体のパフォーマンスにも関係する問題として、スロークエリの問題があります。スロークエリとは、データベースのパフォーマンスを低下させ、処理に時間がかかりすぎるクエリのことです。MySQLを使っているなら、スロークエリログを見てみると問題の特定に役立ちます。このログを見つけるには、データベースインスタンスに次のコマンドを発行します。
cat /db/mysql/log/slow_query.log
問題を起こしているテーブルが見つかったら、そこにインデックスを追加するのも1つの対策です。同じ1000行のテーブルでも、直接検索するよりインデックスを検索したほうが100倍高速です。インデックスを追加する間テーブルはロックされるので、インデックス作成中はテーブルに追加ができない点は注意が必要です。インデックスを追加するサンプルコードを下に挙げておきます。
class AddIndexForStuff
def change
add_index :stuff, :stuff_id
end
end
キャッシング
キャッシングとは、何度も使うものや将来使いそうなものをメモリに格納しておく技術です。Railsを使えば簡単ですが、いちばんいいのはアプリケーションを介さずに行うことです。Nginxなどを使えば静的ファイルをキャッシュできます。たとえばRailsとNginxを使うと、静的ファイルのページキャッシングはこのように簡単に行えます。
# #index、#showなどを生成
caches_page :index
# #creates、#update、#destroyなどを破棄
expire_page :action => :index
こうしてキャッシュしたオブジェクトを正しく配信するためには、Nginxにしかるべきセットアップをしてやる必要があります。フロントエンドサーバーを使う場合は下のようになります(注:このサンプルはウェブサーバーにUnicornを使う前提で書いてあります。Railsのウェブサーバーについて詳しくはこちらの記事をご覧ください)。
upstream upstream_enki {
server unix:/var/run/engineyard/unicorn_enki.sock fail_timeout=0;
}
location ~ ^/(images|assets|javascripts|stylesheets)/ {
try_files $uri $uri/index.html /last_assets/$uri /last_assets/$uri.html @app_enki;
expires 10y;
}
location / {
if (-f $document_root/system/maintenance.html) {return 503; }
try_files $uri $uri/index.html @app_enki;
}
これはEngine Yardアカウントにおける標準セットアップで、画像やアセットアップストリームといった静的アセットを移動させてページキャッシュできるようにし、パフォーマンス向上を図ります。確立したパスに該当するものがない場合、Nginxはアプリケーションをあたってそこにあるものを配信します。
ページキャッシングが使えない場合、パフォーマンス向上に最善の選択はmemcacheとなります。memcacheはRailsでは標準的なキャッシング技術で、使い方はごく簡単です。単純に、cache_storeをmem_cache_storeにセットして、次のようにmemcacheサーバーを追加してやればいいのです。
# config/intializers/memcached.rb
config.cache_store = :mem_cache_store,
"server-1:11211",
"server-2:11211",
"server-3:11211",
"server-4:11211"
memcasheをハッシュしてチャート化できる状態にするまでは、上のサンプルのようにRailsがやってくれますが、パフォーマンスを向上させ、期待通りの性能を得るには、なるべく複数のmemcacheサーバーを使うことをおすすめします。
アプリのパフォーマンスを向上する方法としては、他にアクションキャッシングというものもあります。これはページキャッシングに似ていますが、アクションのコンテンツをそっくりキャッシュストアに格納するという点が違います。良い点は、before_filtersに指定したものは別途呼び出してくれるというところです。これはアイテムをキャッシュしてパフォーマンスを高めつつも、検証やログイン用の機能だけは確実に呼び出すようにするためによく使います。
before_filter :make_sure_things_are_ok
caches_action :all_the_things
def all_the_things
@all_things = Thing.all_in_some_way
end
def expire
expire_action :action => :all_the_things
end
ここではキャッシングとデータベースパフォーマンスの2つだけを取り上げましたが、アプリケーションを最高の状態で走らせるために考慮すべき部分は他にもたくさんあります。最高のユーザーエクスペリエンスは、あらゆる開発者がめざす目標といえます。パフォーマンスの向上はそれを実現する1つの方法です。
(翻訳:福嶋美絵子)